From c24dddab1e723ffbd34e36c8f6bd01b93fd3ef22 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 31 Jul 2020 11:49:21 -0400 Subject: [PATCH 1/8] Offset Continue button by margin value --- .../app/document-capture/components/form-steps.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/app/document-capture/components/form-steps.jsx b/app/javascript/app/document-capture/components/form-steps.jsx index 729decfc7c8..ee48f1d9467 100644 --- a/app/javascript/app/document-capture/components/form-steps.jsx +++ b/app/javascript/app/document-capture/components/form-steps.jsx @@ -106,7 +106,12 @@ function FormSteps({ steps, onComplete }) { value={values} onChange={(nextValuesPatch) => setValues({ ...values, ...nextValuesPatch })} /> - From f2aa538bcc79ca59492a8a94974510beb33298a1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 31 Jul 2020 11:50:14 -0400 Subject: [PATCH 2/8] Enhance FileInput to support device-aware banner text --- app/assets/stylesheets/application.css.scss | 1 + .../stylesheets/components/_file-input.scss | 76 +++++++++++++++++++ .../components/file-input.jsx | 49 ++++++++---- .../components/file-input-spec.jsx | 53 ++++++++++++- 4 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 app/assets/stylesheets/components/_file-input.scss diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index face37fd71a..1c932bb043c 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -11,4 +11,5 @@ // up work should upgrade to use a newer USWDS, at which time this should be removed altogether. #document-capture-form { // scss-lint:disable IdSelector @import 'uswds/dist/scss/elements/form-controls/file-input'; + @import 'components/file-input'; } diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss new file mode 100644 index 00000000000..0a52baad5db --- /dev/null +++ b/app/assets/stylesheets/components/_file-input.scss @@ -0,0 +1,76 @@ +.id-card-file-input .usa-file-input { + @include u-margin-top(1); + max-width: 375px; + position: relative; + + &::after { + content: ''; + display: block; + // 2.125" x 3.375" are common standard ID dimensions + padding-bottom: ( ( 2.125 / 3.375 ) * 100 ) + unquote('%'); + } + + .usa-file-input__target { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + flex-direction: column; + height: 100%; + margin-top: 0; + } + + &:not(.usa-file-input--has-value) .usa-file-input__target { + align-items: center; + justify-content: center; + } +} + +//================================================ +// Pending upstream Login Design System revisions: +//================================================ + +.usa-file-input:not(.usa-file-input--has-value) .usa-file-input__target { + border-color: color('primary'); + border-width: 2px; + + &:hover { + border-color: color('primary-darker'); + } +} + +.usa-file-input__banner-text { + @include u-font('sans', 'xl'); + @include u-margin-bottom(1); + display: block; + text-transform: uppercase; + color: color('primary'); +} + +.usa-file-input.usa-file-input--single-value.usa-file-input--has-value { + .usa-file-input__target { + display: flex; + flex-direction: column; + } + + .usa-file-input__preview-heading { + flex: 0 0 auto; + } + + .usa-file-input__preview { + min-height: 0; + flex: 0 1 100%; + padding: 0; + margin: 0; + background: #112e51; + } + + .usa-file-input__preview__image { + height: 100%; + width: auto; + margin-left: auto; + margin-right: auto; + } +} diff --git a/app/javascript/app/document-capture/components/file-input.jsx b/app/javascript/app/document-capture/components/file-input.jsx index 342d474a48a..5359fff8787 100644 --- a/app/javascript/app/document-capture/components/file-input.jsx +++ b/app/javascript/app/document-capture/components/file-input.jsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import FileImage from './file-image'; +import DeviceContext from '../context/device'; import useInstanceId from '../hooks/use-instance-id'; import useI18n from '../hooks/use-i18n'; @@ -15,14 +16,15 @@ export function isImageFile(file) { return /^image\//.test(file.type); } -function FileInput({ label, hint, accept, value, onChange }) { +function FileInput({ label, hint, bannerText, accept, value, onChange, className }) { const { t, formatHTML } = useI18n(); const instanceId = useInstanceId(); + const { isMobile } = useContext(DeviceContext); const inputId = `file-input-${instanceId}`; const hintId = `${inputId}-hint`; return ( - <> +
{/* * Disable reason: The Airbnb configuration of the `jsx-a11y` rule is strict in that it * requires _both_ the `for` attribute and nesting, to maximize support for assistive @@ -42,11 +44,19 @@ function FileInput({ label, hint, accept, value, onChange }) { {hint} )} -
+
- {value && ( + {value && !isMobile && (
- {t('doc_auth.forms.selected_file')}{' '} + {t('doc_auth.forms.selected_file')}: + {value.name}{' '} {t('doc_auth.forms.change_file')}
)} @@ -57,14 +67,17 @@ function FileInput({ label, hint, accept, value, onChange }) { )} {!value && ( )}
@@ -80,23 +93,27 @@ function FileInput({ label, hint, accept, value, onChange }) { />
- +
); } FileInput.propTypes = { label: PropTypes.string.isRequired, hint: PropTypes.string, + bannerText: PropTypes.string, accept: PropTypes.arrayOf(PropTypes.string), value: PropTypes.instanceOf(window.File), onChange: PropTypes.func, + className: PropTypes.string, }; FileInput.defaultProps = { - accept: [], hint: null, + bannerText: null, + accept: [], value: undefined, onChange: () => {}, + className: null, }; export default FileInput; diff --git a/spec/javascripts/app/document-capture/components/file-input-spec.jsx b/spec/javascripts/app/document-capture/components/file-input-spec.jsx index 4ebd071bd00..a0d66e3a684 100644 --- a/spec/javascripts/app/document-capture/components/file-input-spec.jsx +++ b/spec/javascripts/app/document-capture/components/file-input-spec.jsx @@ -1,10 +1,12 @@ import React from 'react'; import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; +import { expect } from 'chai'; import render from '../../../support/render'; import FileInput, { isImageFile, } from '../../../../../app/javascript/app/document-capture/components/file-input'; +import DeviceContext from '../../../../../app/javascript/app/document-capture/context/device'; describe('document-capture/components/file-input', () => { describe('isImageFile', () => { @@ -17,6 +19,12 @@ describe('document-capture/components/file-input', () => { }); }); + it('renders with custom className', () => { + const { container } = render(); + + expect(container.firstChild.classList.contains('my-custom-class')).to.be.true(); + }); + it('renders file input with label', () => { const { getByLabelText } = render(); @@ -26,6 +34,14 @@ describe('document-capture/components/file-input', () => { expect(input.type).to.equal('file'); }); + it('renders decorative banner text', () => { + const { getByText } = render( + , + ); + + expect(getByText('File Goes Here', { hidden: true })).to.be.ok(); + }); + it('renders an optional hint', () => { const { getByLabelText } = render(); @@ -36,7 +52,7 @@ describe('document-capture/components/file-input', () => { }); it('renders a value preview', async () => { - const { findByRole, getByLabelText } = render( + const { container, findByRole, getByLabelText } = render( , ); @@ -45,6 +61,9 @@ describe('document-capture/components/file-input', () => { expect(input).to.be.ok(); expect(preview.getAttribute('src')).to.match(/^data:image\/png;base64,/); + expect(container.querySelector('.usa-file-input__preview-heading').textContent).to.equal( + 'doc_auth.forms.selected_file: demo doc_auth.forms.change_file', + ); }); it('does not render preview if value is not image', async () => { @@ -88,6 +107,38 @@ describe('document-capture/components/file-input', () => { expect(onChange.getCall(1).args[0]).to.equal(file2); }); + it('omits desktop-relevant details in mobile context', async () => { + const { container, getByText, findByRole, rerender } = render( + + + , + ); + + expect(getByText('doc_auth.forms.choose_file_html', { hidden: true })).to.be.ok(); + + rerender( + + + , + ); + + expect(() => getByText('doc_auth.forms.choose_file_html', { hidden: true })).to.throw(); + expect(getByText('File goes here', { hidden: true })).to.be.ok(); + + rerender( + + + , + ); + + await findByRole('img', { hidden: true }); + expect(container.querySelector('.usa-file-input__preview-heading')).to.not.be.ok(); + }); + it.skip('supports change by drag and drop', () => {}); it.skip('shows an error state', () => {}); From f73e23bd406854c5020c384c997569d1b8ce0954 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 31 Jul 2020 16:13:26 -0400 Subject: [PATCH 3/8] Configure SCSS Lint to respect BEM selector format **Why**: Login Design System uses BEM --- .scss-lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.scss-lint.yml b/.scss-lint.yml index 29a53d33b4a..972f6595534 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -5,3 +5,5 @@ exclude: linters: ColorVariable: enabled: false + SelectorFormat: + convention: hyphenated_BEM From ba6a729952d2d1168ce76253e934ccd6ecaf3651 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 31 Jul 2020 11:51:00 -0400 Subject: [PATCH 4/8] Render FileInput with strings from design --- app/assets/javascripts/i18n-strings.js.erb | 7 ++++-- .../stylesheets/components/_file-input.scss | 24 +++++++++---------- .../components/documents-step.jsx | 6 +++-- config/locales/doc_auth/en.yml | 2 ++ config/locales/doc_auth/es.yml | 2 ++ config/locales/doc_auth/fr.yml | 2 ++ .../components/document-capture-spec.jsx | 6 ++--- .../components/documents-step-spec.jsx | 8 +++---- 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/i18n-strings.js.erb b/app/assets/javascripts/i18n-strings.js.erb index 8136cb8cdcf..19138b2dc25 100644 --- a/app/assets/javascripts/i18n-strings.js.erb +++ b/app/assets/javascripts/i18n-strings.js.erb @@ -52,10 +52,13 @@ window.LoginGov = window.LoginGov || {}; 'doc_auth.forms.selected_file', 'doc_auth.forms.change_file', 'doc_auth.forms.choose_file_html', + 'doc_auth.headings.back', 'doc_auth.headings.document_capture', - 'doc_auth.headings.upload_front', - 'doc_auth.headings.upload_back', + 'doc_auth.headings.document_capture_front', + 'doc_auth.headings.document_capture_back', + 'doc_auth.headings.front', 'doc_auth.tips.document_capture_header_text', + 'doc_auth.tips.document_capture_hint', 'doc_auth.tips.document_capture_id_text1', 'doc_auth.tips.document_capture_id_text2', 'doc_auth.tips.document_capture_id_text3', diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss index 0a52baad5db..a3917cc9fd1 100644 --- a/app/assets/stylesheets/components/_file-input.scss +++ b/app/assets/stylesheets/components/_file-input.scss @@ -7,19 +7,19 @@ content: ''; display: block; // 2.125" x 3.375" are common standard ID dimensions - padding-bottom: ( ( 2.125 / 3.375 ) * 100 ) + unquote('%'); + padding-bottom: ((2.125 / 3.375) * 100) + unquote('%'); } .usa-file-input__target { - position: absolute; - top: 0; - right: 0; bottom: 0; - left: 0; display: flex; flex-direction: column; height: 100%; + left: 0; margin-top: 0; + position: absolute; + right: 0; + top: 0; } &:not(.usa-file-input--has-value) .usa-file-input__target { @@ -44,9 +44,9 @@ .usa-file-input__banner-text { @include u-font('sans', 'xl'); @include u-margin-bottom(1); + color: color('primary'); display: block; text-transform: uppercase; - color: color('primary'); } .usa-file-input.usa-file-input--single-value.usa-file-input--has-value { @@ -58,19 +58,19 @@ .usa-file-input__preview-heading { flex: 0 0 auto; } - + .usa-file-input__preview { - min-height: 0; + background: #112e51; flex: 0 1 100%; - padding: 0; margin: 0; - background: #112e51; + min-height: 0; + padding: 0; } - + .usa-file-input__preview__image { height: 100%; - width: auto; margin-left: auto; margin-right: auto; + width: auto; } } diff --git a/app/javascript/app/document-capture/components/documents-step.jsx b/app/javascript/app/document-capture/components/documents-step.jsx index 3d82f607b69..b2478e3ac0e 100644 --- a/app/javascript/app/document-capture/components/documents-step.jsx +++ b/app/javascript/app/document-capture/components/documents-step.jsx @@ -29,16 +29,18 @@ function DocumentsStep({ value, onChange }) { {!isMobile &&
  • {t('doc_auth.tips.document_capture_id_text4')}
  • } {DOCUMENT_SIDES.map((side) => { - const label = t(`doc_auth.headings.upload_${side}`); const inputKey = `${side}_image`; return ( onChange({ [inputKey]: nextValue })} + className="id-card-file-input" /> ); })} diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index d840943fd57..e2016e4f217 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -24,6 +24,7 @@ en: zip_code: Zip Code headings: address: Mailing Address + back: Back capture_complete: We have verified your state issued ID document_capture: Add your state-issued ID document_capture_back: Back of your ID @@ -32,6 +33,7 @@ en: document_capture_heading_with_selfie_html: Add your state‑issued ID and selfie document_capture_selfie: Your photo + front: Front selfie: Take a selfie. ssn: Please enter your social security number. take_pic_back: Take a photo of the back of your ID diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 60e139b9ddf..1c8cc75b10a 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -25,6 +25,7 @@ es: zip_code: Código postal headings: address: Dirección de envio + back: Parte posterior capture_complete: Hemos verificado la identificación emitida por su estado document_capture: Agregue su identificación emitida por el estado document_capture_back: Detrás de su identificación @@ -33,6 +34,7 @@ es: document_capture_heading_with_selfie_html: Cargue su identificación emitida por el estado y una foto suya document_capture_selfie: Tu foto + front: Frente selfie: Toma una selfie. ssn: Por favor ingrese su número de seguro social. take_pic_back: Toma una foto de la parte posterior de tu identificación diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 2906b0ea405..7497f89fd8e 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -26,6 +26,7 @@ fr: zip_code: Code postal headings: address: Adresse mail + back: Verso capture_complete: Nous avons vérifié votre ID émis par l'état document_capture: Ajoutez votre pièce d'identité émise par l'État document_capture_back: Dos de votre pièce d'identité @@ -35,6 +36,7 @@ fr: document_capture_heading_with_selfie_html: Téléchargez votre pièce d'identité officielle et une photo de vous document_capture_selfie: Ta photo + front: De face selfie: Prendre un selfie. ssn: S'il vous plaît entrez votre numéro de sécurité sociale. take_pic_back: Prenez une photo au verso de votre identifiant diff --git a/spec/javascripts/app/document-capture/components/document-capture-spec.jsx b/spec/javascripts/app/document-capture/components/document-capture-spec.jsx index a4a2b5938e9..42a32b3256c 100644 --- a/spec/javascripts/app/document-capture/components/document-capture-spec.jsx +++ b/spec/javascripts/app/document-capture/components/document-capture-spec.jsx @@ -7,7 +7,7 @@ describe('document-capture/components/document-capture', () => { it('renders the form steps', () => { const { getByText } = render(); - const step = getByText('doc_auth.headings.upload_front'); + const step = getByText('doc_auth.headings.document_capture_front'); expect(step).to.be.ok(); }); @@ -16,11 +16,11 @@ describe('document-capture/components/document-capture', () => { const { getByLabelText, getByText, findByText } = render(); userEvent.upload( - getByLabelText('doc_auth.headings.upload_front'), + getByLabelText('doc_auth.headings.document_capture_front'), new window.File([''], 'upload.png', { type: 'image/png' }), ); userEvent.upload( - getByLabelText('doc_auth.headings.upload_back'), + getByLabelText('doc_auth.headings.document_capture_back'), new window.File([''], 'upload.png', { type: 'image/png' }), ); userEvent.click(getByText('forms.buttons.continue')); diff --git a/spec/javascripts/app/document-capture/components/documents-step-spec.jsx b/spec/javascripts/app/document-capture/components/documents-step-spec.jsx index 7a54900dacf..3696cdf2b52 100644 --- a/spec/javascripts/app/document-capture/components/documents-step-spec.jsx +++ b/spec/javascripts/app/document-capture/components/documents-step-spec.jsx @@ -9,8 +9,8 @@ describe('document-capture/components/documents-step', () => { it('renders with front and back inputs', () => { const { getByLabelText } = render(); - const front = getByLabelText('doc_auth.headings.upload_front'); - const back = getByLabelText('doc_auth.headings.upload_back'); + const front = getByLabelText('doc_auth.headings.document_capture_front'); + const back = getByLabelText('doc_auth.headings.document_capture_back'); expect(front).to.be.ok(); expect(back).to.be.ok(); @@ -21,7 +21,7 @@ describe('document-capture/components/documents-step', () => { const { getByLabelText } = render(); const file = new window.File([''], 'upload.png', { type: 'image/png' }); - userEvent.upload(getByLabelText('doc_auth.headings.upload_front'), file); + userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), file); expect(onChange.calledOnce).to.be.true(); expect(onChange.getCall(0).args[0]).to.deep.equal({ front_image: file }); @@ -31,7 +31,7 @@ describe('document-capture/components/documents-step', () => { const onChange = sinon.spy(); const { getByLabelText } = render(); - const input = getByLabelText('doc_auth.headings.upload_front'); + const input = getByLabelText('doc_auth.headings.document_capture_front'); // Ideally this wouldn't be so tightly-coupled with the DOM implementation, but instead attempt // to upload a file of an invalid type. `@testing-library/user-event` doesn't currently support From 3f4b40907b89758e02a62d959400b1b83c9c3c0f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 31 Jul 2020 16:50:37 -0400 Subject: [PATCH 5/8] Ignore doc_auth strings in i18n unused task --- config/i18n-tasks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 30e83265c79..8c1d018419c 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -126,6 +126,7 @@ ignore_unused: - 'user_mailer.email_confirmation_instructions.subject' - 'valid_email.validations.email.invalid' - 'friendly_errors.*' + - 'doc_auth.*' # - 'simple_form.{yes,no}' # - 'simple_form.{placeholders,hints,labels}.*' # - 'simple_form.{error_notification,required}.:' From 7b73e68784032ce3d6cc00a542971650abc9cd0e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 3 Aug 2020 08:38:20 -0400 Subject: [PATCH 6/8] Use i18n-tasks-use hint to hint string usage --- .../app/document-capture/components/documents-step.jsx | 4 ++++ config/i18n-tasks.yml | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/app/document-capture/components/documents-step.jsx b/app/javascript/app/document-capture/components/documents-step.jsx index b2478e3ac0e..5b4daf431a6 100644 --- a/app/javascript/app/document-capture/components/documents-step.jsx +++ b/app/javascript/app/document-capture/components/documents-step.jsx @@ -34,8 +34,12 @@ function DocumentsStep({ value, onChange }) { return ( Date: Mon, 3 Aug 2020 11:34:56 -0400 Subject: [PATCH 7/8] Scale image preview to natural full width --- .../stylesheets/components/_file-input.scss | 69 ++++++++----------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss index a3917cc9fd1..c31d6e38261 100644 --- a/app/assets/stylesheets/components/_file-input.scss +++ b/app/assets/stylesheets/components/_file-input.scss @@ -1,30 +1,30 @@ .id-card-file-input .usa-file-input { - @include u-margin-top(1); max-width: 375px; - position: relative; - &::after { - content: ''; - display: block; - // 2.125" x 3.375" are common standard ID dimensions - padding-bottom: ((2.125 / 3.375) * 100) + unquote('%'); - } - - .usa-file-input__target { - bottom: 0; - display: flex; - flex-direction: column; - height: 100%; - left: 0; - margin-top: 0; - position: absolute; - right: 0; - top: 0; - } - - &:not(.usa-file-input--has-value) .usa-file-input__target { - align-items: center; - justify-content: center; + &:not(.usa-file-input--has-value) { + @include u-margin-top(1); + position: relative; + + .usa-file-input__target { + align-items: center; + bottom: 0; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + left: 0; + margin-top: 0; + position: absolute; + right: 0; + top: 0; + } + + &::after { + content: ''; + display: block; + // 2.125" x 3.375" are common standard ID dimensions + padding-bottom: ((2.125 / 3.375) * 100) + unquote('%'); + } } } @@ -49,28 +49,15 @@ text-transform: uppercase; } -.usa-file-input.usa-file-input--single-value.usa-file-input--has-value { - .usa-file-input__target { - display: flex; - flex-direction: column; - } - - .usa-file-input__preview-heading { - flex: 0 0 auto; - } - +.usa-file-input.usa-file-input--single-value { .usa-file-input__preview { - background: #112e51; - flex: 0 1 100%; - margin: 0; - min-height: 0; padding: 0; } .usa-file-input__preview__image { - height: 100%; + height: auto; margin-left: auto; - margin-right: auto; - width: auto; + margin-right: 0; + width: 100%; } } From 8f8a7275a5b3ff35930dc47a2854354af3f0c4bb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 3 Aug 2020 11:45:10 -0400 Subject: [PATCH 8/8] Offset focus border to appear outside dotted line --- .../stylesheets/components/_file-input.scss | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss index c31d6e38261..0f1330e6a54 100644 --- a/app/assets/stylesheets/components/_file-input.scss +++ b/app/assets/stylesheets/components/_file-input.scss @@ -32,12 +32,22 @@ // Pending upstream Login Design System revisions: //================================================ -.usa-file-input:not(.usa-file-input--has-value) .usa-file-input__target { - border-color: color('primary'); - border-width: 2px; +.usa-file-input__input { + outline-offset: 2px; +} + +.usa-file-input:not(.usa-file-input--has-value) { + .usa-file-input__target { + border-color: color('primary'); + border-width: 2px; + + &:hover { + border-color: color('primary-darker'); + } + } - &:hover { - border-color: color('primary-darker'); + .usa-file-input__input:focus { + outline-offset: 3px; } }