diff --git a/app/assets/images/alert/error.svg b/app/assets/images/alert/error.svg deleted file mode 100644 index db7505a17f5..00000000000 --- a/app/assets/images/alert/error.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/stylesheets/components/_form.scss b/app/assets/stylesheets/components/_form.scss index 0c3a308ac61..077e2d376df 100644 --- a/app/assets/stylesheets/components/_form.scss +++ b/app/assets/stylesheets/components/_form.scss @@ -45,7 +45,6 @@ textarea { // error states .has-error input { - background-image: url(image-path('alert/error.svg')); background-position: center right $form-field-padding-x; background-repeat: no-repeat; background-size: 1rem 1rem; @@ -74,6 +73,18 @@ input::-webkit-inner-spin-button { [aria-invalid="true"] ~ & { display: block; } + + [aria-invalid="value-missing"] ~ &.display-if-invalid--value-missing { + display: block; + } + + [aria-invalid="pattern-mismatch"] ~ &.display-if-invalid--pattern-mismatch { + display: block; + } +} + +.usa-input--error.field:invalid { + @include u-border($theme-input-state-border-width, 'error'); } .usa-hint { diff --git a/app/javascript/packs/form-validation.js b/app/javascript/packs/form-validation.js index 5b2fdb38846..4b04d30bb9c 100644 --- a/app/javascript/packs/form-validation.js +++ b/app/javascript/packs/form-validation.js @@ -19,25 +19,41 @@ function disableFormSubmit(event) { }); } +function kebabCase(string) { + return string.replace(/(.)([A-Z])/g, '$1-$2').toLowerCase(); +} + +function resetInput(input) { + input.setCustomValidity(''); + input.setAttribute('aria-invalid', 'false'); + input.classList.remove('usa-input--error'); +} + /** * Given an `input` or `invalid` event, updates custom validity of the given input. * * @param {Event} event Input or invalid event. */ + function checkInputValidity(event) { const input = /** @type {HTMLInputElement} */ (event.target); - input.setCustomValidity(''); - input.setAttribute('aria-invalid', String(!input.validity.valid)); + resetInput(input); if ( event.type === 'invalid' && !input.validity.valid && input.parentNode?.querySelector('.display-if-invalid') ) { event.preventDefault(); + const errors = Object.keys(ValidityState.prototype) + .filter((key) => key !== 'valid') + .filter((key) => input.validity[key]); + + input.setAttribute('aria-invalid', errors.length ? kebabCase(errors[0]) : 'false'); + input.classList.add('usa-input--error'); input.focus(); } - const { I18n } = /** @type {typeof window & LoginGovGlobal} */ (window).LoginGov; + const { I18n } = /** @type {typeof window & LoginGovGlobal} */ (window).LoginGov; if (input.validity.valueMissing) { input.setCustomValidity(I18n.t('simple_form.required.text')); } else if (input.validity.patternMismatch) { diff --git a/app/javascript/packs/ial2-consent-button.js b/app/javascript/packs/ial2-consent-button.js index 409ddbc8485..345108c0481 100644 --- a/app/javascript/packs/ial2-consent-button.js +++ b/app/javascript/packs/ial2-consent-button.js @@ -5,7 +5,10 @@ function toggleButton() { function sync() { continueButton.classList.toggle('usa-button--disabled', !checkbox.checked); - errorMessage.classList.toggle('display-none', checkbox.getAttribute('aria-invalid') !== 'true'); + errorMessage.classList.toggle( + 'display-none', + checkbox.getAttribute('aria-invalid') !== 'value-missing', + ); } sync(); diff --git a/app/views/idv/doc_auth/agreement.html.erb b/app/views/idv/doc_auth/agreement.html.erb index 0f9a3ed0ba5..d88e745257e 100644 --- a/app/views/idv/doc_auth/agreement.html.erb +++ b/app/views/idv/doc_auth/agreement.html.erb @@ -34,7 +34,8 @@ <%= t('doc_auth.instructions.consent', app_name: APP_NAME) %> <%= new_window_link_to t('doc_auth.instructions.learn_more'), MarketingSite.security_and_privacy_practices_url %> - diff --git a/app/views/idv/doc_auth/send_link.html.erb b/app/views/idv/doc_auth/send_link.html.erb index f7a8bedad3e..45d8745acc2 100644 --- a/app/views/idv/doc_auth/send_link.html.erb +++ b/app/views/idv/doc_auth/send_link.html.erb @@ -17,7 +17,6 @@

<%= t('doc_auth.info.camera_required') %>

<%= t('doc_auth.instructions.send_sms') %>

- <%= validated_form_for( :doc_auth, url: url_for, diff --git a/app/views/idv/phone/new.html.erb b/app/views/idv/phone/new.html.erb index b4882668d1e..6e9f8606096 100644 --- a/app/views/idv/phone/new.html.erb +++ b/app/views/idv/phone/new.html.erb @@ -57,10 +57,15 @@ method: :put, class: 'margin-top-2', }) do |f| %> - <%= f.label :phone, label: t('idv.form.phone'), class: 'bold' %> - <%= f.input :phone, required: true, input_html: { aria: { invalid: false }, class: 'sm-col-8' }, label: false, - wrapper_html: { class: 'margin-right-2' } %> - + <%= f.input :phone, required: true, + input_html: { aria: { invalid: false }, + class: 'sm-col-8' }, + label: t('idv.form.phone'), + wrapper: false, + label_html: { class: 'bold' } %> + + <%= t('simple_form.required.text') %> +
<%= render('shared/spinner_button', action_message: t('doc_auth.info.verifying')) do %> <%= f.button :submit, t('forms.buttons.continue'), class: 'usa-button--big usa-button--wide' %> diff --git a/app/views/idv/review/new.html.erb b/app/views/idv/review/new.html.erb index 2548e53c99f..39e459b9869 100644 --- a/app/views/idv/review/new.html.erb +++ b/app/views/idv/review/new.html.erb @@ -26,8 +26,14 @@ current_user, url: idv_review_path, html: { autocomplete: 'off', method: :put } ) do |f| %> - <%= f.input :password, label: t('idv.form.password'), required: true, - input_html: { aria: { invalid: false }, class: 'password-toggle' } %> + +
+ <%= f.input :password, label: t('idv.form.password'), required: true, + input_html: { aria: { invalid: false }, class: 'password-toggle' }, wrapper: false %> + + <%= t('simple_form.required.text') %> + +
<%= t( 'idv.forgot_password.link_html', diff --git a/app/views/shared/_ssn_field.html.erb b/app/views/shared/_ssn_field.html.erb index ea333cd852b..4405b07d6bc 100644 --- a/app/views/shared/_ssn_field.html.erb +++ b/app/views/shared/_ssn_field.html.erb @@ -13,10 +13,18 @@ locals: <%= f.input( :ssn, as: :password, + wrapper: false, label: false, required: true, pattern: '^\d{3}-?\d{2}-?\d{4}$', maxlength: 11, - input_html: { aria: { invalid: false }, class: 'ssn ssn-toggle', value: '' } + input_html: { aria: { invalid: false }, class: 'ssn ssn-toggle usa-input', value: '' } ) %> + + <%= t('simple_form.required.text') %> + + + + <%= t('idv.errors.pattern_mismatch.ssn') %> + <%= javascript_packs_tag_once('ssn-field') %> diff --git a/app/views/sign_up/registrations/new.html.erb b/app/views/sign_up/registrations/new.html.erb index c66298a23a0..700752a886f 100644 --- a/app/views/sign_up/registrations/new.html.erb +++ b/app/views/sign_up/registrations/new.html.erb @@ -51,7 +51,7 @@ <%= t('sign_up.terms', app_name: APP_NAME) %> <%= new_window_link_to(t('titles.rules_of_use'), MarketingSite.rules_of_use_url) %> - diff --git a/app/views/users/rules_of_use/new.html.erb b/app/views/users/rules_of_use/new.html.erb index acc4479f02e..377e05fac9e 100644 --- a/app/views/users/rules_of_use/new.html.erb +++ b/app/views/users/rules_of_use/new.html.erb @@ -21,7 +21,7 @@ <%= t('users.rules_of_use.check_box_to_accept', app_name: APP_NAME) %> <%= new_window_link_to(t('titles.rules_of_use'), MarketingSite.rules_of_use_url) %> <% end %> - diff --git a/app/views/users/verify_account/index.html.erb b/app/views/users/verify_account/index.html.erb index b089354c1ad..5ba49883859 100644 --- a/app/views/users/verify_account/index.html.erb +++ b/app/views/users/verify_account/index.html.erb @@ -34,10 +34,14 @@ value: @code, }, label: t('forms.verify_profile.name'), - class: 'margin-bottom-5' %> + wrapper: false %> + + + <%= t('simple_form.required.text') %> + <%= f.button :submit, t('forms.verify_profile.submit'), - class: 'usa-button--big usa-button--full-width' %> + class: 'usa-button--big usa-button--full-width margin-top-5' %>
<% end %> diff --git a/spec/features/idv/doc_auth/ssn_step_spec.rb b/spec/features/idv/doc_auth/ssn_step_spec.rb index c85ae8fee91..9f1cb4d9dbb 100644 --- a/spec/features/idv/doc_auth/ssn_step_spec.rb +++ b/spec/features/idv/doc_auth/ssn_step_spec.rb @@ -27,9 +27,10 @@ it 'does not proceed to the next page with invalid info', js: true do fill_out_ssn_form_fail - expect(page.find('#doc_auth_ssn')['aria-invalid']).to eq('true') click_idv_continue + expect(page.find('#doc_auth_ssn')['aria-invalid']).to eq('value-missing') + expect(page).to have_current_path(idv_doc_auth_ssn_step) end