-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(accessibility): add password visibility toggle, use dedicated pa…
…ssword 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.
- Loading branch information
1 parent
3e49e6c
commit 43d6d27
Showing
19 changed files
with
272 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<boolean>(); | ||
|
||
/** | ||
* 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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/app/shared/formly/types/password-field/password-field.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<div class="password-field-wrapper" ishFocusOutside (isFocusedOutside)="onFocusOutside($event)"> | ||
<input | ||
[type]="showPassword ? 'text' : 'password'" | ||
[formControl]="formControl" | ||
[formlyAttributes]="field" | ||
class="form-control" | ||
[ngClass]="props.inputClass" | ||
[attr.data-testing-id]="field.key" | ||
[attr.aria-label]="props.ariaLabel | translate" | ||
[attr.aria-required]="props.required ? 'true' : undefined" | ||
[attr.aria-invalid]="showError ? 'true' : undefined" | ||
[attr.aria-describedby]="ariaDescribedByIds" | ||
(input)="onInput()" | ||
/> | ||
|
||
<button | ||
[attr.aria-label]=" | ||
showPassword ? ('form.password.hide.button.label' | translate) : ('form.password.show.button.label' | translate) | ||
" | ||
class="btn btn-link text-decoration-none" | ||
[ngClass]="{ visible: !isButtonDisabled }" | ||
type="button" | ||
[disabled]="isButtonDisabled" | ||
(click)="togglePasswordVisibility()" | ||
> | ||
<fa-icon *ngIf="showPassword" [icon]="['fas', 'eye-slash']" /> | ||
<fa-icon *ngIf="!showPassword" [icon]="['fas', 'eye']" /> | ||
</button> | ||
</div> |
70 changes: 70 additions & 0 deletions
70
src/app/shared/formly/types/password-field/password-field.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; | ||
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; | ||
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; | ||
import { TranslateModule } from '@ngx-translate/core'; | ||
import { MockComponent } from 'ng-mocks'; | ||
|
||
import { FormlyTestingComponentsModule } from 'ish-shared/formly/dev/testing/formly-testing-components.module'; | ||
import { FormlyTestingContainerComponent } from 'ish-shared/formly/dev/testing/formly-testing-container/formly-testing-container.component'; | ||
|
||
import { PasswordFieldComponent } from './password-field.component'; | ||
|
||
describe('Password Field Component', () => { | ||
let component: FormlyTestingContainerComponent; | ||
let fixture: ComponentFixture<FormlyTestingContainerComponent>; | ||
let element: HTMLElement; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [ | ||
FormlyModule.forRoot({ | ||
types: [ | ||
{ | ||
name: 'ish-password-field', | ||
component: PasswordFieldComponent, | ||
}, | ||
], | ||
}), | ||
FormlyTestingComponentsModule, | ||
ReactiveFormsModule, | ||
TranslateModule.forRoot(), | ||
], | ||
declarations: [MockComponent(FaIconComponent), PasswordFieldComponent], | ||
}).compileComponents(); | ||
}); | ||
|
||
beforeEach(() => { | ||
const testComponentInputs = { | ||
fields: [ | ||
{ | ||
key: 'password', | ||
type: 'ish-password-field', | ||
props: { | ||
required: true, | ||
}, | ||
} as FormlyFieldConfig, | ||
], | ||
form: new FormGroup({}), | ||
model: { | ||
displayValue: 'testValue', | ||
}, | ||
}; | ||
fixture = TestBed.createComponent(FormlyTestingContainerComponent); | ||
component = fixture.componentInstance; | ||
element = fixture.nativeElement; | ||
|
||
component.testComponentInputs = testComponentInputs; | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(component).toBeTruthy(); | ||
expect(element).toBeTruthy(); | ||
expect(() => fixture.detectChanges()).not.toThrow(); | ||
}); | ||
|
||
it('should be rendered after creation', () => { | ||
fixture.detectChanges(); | ||
expect(element.querySelector('ish-password-field')).toBeTruthy(); | ||
}); | ||
}); |
Oops, something went wrong.