From 046af1f2d433fc2f08334c6e5d440af3d0da6b80 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 30 Mar 2018 13:26:12 -0700 Subject: [PATCH] Fixed autocomplete a11y for list options --- .../autocomplete/autocomplete.component.html | 13 +++++++--- .../autocomplete.component.spec.ts | 12 ++++++---- .../autocomplete/autocomplete.component.ts | 24 ++++++++++++------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.html b/ui/src/app/shared/autocomplete/autocomplete.component.html index e4b5addb6..977f346db 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.html +++ b/ui/src/app/shared/autocomplete/autocomplete.component.html @@ -22,7 +22,7 @@ class="dropdown-item" [class.active]="state.currentState.selected === i" [class.focused]="state.currentState.focused === i" - id="{{ id }}__option--{{ i }}" + [id]="this.getOptionId(i)" (focusout)="handleOptionBlur($event, i)" (click)="handleOptionClick(i)" (mousedown)="handleOptionMouseDown($event)" @@ -35,8 +35,15 @@ - diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.spec.ts b/ui/src/app/shared/autocomplete/autocomplete.component.spec.ts index a65503435..c7c75abf5 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.spec.ts +++ b/ui/src/app/shared/autocomplete/autocomplete.component.spec.ts @@ -226,20 +226,23 @@ describe('AutoComplete Input Component', () => { describe('handleEnter handler', () => { const opts = ['foo', 'bar', 'baz']; it('should call preventDefault on the provided event if the menu is currently open', () => { - const ev = { preventDefault: jasmine.createSpy('preventDefault') }; + const ev = { preventDefault: () => {} } as KeyboardEvent; + spyOn(ev, 'preventDefault'); instanceUnderTest.state.setState({ menuOpen: true}); instanceUnderTest.handleEnter(ev); expect(ev.preventDefault).toHaveBeenCalled(); }); it('should NOT call preventDefault on the provided event if the menu is not open', () => { - const ev = { preventDefault: jasmine.createSpy('preventDefault') }; + const ev = { preventDefault: () => { } } as KeyboardEvent; + spyOn(ev, 'preventDefault'); instanceUnderTest.state.setState({ menuOpen: false }); instanceUnderTest.handleEnter(ev); expect(ev.preventDefault).not.toHaveBeenCalled(); }); it('should call componentBlur if there is no selected option and the query is not in the options', () => { - const ev = { preventDefault: jasmine.createSpy('preventDefault') }; + const ev = { preventDefault: () => { } } as KeyboardEvent; + spyOn(ev, 'preventDefault'); spyOn(instanceUnderTest, 'handleComponentBlur'); instanceUnderTest.state.setState({ menuOpen: true, selected: -1 }); instanceUnderTest.handleEnter(ev); @@ -253,7 +256,8 @@ describe('AutoComplete Input Component', () => { it('should call handleOptionClick if there is no selected option but the query is in the options', () => { const i = 0; const val = opts[i]; - const ev = { preventDefault: jasmine.createSpy('preventDefault') }; + const ev = { preventDefault: () => { } } as KeyboardEvent; + spyOn(ev, 'preventDefault'); testHostInstance.configure({ options: opts }); instanceUnderTest.input.setValue(val); testHostFixture.detectChanges(); diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.ts b/ui/src/app/shared/autocomplete/autocomplete.component.ts index 340b02c64..86a2b4e33 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.ts +++ b/ui/src/app/shared/autocomplete/autocomplete.component.ts @@ -151,7 +151,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte }); } - handleViewMore($event: MouseEvent): void { + handleViewMore($event: MouseEvent | KeyboardEvent | Event): void { $event.preventDefault(); $event.stopPropagation(); this.handleInputBlur(); @@ -274,9 +274,12 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte handleDownArrow(event: KeyboardEvent): void { event.preventDefault(); - const isNotAtBottom = this.state.currentState.selected !== this.matches.length - 1; + let isNotAtBottom = this.state.currentState.selected !== this.matches.length - 1; + if (this.showMoreAvailable) { + isNotAtBottom = this.state.currentState.selected !== this.matches.length; + } const allowMoveDown = isNotAtBottom && this.state.currentState.menuOpen; - if (allowMoveDown || this.showMoreAvailable) { + if (allowMoveDown) { this.handleOptionFocus(this.state.currentState.selected + 1); } } @@ -289,7 +292,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte } } - handleEnter(event: KeyboardEvent | { preventDefault: () => {} }): void { + handleEnter(event: KeyboardEvent): void { let { selected, menuOpen } = this.state.currentState, query = this.input.value; if (menuOpen) { @@ -300,8 +303,10 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte hasSelectedOption = queryIndex > -1; selected = hasSelectedOption ? queryIndex : selected; } - if (hasSelectedOption) { + if (hasSelectedOption && selected < this.matches.length) { this.handleOptionClick(selected); + } else if (hasSelectedOption && selected === this.matches.length) { + this.handleViewMore(event as KeyboardEvent); } else { this.handleComponentBlur({ focused: -1, @@ -333,15 +338,18 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte return !!(agent.match(/(iPod|iPhone|iPad)/g) && agent.match(/AppleWebKit/g)); } + getOptionId(index): string { + return `${this.id}__option--${index}`; + } + get hasAutoselect(): boolean { return this.isIosDevice() ? false : this.autoSelect; } get activeDescendant (): string { - let state = this.state.currentState, - focused = state.focused, + let { focused } = this.state.currentState, optionFocused = focused !== -1 && focused !== null; - return optionFocused ? `${ this.id }__option--${ focused }` : 'false'; + return optionFocused ? this.getOptionId(focused) : null; } get displayState(): any {