From 3a6e26b3032cdc0bfebecd1de6929524b59a8c09 Mon Sep 17 00:00:00 2001 From: Mihai Agape Date: Fri, 23 Oct 2020 02:31:36 +0300 Subject: [PATCH 01/10] fix(suggest): improve title accessibility --- .../components/ui-suggest/src/test/index.ts | 1 + .../ui-suggest/src/test/ui-suggest-assert.ts | 50 ++++++++++ .../ui-suggest/src/ui-suggest.component.html | 20 +++- .../src/ui-suggest.component.spec.ts | 94 +++++++++++-------- .../src/ui-suggest.mat-form-field.ts | 7 +- 5 files changed, 126 insertions(+), 46 deletions(-) create mode 100644 projects/angular/components/ui-suggest/src/test/ui-suggest-assert.ts diff --git a/projects/angular/components/ui-suggest/src/test/index.ts b/projects/angular/components/ui-suggest/src/test/index.ts index 53016b43c..cfcc78e0b 100644 --- a/projects/angular/components/ui-suggest/src/test/index.ts +++ b/projects/angular/components/ui-suggest/src/test/index.ts @@ -1 +1,2 @@ export * from './suggestionItem'; +export * from './ui-suggest-assert'; diff --git a/projects/angular/components/ui-suggest/src/test/ui-suggest-assert.ts b/projects/angular/components/ui-suggest/src/test/ui-suggest-assert.ts new file mode 100644 index 000000000..875ab6115 --- /dev/null +++ b/projects/angular/components/ui-suggest/src/test/ui-suggest-assert.ts @@ -0,0 +1,50 @@ +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import { UiSuggestComponent } from '../ui-suggest.component'; + +export class UiSuggestAssert { + constructor( + private _root: DebugElement, + private _suggest: UiSuggestComponent, + ) { } + + public isOpen(): void { + this._assertOpenState('open'); + } + + public isClosed(): void { + this._assertOpenState('closed'); + } + + public isDisabled(): void { + this._assertDisableState('disabled'); + } + + public isEnabled(): void { + this._assertDisableState('enabled'); + } + + private _assertOpenState(expected: 'open' | 'closed'): void { + const expectedIsOpen = expected === 'open'; + + expect(this._suggest.isOpen).toBe(expectedIsOpen); + + const combo = this._root.query(By.css('[role=combobox]')).nativeElement; + expect(combo).toHaveAttr('aria-expanded', expectedIsOpen.toString()); + + const itemList = this._root.query(By.css('.item-list-container')); + const itemListClasses = itemList.nativeElement.classList; + expect(itemListClasses.contains('item-list-container-state-open')).toBe(expectedIsOpen); + expect(itemListClasses.contains('item-list-container-state-closed')).toBe(!expectedIsOpen); + } + + private _assertDisableState(expected: 'enabled' | 'disabled'): void { + const expectedIsDisabled = expected === 'disabled'; + + expect(this._suggest.disabled).toBe(expectedIsDisabled); + + const combo = this._root.query(By.css('[role=combobox]')).nativeElement; + expect(combo).toHaveAttr('aria-disabled', expectedIsDisabled.toString()); + } +} diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.html b/projects/angular/components/ui-suggest/src/ui-suggest.component.html index 63024b65c..4c1a8d4ef 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.html +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.html @@ -1,16 +1,26 @@
- + +
+ class="display" + role="combobox">
+ class="display-title" + aria-hidden="true"> {{ placeholder }}: diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts index 20d76fb30..150f093e9 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts @@ -47,6 +47,7 @@ import { import { generateSuggestionItem, generateSuggetionItemList, + UiSuggestAssert, } from './test'; import { UiSuggestComponent } from './ui-suggest.component'; import { UiSuggestModule } from './ui-suggest.module'; @@ -102,12 +103,14 @@ const sharedSpecifications = ( let fixture: ComponentFixture; let component: UiSuggestFixtureDirective; let uiSuggest: UiSuggestComponent; + let assert: UiSuggestAssert; beforeEach(() => { const setup = beforeEachFn(); fixture = setup.fixture; component = setup.component; uiSuggest = setup.uiSuggest; + assert = new UiSuggestAssert(fixture.debugElement, uiSuggest); }); describe('Behavior: standard usage', () => { @@ -506,18 +509,13 @@ const sharedSpecifications = ( fixture.detectChanges(); - const itemList = fixture.debugElement.query(By.css('.item-list-container')); - const itemListClasses = itemList.nativeElement.classList; - - expect(uiSuggest.isOpen).toBeTruthy(); - expect(itemListClasses.contains('item-list-container-state-open')).toBeTruthy(); + assert.isOpen(); component[state] = true; fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); - expect(itemListClasses.contains('item-list-container-state-closed')).toBeTruthy(); + assert.isClosed(); }); it(`should not open if the component is ${state}`, () => { @@ -525,14 +523,14 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); fixture.detectChanges(); const display = fixture.debugElement.query(By.css('.display')); display.nativeElement.dispatchEvent(EventGenerator.click); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); }); }); @@ -545,13 +543,13 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.disabled).toBeTruthy(); + assert.isDisabled(); expect(uiSuggest.loading$.value).toBeFalsy(); component.disabled = false; fixture.detectChanges(); - expect(uiSuggest.disabled).toBeFalsy(); + assert.isEnabled(); const display = fixture.debugElement.query(By.css('.display')); display.nativeElement.dispatchEvent(EventGenerator.click); @@ -572,11 +570,11 @@ const sharedSpecifications = ( }; fixture.detectChanges(); - expect(uiSuggest.disabled).toBeTruthy(); + assert.isDisabled(); component.disabled = false; fixture.detectChanges(); - expect(uiSuggest.disabled).toBeFalsy(); + assert.isEnabled(); const display = fixture.debugElement.query(By.css('.display')); display.nativeElement.dispatchEvent(EventGenerator.click); @@ -590,18 +588,22 @@ const sharedSpecifications = ( it('should not open on first click and close on the second', () => { fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); fixture.detectChanges(); const display = fixture.debugElement.query(By.css('.display')); display.nativeElement.dispatchEvent(EventGenerator.click); - expect(uiSuggest.isOpen).toBeTruthy(); + fixture.detectChanges(); + + assert.isOpen(); display.nativeElement.dispatchEvent(EventGenerator.click); - expect(uiSuggest.isOpen).toBeFalsy(); + fixture.detectChanges(); + + assert.isClosed(); }); }); @@ -626,7 +628,7 @@ const sharedSpecifications = ( it('should open when pressing Enter', () => { fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); fixture.detectChanges(); const display = fixture.debugElement.query(By.css('.display')); @@ -634,13 +636,15 @@ const sharedSpecifications = ( EventGenerator.keyDown(Key.Enter), ); - expect(uiSuggest.isOpen).toBeTruthy(); + fixture.detectChanges(); + + assert.isOpen(); }); it('should open when pressing Space', () => { fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); fixture.detectChanges(); const display = fixture.debugElement.query(By.css('.display')); @@ -648,7 +652,9 @@ const sharedSpecifications = ( EventGenerator.keyUp(Key.Space), ); - expect(uiSuggest.isOpen).toBeTruthy(); + fixture.detectChanges(); + + assert.isOpen(); }); it('should have the active index set when opened', () => { @@ -814,7 +820,7 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); const itemContainer = fixture.debugElement.query(By.css('.item-list-container')); @@ -826,7 +832,9 @@ const sharedSpecifications = ( EventGenerator.keyDown(Key.Enter), ); - expect(uiSuggest.isOpen).toBeFalsy(); + fixture.detectChanges(); + + assert.isClosed(); }); it('should NOT close if selecting an item via navigation and multiple selection is ENABLED', () => { @@ -840,7 +848,7 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); const itemContainer = fixture.debugElement.query(By.css('.item-list-container')); @@ -852,7 +860,7 @@ const sharedSpecifications = ( EventGenerator.keyDown(Key.Enter), ); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); }); it('should close if Tab is pressed', () => { @@ -864,7 +872,7 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); const itemContainer = fixture.debugElement.query(By.css('.item-list-container')); @@ -872,7 +880,9 @@ const sharedSpecifications = ( EventGenerator.keyDown(Key.Tab), ); - expect(uiSuggest.isOpen).toBeFalsy(); + fixture.detectChanges(); + + assert.isClosed(); }); it('should close if Shift + Tab is pressed', () => { @@ -884,7 +894,7 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); const itemContainer = fixture.debugElement.query(By.css('.item-list-container')); @@ -892,7 +902,9 @@ const sharedSpecifications = ( EventGenerator.keyDown(Key.Tab, Key.Shift), ); - expect(uiSuggest.isOpen).toBeFalsy(); + fixture.detectChanges(); + + assert.isClosed(); }); it('should close if Esc is pressed', () => { @@ -904,7 +916,7 @@ const sharedSpecifications = ( fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); const itemContainer = fixture.debugElement.query(By.css('.item-list-container')); @@ -912,7 +924,9 @@ const sharedSpecifications = ( EventGenerator.keyUp(Key.Escape), ); - expect(uiSuggest.isOpen).toBeFalsy(); + fixture.detectChanges(); + + assert.isClosed(); }); it('should clear selection if Esc is pressed', () => { @@ -1892,6 +1906,7 @@ describe('Component: UiSuggest', () => { let fixture: ComponentFixture; let component: UiSuggestFormControlFixtureComponent; let uiSuggest: UiSuggestComponent; + let assert: UiSuggestAssert; const beforeEachFn = () => { TestBed.configureTestingModule({ @@ -1925,6 +1940,7 @@ describe('Component: UiSuggest', () => { fixture = setup.fixture; component = setup.component; uiSuggest = setup.uiSuggest; + assert = new UiSuggestAssert(fixture.debugElement, uiSuggest); component.items = generateSuggetionItemList('random'); }); @@ -1948,7 +1964,7 @@ describe('Component: UiSuggest', () => { it('should open the list when clicking the container', () => { fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeFalsy(); + assert.isClosed(); const formFieldUnderline = fixture.debugElement.query(By.css('.test-form-field .mat-form-field-label')); @@ -1956,15 +1972,19 @@ describe('Component: UiSuggest', () => { fixture.detectChanges(); - expect(uiSuggest.isOpen).toBeTruthy(); + assert.isOpen(); }); it('should be marked as disabled when updating state via form', () => { fixture.detectChanges(); + assert.isEnabled(); + component.formControl!.disable(); - expect(uiSuggest.disabled).toBeTruthy(); + fixture.detectChanges(); + + assert.isDisabled(); }); it('should remove NULL and Undefined entries when updating value via form', () => { @@ -2012,15 +2032,15 @@ describe('Component: UiSuggest', () => { uiSuggest.required = true; fixture.detectChanges(); - const suggest = fixture.debugElement.query(By.css('ui-suggest')).nativeElement as HTMLElement; - expect(suggest.getAttribute('aria-required')).toEqual('true'); + const combobox = fixture.debugElement.query(By.css('ui-suggest [role=combobox]')); + expect(combobox.attributes['aria-required']).toEqual('true'); }); it('should have aria attribute set to false if it is NOT required', () => { fixture.detectChanges(); - const suggest = fixture.debugElement.query(By.css('ui-suggest')).nativeElement as HTMLElement; - expect(suggest.getAttribute('aria-required')).toEqual('false'); + const combobox = fixture.debugElement.query(By.css('ui-suggest [role=combobox]')); + expect(combobox.attributes['aria-required']).toEqual('false'); }); }); }); diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts b/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts index 55e6edbad..cdf2a112a 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.mat-form-field.ts @@ -56,7 +56,6 @@ export abstract class UiSuggestMatFormFieldDirective implements * Configure if the input should be marked as `required` inside the form field. * */ - @HostBinding('attr.aria-required') @Input() public get required() { return this._required; @@ -186,9 +185,7 @@ export abstract class UiSuggestMatFormFieldDirective implements /** * @ignore */ - @HostBinding('attr.aria-describedby') - public describedBy = ''; - + public describedBy?: string; /** * Emits the selected item value an item is selected. @@ -282,7 +279,7 @@ export abstract class UiSuggestMatFormFieldDirective implements * @ignore */ public setDescribedByIds(ids: string[]) { - this.describedBy = ids.join(' '); + this.describedBy = ids.join(' ').trim() || undefined; } /** From 537853683bb9aab6abb8c6dc263ecda70a3822c3 Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Thu, 22 Oct 2020 23:49:38 +0300 Subject: [PATCH 02/10] fix(suggest): add announcement of currently selected item --- .../components/ui-suggest/src/ui-suggest.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts index c4ec287c3..0bb7ce347 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts @@ -747,6 +747,9 @@ export class UiSuggestComponent extends UiSuggestMatFormFieldDirective ); this._scrollTo$.next(this.activeIndex); + if (!this.loading$.value) { + this._announceNavigate(); + } } /** @@ -1026,9 +1029,9 @@ export class UiSuggestComponent extends UiSuggestMatFormFieldDirective } private _announceNavigate() { - const textToAnnounce = !this._isOnCustomValueIndex ? - this.items[this.activeIndex].text : - `${this.intl.customValueLiveLabel} ${this.customValueLabelTranslator(this.inputControl.value)}`; + const textToAnnounce = !this._isOnCustomValueIndex + ? this.items[this.activeIndex].text + : `${this.intl.customValueLiveLabel} ${this.customValueLabelTranslator(this.inputControl.value)}`; this._liveAnnouncer.announce(this.intl.currentItemLabel(textToAnnounce, this.activeIndex + 1, this._items.length)); } From 7df233a01af299af121684a727f1337b9b67cae1 Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Thu, 22 Oct 2020 20:56:15 +0300 Subject: [PATCH 03/10] fix(a11y): specify role attributes for suggest list --- .../components/ui-suggest/src/ui-suggest.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.html b/projects/angular/components/ui-suggest/src/ui-suggest.component.html index 4c1a8d4ef..24a9d4467 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.html +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.html @@ -114,6 +114,7 @@ [refocus]="isOpen" selectionLocation="end" autocomplete="off" + aria-autocomplete="list" matInput> search @@ -125,7 +126,7 @@
- + @@ -198,6 +200,7 @@ !isCustomValueVisible && !(loading$ | async)" [matTooltip]="label" + [attr.role]="'option'" matTooltipPosition="right" class="text-ellipsis no-results-text"> {{label}} @@ -214,6 +217,7 @@ [matTooltip]="customValueLabelTranslator(inputControl.value)" [style.height.px]="itemSize" (click)="preventDefault($event); updateValue(inputControl.value, !multiple, true);" + [attr.role]="'option'" matTooltipPosition="right" class="text-ellipsis custom-item">
From c1d1befadf22866d48fd05e360183b53ed84ab02 Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Thu, 22 Oct 2020 16:58:37 +0300 Subject: [PATCH 04/10] feat(grid): differentiate between user sort and programmatic sort --- .../ui-grid/src/managers/sort-manager.spec.ts | 17 ++++++++++++++++- .../ui-grid/src/managers/sort-manager.ts | 9 ++++++--- .../components/ui-grid/src/models/sortModel.ts | 5 +++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/projects/angular/components/ui-grid/src/managers/sort-manager.spec.ts b/projects/angular/components/ui-grid/src/managers/sort-manager.spec.ts index 286fe0fa7..073943d8b 100644 --- a/projects/angular/components/ui-grid/src/managers/sort-manager.spec.ts +++ b/projects/angular/components/ui-grid/src/managers/sort-manager.spec.ts @@ -52,7 +52,19 @@ describe('Component: UiGrid', () => { expect(manager.columns).toBe(columns); }); - describe('Event: sort change', () => { + it('should update sort event if columns change', () => { + const cols = generateColumnList('random'); + cols.forEach(column => { + column.sortable = true; + column.sort = 'asc'; + }); + + manager.columns = cols; + expect(Object.keys(manager.sort$.getValue()).length).toBe(4); + expect(manager.sort$.getValue().userEvent).toBeFalse(); + }); + + describe('Event: user sort change', () => { it(`should cycle column sort from '' to 'asc'`, () => { const column = faker.helpers.randomize(columns); column.sort = ''; @@ -110,6 +122,7 @@ describe('Component: UiGrid', () => { ).subscribe(sort => { expect(sort.field).toEqual(column.property!); expect(sort.direction).toEqual(column.sort); + expect(sort.userEvent).toBeTrue(); }); manager.changeSort(column); @@ -129,6 +142,7 @@ describe('Component: UiGrid', () => { ).subscribe(sort => { expect(sort.field).toEqual(first.property!); expect(sort.direction).toEqual(first.sort); + expect(sort.userEvent).toBeTrue(); }); manager.sort$ @@ -139,6 +153,7 @@ describe('Component: UiGrid', () => { ).subscribe(sort => { expect(sort.field).toEqual(second.property!); expect(sort.direction).toEqual(second.sort); + expect(sort.userEvent).toBeTrue(); }); manager.changeSort(first); diff --git a/projects/angular/components/ui-grid/src/managers/sort-manager.ts b/projects/angular/components/ui-grid/src/managers/sort-manager.ts index 8650c7e5f..c6cf147ff 100644 --- a/projects/angular/components/ui-grid/src/managers/sort-manager.ts +++ b/projects/angular/components/ui-grid/src/managers/sort-manager.ts @@ -48,7 +48,9 @@ export class SortManager { this._emitSort(sortedColumn); } - + /** + * Sort based on user action on column header + */ public changeSort(column: UiGridColumnDirective) { if (!column.sortable) { return; } @@ -58,18 +60,19 @@ export class SortManager { column.sort = SORT_CYCLE_MAP[column.sort]; - this._emitSort(column); + this._emitSort(column, true); } public destroy() { this.sort$.complete(); } - private _emitSort(column: UiGridColumnDirective) { + private _emitSort(column: UiGridColumnDirective, userEvent = false) { const updatedSort = { direction: column.sort, field: column.property, title: column.title, + userEvent, } as ISortModel; if (isEqual(this.sort$.getValue(), updatedSort)) { return; } diff --git a/projects/angular/components/ui-grid/src/models/sortModel.ts b/projects/angular/components/ui-grid/src/models/sortModel.ts index 917f52986..c109e0e1d 100644 --- a/projects/angular/components/ui-grid/src/models/sortModel.ts +++ b/projects/angular/components/ui-grid/src/models/sortModel.ts @@ -21,4 +21,9 @@ export interface ISortModel { * */ title: string; + /** + * Sort event as a result of direct user intent to sort by a column. + * + */ + userEvent?: boolean; } From 070d375b95e88d3bc10c26d591b3ab12e2f6112a Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Thu, 22 Oct 2020 18:32:07 +0300 Subject: [PATCH 05/10] fix(grid): announce only user sort events --- .../angular/components/ui-grid/src/ui-grid.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/angular/components/ui-grid/src/ui-grid.component.ts b/projects/angular/components/ui-grid/src/ui-grid.component.ts index 0a81c71b5..6d91703d0 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.component.ts +++ b/projects/angular/components/ui-grid/src/ui-grid.component.ts @@ -32,9 +32,9 @@ import { import { debounceTime, distinctUntilChanged, + filter, map, observeOn, - skip, switchMap, take, takeUntil, @@ -499,7 +499,9 @@ export class UiGridComponent extends ResizableGrid msg => this._queuedAnnouncer.enqueue(msg), this.intl, this.dataManager.data$, - this.sortManager.sort$.pipe(skip(1)), + this.sortManager.sort$.pipe( + filter(({ userEvent }) => !!userEvent), + ), this.refresh, this.footer && this.footer.pageChange, ); From ce4debdf724eb7e3c350578f3abfa043d6c39a5e Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Thu, 22 Oct 2020 18:33:22 +0300 Subject: [PATCH 06/10] fix(grid): remove redundant column class --- projects/angular/components/ui-grid/src/ui-grid.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/angular/components/ui-grid/src/ui-grid.component.html b/projects/angular/components/ui-grid/src/ui-grid.component.html index 37709e83f..875bb15b3 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.component.html +++ b/projects/angular/components/ui-grid/src/ui-grid.component.html @@ -172,7 +172,6 @@
From 0ac47d849efbd5d6cc704c2f23539d02ed0920c6 Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Mon, 26 Oct 2020 14:58:28 +0200 Subject: [PATCH 07/10] fix(grid): expose translation method for checkbox message - render tooltip for grid checkboxes --- .../ui-grid/src/ui-grid.component.html | 9 ++++-- .../ui-grid/src/ui-grid.component.spec.ts | 32 ++++++++++++++++--- .../ui-grid/src/ui-grid.component.ts | 17 ++++++++-- .../components/ui-grid/src/ui-grid.intl.ts | 12 +++++++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/projects/angular/components/ui-grid/src/ui-grid.component.html b/projects/angular/components/ui-grid/src/ui-grid.component.html index 875bb15b3..0a417245b 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.component.html +++ b/projects/angular/components/ui-grid/src/ui-grid.component.html @@ -126,7 +126,8 @@ [disabled]="!dataManager.length" [checked]="isEveryVisibleRowChecked" [indeterminate]="hasValueOnVisiblePage && !isEveryVisibleRowChecked" - [aria-label]="checkboxLabel()" + [matTooltip]="checkboxTooltip()" + [aria-label]="checkboxTooltip()" tabindex="0">
@@ -290,7 +291,8 @@ (keyup.space)="checkShift($event)" (change)="handleSelection(index, row)" [checked]="selectionManager.isSelected(row)" - [aria-label]="checkboxLabel(row)" + [matTooltip]="checkboxTooltip(row)" + [aria-label]="checkboxTooltip(row)" tabindex="0">
@@ -313,7 +315,8 @@ (keyup.space)="checkShift($event)" (change)="handleSelection(index, row)" [checked]="selectionManager.isSelected(row)" - [aria-label]="checkboxLabel(row)" + [matTooltip]="checkboxTooltip(row)" + [aria-label]="checkboxTooltip(row)" tabindex="0">
diff --git a/projects/angular/components/ui-grid/src/ui-grid.component.spec.ts b/projects/angular/components/ui-grid/src/ui-grid.component.spec.ts index 0f9cd0be1..e5c04c28c 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.component.spec.ts +++ b/projects/angular/components/ui-grid/src/ui-grid.component.spec.ts @@ -345,16 +345,34 @@ describe('Component: UiGrid', () => { }); describe('Feature: checkbox', () => { - it('should check all rows if the header checkbox is clicked', () => { + it('should have ariaLabel set correctly for toggle selection', () => { const checkboxHeader = fixture.debugElement.query(By.css('.ui-grid-header-cell.ui-grid-checkbox-cell')); + const matCheckbox = checkboxHeader.query(By.css('mat-checkbox')).componentInstance as MatCheckbox; + + expect(matCheckbox.checked).toEqual(false); + expect(matCheckbox.ariaLabel).toEqual('Select all'); const checkboxInput = checkboxHeader.query(By.css('input')); checkboxInput.nativeElement.dispatchEvent(EventGenerator.click); fixture.detectChanges(); + + expect(matCheckbox.checked).toEqual(true); + expect(matCheckbox.ariaLabel).toEqual('Deselect all'); + }); + + it('should check all rows if the header checkbox is clicked', () => { + const checkboxHeader = fixture.debugElement.query(By.css('.ui-grid-header-cell.ui-grid-checkbox-cell')); const matCheckbox = checkboxHeader.query(By.css('mat-checkbox')).componentInstance as MatCheckbox; + expect(matCheckbox.checked).toEqual(false); + + const checkboxInput = checkboxHeader.query(By.css('input')); + checkboxInput.nativeElement.dispatchEvent(EventGenerator.click); + + fixture.detectChanges(); + expect(matCheckbox.checked).toEqual(true); const rowCheckboxList = fixture.debugElement @@ -388,7 +406,10 @@ describe('Component: UiGrid', () => { expect(rowCheckboxList.length).toEqual(data.length); - rowCheckboxList.forEach(checkbox => expect(checkbox.checked).toEqual(true)); + rowCheckboxList.forEach((checkbox, i) => { + expect(checkbox.checked).toEqual(true); + expect(checkbox.ariaLabel).toEqual(`Deselect row ${i}`); + }); expect(grid.selectionManager.selected.length).toEqual(data.length); expect(grid.selectionManager.selected).toEqual(data); @@ -398,7 +419,10 @@ describe('Component: UiGrid', () => { fixture.detectChanges(); expect(matCheckbox.checked).toEqual(false); - rowCheckboxList.forEach(checkbox => expect(checkbox.checked).toEqual(false)); + rowCheckboxList.forEach((checkbox, i) => { + expect(checkbox.checked).toEqual(false); + expect(checkbox.ariaLabel).toEqual(`Select row ${i}`); + }); expect(grid.hasValueOnVisiblePage).toEqual(false); }); @@ -478,7 +502,7 @@ describe('Component: UiGrid', () => { rowCheckboxInputList[10].nativeElement.dispatchEvent(EventGenerator.click); - for (let i = 0; i <= 10; i ++) { + for (let i = 0; i <= 10; i++) { expect(grid.selectionManager.isSelected(data[i])).toBeTrue(); } }); diff --git a/projects/angular/components/ui-grid/src/ui-grid.component.ts b/projects/angular/components/ui-grid/src/ui-grid.component.ts index 6d91703d0..959a9d325 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.component.ts +++ b/projects/angular/components/ui-grid/src/ui-grid.component.ts @@ -114,7 +114,7 @@ export class UiGridComponent extends ResizableGrid * */ public get isEveryVisibleRowChecked() { - return this.dataManager.length && + return !!this.dataManager.length && this.dataManager.every(row => this.selectionManager.isSelected(row!)); } @@ -640,10 +640,23 @@ export class UiGridComponent extends ResizableGrid } /** - * Determines the `checkbox` `aria-label`. + * Determines the `checkbox` `matToolTip`. * * @param [row] The row for which the label is computed. */ + public checkboxTooltip(row?: T): string { + if (!row) { + return this.intl.checkboxTooltip(this.isEveryVisibleRowChecked); + } + + return this.intl.checkboxTooltip(this.selectionManager.isSelected(row), this.dataManager.indexOf(row)); + } + + /** + * Determines the `checkbox` aria-label`. + * **DEPRECATED** + * @param [row] The row for which the label is computed. + */ public checkboxLabel(row?: T): string { if (!row) { return `${this.isEveryVisibleRowChecked ? 'select' : 'deselect'} all`; diff --git a/projects/angular/components/ui-grid/src/ui-grid.intl.ts b/projects/angular/components/ui-grid/src/ui-grid.intl.ts index a69bccbcf..6143eaaef 100644 --- a/projects/angular/components/ui-grid/src/ui-grid.intl.ts +++ b/projects/angular/components/ui-grid/src/ui-grid.intl.ts @@ -103,6 +103,18 @@ export class UiGridIntl implements OnDestroy { */ public descending = 'descending'; + /** + * Determines the `checkbox` `matToolTip`. + * + * @param [rowIndex] The rowIndex for which the label is computed. + */ + public checkboxTooltip(selected: boolean, rowIndex?: number): string { + if (rowIndex == null) { + return `${selected ? 'Deselect' : 'Select'} all`; + } + return `${selected ? 'Deselect' : 'Select'} row ${rowIndex}`; + } + /** * Generates a selection label for the given count. From e87bd8672858bec8098480ceb36be3ed0492224a Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Mon, 26 Oct 2020 14:58:52 +0200 Subject: [PATCH 08/10] fix(grid): remove unreferenced icon --- .../src/components/ui-grid-search/ui-grid-search.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/angular/components/ui-grid/src/components/ui-grid-search/ui-grid-search.component.html b/projects/angular/components/ui-grid/src/components/ui-grid-search/ui-grid-search.component.html index e76c30e43..dc6c4afa3 100644 --- a/projects/angular/components/ui-grid/src/components/ui-grid-search/ui-grid-search.component.html +++ b/projects/angular/components/ui-grid/src/components/ui-grid-search/ui-grid-search.component.html @@ -31,7 +31,6 @@ Date: Mon, 26 Oct 2020 21:25:29 +0200 Subject: [PATCH 09/10] fix(suggest): announce "no results" msg if empty fix runtime error on open if empty; introduced by 1fdfa2 --- .../src/ui-suggest.component.spec.ts | 38 +++++++++++++++++++ .../ui-suggest/src/ui-suggest.component.ts | 4 ++ 2 files changed, 42 insertions(+) diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts index 150f093e9..2ccd68283 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.spec.ts @@ -607,6 +607,44 @@ const sharedSpecifications = ( }); }); + describe('Behavior: a11y on open', () => { + it(`should announce if empty`, () => { + const spy = spyOn(uiSuggest['_liveAnnouncer'], 'announce'); + const display = fixture.debugElement.query(By.css('.display')); + display.nativeElement.dispatchEvent(EventGenerator.click); + + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledWith('No results'); + }); + + it(`should NOT annuonce a highlight if in loading state`, () => { + component.items = generateSuggetionItemList('random'); + fixture.detectChanges(); + + const spy = spyOn(uiSuggest['_liveAnnouncer'], 'announce'); + uiSuggest.loading$.next(true); + + const display = fixture.debugElement.query(By.css('.display')); + display.nativeElement.dispatchEvent(EventGenerator.click); + + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it(`should announce as highlighted first item`, () => { + component.items = generateSuggetionItemList('random'); + fixture.detectChanges(); + + const spy = spyOn(uiSuggest['_liveAnnouncer'], 'announce'); + const display = fixture.debugElement.query(By.css('.display')); + display.nativeElement.dispatchEvent(EventGenerator.click); + + fixture.detectChanges(); + expect(spy).toHaveBeenCalledWith(`${component.items[0].text} item 1 out of ${component.items.length}`); + }); + }); + describe('Behavior: keyboard navigation', () => { let items: ISuggestValue[]; diff --git a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts index 0bb7ce347..25d293007 100644 --- a/projects/angular/components/ui-suggest/src/ui-suggest.component.ts +++ b/projects/angular/components/ui-suggest/src/ui-suggest.component.ts @@ -1029,6 +1029,10 @@ export class UiSuggestComponent extends UiSuggestMatFormFieldDirective } private _announceNavigate() { + if (!this.items.length && !this._isOnCustomValueIndex) { + this._liveAnnouncer.announce(this.intl.noResultsLabel); + return; + } const textToAnnounce = !this._isOnCustomValueIndex ? this.items[this.activeIndex].text : `${this.intl.customValueLiveLabel} ${this.customValueLabelTranslator(this.inputControl.value)}`; From 370c263be9821d1bd34af0c8823960851ad09a4d Mon Sep 17 00:00:00 2001 From: Andrei Balasescu Date: Wed, 28 Oct 2020 07:39:18 +0200 Subject: [PATCH 10/10] chore: bump version w/ changelog --- CHANGELOG.md | 9 +++++++++ package-lock.json | 2 +- package.json | 2 +- projects/angular/package.json | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 709a8e447..9f0c588df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# v10.0.0-rc.2 (2020-10-28) +* **grid** differentiate between user sort and programmatic sort events +* **grid** a11y: announce only user `sort` events +* **grid** a11y: expose translateable aria-label for checkboxes +* **grid** added `matTooltip` for checkboxes +* **suggest** a11y: fixes to title, specify `role` attributes for list +* **suggest** a11y: announce current `option` on open +* **suggest** announce "no results" msg if empty + # v10.0.0-rc.1 (2020-10-15) * **grid** fix multiple row selection with shift diff --git a/package-lock.json b/package-lock.json index 13546adaa..69e618ae5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "angular-components", - "version": "10.0.0-rc.1", + "version": "10.0.0-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 33a0a1756..eb0915855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-components", - "version": "10.0.0-rc.1", + "version": "10.0.0-rc.2", "author": { "name": "UiPath Inc", "url": "https://uipath.com" diff --git a/projects/angular/package.json b/projects/angular/package.json index ff8772280..5f7738b2d 100644 --- a/projects/angular/package.json +++ b/projects/angular/package.json @@ -1,6 +1,6 @@ { "name": "@uipath/angular", - "version": "10.0.0-rc.1", + "version": "10.0.0-rc.2", "license": "MIT", "author": { "name": "UiPath Inc",