From 43d6d27ad993b971bc486e840e1165b948a28379 Mon Sep 17 00:00:00 2001 From: Andreas Steinmann Date: Wed, 11 Sep 2024 16:05:53 +0200 Subject: [PATCH] feat(accessibility): add password visibility toggle, use dedicated password component and update error indicators (#1694) * Add functionality to toggle password visibility for password fields. * Replace TextInputFieldComponent with PasswordFieldComponent to provide password-specific functionality. * Update error field indicator from "x" to "!" for improved clarity and accessibility. BREAKING CHANGES: The TextInputFieldComponent is no longer used for password fields. Instead, the PasswordFieldComponent is now used, which includes functionality to toggle password visibility. Password validators are applied to each instance as needed. --- src/app/core/directives.module.ts | 3 + .../directives/focus-outside.directive.ts | 32 +++++++++ src/app/core/icon.module.ts | 6 ++ .../punchout-user-form.component.ts | 11 ++- .../account-profile-password.component.ts | 14 ++-- .../update-password-form.component.spec.ts | 2 +- .../update-password-form.component.ts | 11 ++- ...registration-form-configuration.service.ts | 11 ++- .../login/login-form/login-form.component.ts | 3 +- .../validation-icons.component.html | 4 +- .../password-field.component.html | 29 ++++++++ .../password-field.component.spec.ts | 70 +++++++++++++++++++ .../password-field.component.ts | 45 ++++++++++++ .../text-input-field.component.ts | 6 +- src/app/shared/formly/types/types.module.ts | 11 ++- src/assets/i18n/de_DE.json | 2 + src/assets/i18n/en_US.json | 2 + src/assets/i18n/fr_FR.json | 2 + src/styles/global/forms.scss | 37 ++++++++++ 19 files changed, 272 insertions(+), 29 deletions(-) create mode 100644 src/app/core/directives/focus-outside.directive.ts create mode 100644 src/app/shared/formly/types/password-field/password-field.component.html create mode 100644 src/app/shared/formly/types/password-field/password-field.component.spec.ts create mode 100644 src/app/shared/formly/types/password-field/password-field.component.ts diff --git a/src/app/core/directives.module.ts b/src/app/core/directives.module.ts index 90bc5490f4..f5597f0880 100644 --- a/src/app/core/directives.module.ts +++ b/src/app/core/directives.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { BrowserLazyViewDirective } from './directives/browser-lazy-view.directive'; import { ClickOutsideDirective } from './directives/click-outside.directive'; +import { FocusOutsideDirective } from './directives/focus-outside.directive'; import { IdentityProviderCapabilityDirective } from './directives/identity-provider-capability.directive'; import { IntersectionObserverDirective } from './directives/intersection-observer.directive'; import { LazyLoadingContentDirective } from './directives/lazy-loading-content.directive'; @@ -14,6 +15,7 @@ import { ServerHtmlDirective } from './directives/server-html.directive'; declarations: [ BrowserLazyViewDirective, ClickOutsideDirective, + FocusOutsideDirective, IdentityProviderCapabilityDirective, IntersectionObserverDirective, LazyLoadingContentDirective, @@ -25,6 +27,7 @@ import { ServerHtmlDirective } from './directives/server-html.directive'; exports: [ BrowserLazyViewDirective, ClickOutsideDirective, + FocusOutsideDirective, IdentityProviderCapabilityDirective, IntersectionObserverDirective, LazyLoadingContentDirective, diff --git a/src/app/core/directives/focus-outside.directive.ts b/src/app/core/directives/focus-outside.directive.ts new file mode 100644 index 0000000000..37c5ee0609 --- /dev/null +++ b/src/app/core/directives/focus-outside.directive.ts @@ -0,0 +1,32 @@ +import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core'; + +/** + * This directive can be set on a html tag to check focus outside of the tag. + * It only works on desktop devices, NOT on touch devices. + */ +@Directive({ + selector: '[ishFocusOutside]', +}) +export class FocusOutsideDirective { + private isTouchDevice: boolean; + + constructor(private elementRef: ElementRef) { + this.isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + } + + /** + * Event to tell the listener, when focus moves outside the target element + */ + @Output() isFocusedOutside = new EventEmitter(); + + /** + * Method to check if focus is outside of the targetElement. Emits true when focus moves outside. + */ + @HostListener('document:focusin', ['$event.target']) + onFocusIn(targetElement: ElementRef): void { + const focusedInside = this.elementRef.nativeElement.contains(targetElement); + if (!focusedInside && !this.isTouchDevice) { + this.isFocusedOutside.emit(true); + } + } +} diff --git a/src/app/core/icon.module.ts b/src/app/core/icon.module.ts index ccbc96a2c5..5f17f04471 100644 --- a/src/app/core/icon.module.ts +++ b/src/app/core/icon.module.ts @@ -22,6 +22,9 @@ import { faCog, faCogs, faEnvelope, + faExclamationTriangle, + faEye, + faEyeSlash, faFastForward, faFax, faGear, @@ -90,6 +93,9 @@ export class IconModule { faGlobeAmericas, faHeart, faHome, + faExclamationTriangle, + faEye, + faEyeSlash, faInbox, faInfoCircle, faList, diff --git a/src/app/extensions/punchout/shared/punchout-user-form/punchout-user-form.component.ts b/src/app/extensions/punchout/shared/punchout-user-form/punchout-user-form.component.ts index 748ff1e734..25fd939b11 100644 --- a/src/app/extensions/punchout/shared/punchout-user-form/punchout-user-form.component.ts +++ b/src/app/extensions/punchout/shared/punchout-user-form/punchout-user-form.component.ts @@ -4,7 +4,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; import { FormsService } from 'ish-shared/forms/utils/forms.service'; -import { SpecialValidators } from 'ish-shared/forms/validators/special-validators'; +import { SpecialValidators, formlyValidation } from 'ish-shared/forms/validators/special-validators'; import { PunchoutType, PunchoutUser } from '../../models/punchout-user/punchout-user.model'; @@ -90,12 +90,14 @@ export class PunchoutUserFormComponent implements OnInit { }, hideRequiredMarker: true, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, }, { key: 'passwordConfirmation', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', required: this.punchoutUser ? false : true, label: this.punchoutUser ? 'account.punchout.password.new.confirmation.label' @@ -103,6 +105,9 @@ export class PunchoutUserFormComponent implements OnInit { attributes: { autocomplete: 'new-password' }, hideRequiredMarker: true, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, validation: { messages: { required: 'account.punchout.password.confirmation.error.required', diff --git a/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.ts b/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.ts index 756fb9aca6..6e4be2e3fd 100644 --- a/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.ts +++ b/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.ts @@ -14,7 +14,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; import { FormsService } from 'ish-shared/forms/utils/forms.service'; -import { SpecialValidators } from 'ish-shared/forms/validators/special-validators'; +import { SpecialValidators, formlyValidation } from 'ish-shared/forms/validators/special-validators'; /** * The Account Profile Password Page Component displays a form for changing the user's password @@ -43,9 +43,8 @@ export class AccountProfilePasswordComponent implements OnInit, OnChanges { fieldGroup: [ { key: 'currentPassword', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', required: true, hideRequiredMarker: true, label: 'account.password.label', @@ -70,16 +69,21 @@ export class AccountProfilePasswordComponent implements OnInit, OnChanges { }, attributes: { autocomplete: 'new-password' }, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, }, { key: 'passwordConfirmation', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', required: true, hideRequiredMarker: true, label: 'account.update_password.newpassword_confirmation.label', }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, validation: { messages: { required: 'account.register.password_confirmation.error.default', diff --git a/src/app/pages/forgot-password/update-password-form/update-password-form.component.spec.ts b/src/app/pages/forgot-password/update-password-form/update-password-form.component.spec.ts index 6863e2ee4c..e59e6f094c 100644 --- a/src/app/pages/forgot-password/update-password-form/update-password-form.component.spec.ts +++ b/src/app/pages/forgot-password/update-password-form/update-password-form.component.spec.ts @@ -34,7 +34,7 @@ describe('Update Password Form Component', () => { it('should render forgot password form step 2 for password reminder', () => { fixture.detectChanges(); - expect(element.innerHTML.match(/ish-password-field/g)).toHaveLength(1); + expect(element.innerHTML.match(/ish-password-field/g)).toHaveLength(2); expect(element.innerHTML).toContain('password'); expect(element.innerHTML).toContain('passwordConfirmation'); diff --git a/src/app/pages/forgot-password/update-password-form/update-password-form.component.ts b/src/app/pages/forgot-password/update-password-form/update-password-form.component.ts index 3c7c6091f2..e3f823edad 100644 --- a/src/app/pages/forgot-password/update-password-form/update-password-form.component.ts +++ b/src/app/pages/forgot-password/update-password-form/update-password-form.component.ts @@ -4,7 +4,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core'; import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; import { FormsService } from 'ish-shared/forms/utils/forms.service'; -import { SpecialValidators } from 'ish-shared/forms/validators/special-validators'; +import { SpecialValidators, formlyValidation } from 'ish-shared/forms/validators/special-validators'; /** * The Update Password Form Component displays a Forgot Password Update Password form and triggers the submit. @@ -49,6 +49,9 @@ export class UpdatePasswordFormComponent implements OnInit { args: { 0: '7' }, }, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, validation: { messages: { minLength: 'account.update_password.new_password.error.length', @@ -57,13 +60,15 @@ export class UpdatePasswordFormComponent implements OnInit { }, { key: 'passwordConfirmation', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', required: true, hideRequiredMarker: true, label: 'account.register.password_confirmation.label', }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, validation: { messages: { required: 'account.register.password_confirmation.error.default', diff --git a/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts b/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts index 003aa915b4..732095ea91 100644 --- a/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts +++ b/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts @@ -16,7 +16,7 @@ import { Customer, CustomerRegistrationType } from 'ish-core/models/customer/cus import { User } from 'ish-core/models/user/user.model'; import { ConfirmLeaveModalComponent } from 'ish-shared/components/registration/confirm-leave-modal/confirm-leave-modal.component'; import { FieldLibrary } from 'ish-shared/formly/field-library/field-library'; -import { SpecialValidators } from 'ish-shared/forms/validators/special-validators'; +import { SpecialValidators, formlyValidation } from 'ish-shared/forms/validators/special-validators'; export interface RegistrationConfigType { businessCustomer?: boolean; @@ -285,16 +285,21 @@ export class RegistrationFormConfigurationService { }, attributes: { autocomplete: 'new-password' }, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, }, { key: 'passwordConfirmation', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', required: true, label: 'account.register.password_confirmation.label', attributes: { autocomplete: 'new-password' }, }, + validators: { + password: formlyValidation('password', SpecialValidators.password), + }, validation: { messages: { required: 'account.register.password_confirmation.error.default', diff --git a/src/app/shared/components/login/login-form/login-form.component.ts b/src/app/shared/components/login/login-form/login-form.component.ts index 66420c0620..3471efec11 100644 --- a/src/app/shared/components/login/login-form/login-form.component.ts +++ b/src/app/shared/components/login/login-form/login-form.component.ts @@ -73,9 +73,8 @@ export class LoginFormComponent implements OnInit { }, { key: 'password', - type: 'ish-text-input-field', + type: 'ish-password-field', props: { - type: 'password', label: 'account.login.password.label', labelClass: this.labelClass || 'col-md-3', fieldClass: this.inputClass || 'col-md-6', diff --git a/src/app/shared/formly/components/validation-icons/validation-icons.component.html b/src/app/shared/formly/components/validation-icons/validation-icons.component.html index 2af91f1158..e61747d28f 100644 --- a/src/app/shared/formly/components/validation-icons/validation-icons.component.html +++ b/src/app/shared/formly/components/validation-icons/validation-icons.component.html @@ -2,8 +2,8 @@