-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(multi-select): add select all to keyboard nav (#950)
- Loading branch information
1 parent
ebe1381
commit f101dbf
Showing
8 changed files
with
338 additions
and
14 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
3 changes: 3 additions & 0 deletions
3
projects/ng-aquila/src/dropdown/multi-select/multi-select-all.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,3 @@ | ||
<nx-checkbox [indeterminate]="indeterminate" [class.is-active]="active" [class.is-selected]="selected" [checked]="selected"> | ||
{{ label }} | ||
</nx-checkbox> |
8 changes: 8 additions & 0 deletions
8
projects/ng-aquila/src/dropdown/multi-select/multi-select-all.component.scss
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,8 @@ | ||
@import '../../shared-styles/index'; | ||
|
||
.nx-checkbox { | ||
&.is-active { | ||
@include focus-style; | ||
border-radius: 4px; | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
projects/ng-aquila/src/dropdown/multi-select/multi-select-all.component.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,102 @@ | ||
import { Highlightable, LiveAnnouncer } from '@angular/cdk/a11y'; | ||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; | ||
|
||
let nextId = 0; | ||
|
||
/** | ||
* A single option witin the multi select. | ||
* | ||
* @docs-private | ||
*/ | ||
@Component({ | ||
selector: 'nx-multi-select-all', | ||
styleUrls: ['./multi-select-all.component.scss'], | ||
templateUrl: './multi-select-all.component.html', | ||
host: { | ||
role: 'option', | ||
'[id]': 'id', | ||
'[attr.aria-selected]': 'selected || null', | ||
'[attr.aria-disabled]': 'disabled || null', | ||
}, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class NxMultiSelectAllComponent<T> implements Highlightable { | ||
private _active = false; | ||
|
||
id = `nx-multi-select-all-${nextId++}`; | ||
|
||
/** | ||
* Value of this option. | ||
*/ | ||
@Input() value?: T; | ||
|
||
/** | ||
* Label of this option. | ||
*/ | ||
@Input() label = ''; | ||
|
||
/** | ||
* Indeterminate | ||
*/ | ||
@Input() indeterminate = false; | ||
|
||
/** | ||
* Whether this option is selected. | ||
*/ | ||
@Input() selected = false; | ||
|
||
/** | ||
* Whether thisoption is disabled. | ||
*/ | ||
@Input() disabled = false; | ||
|
||
/** | ||
* Emits an event when this option is selected or unselected by the user. | ||
*/ | ||
@Output() readonly selectedAllChange = new EventEmitter<boolean>(); | ||
|
||
/** | ||
* Sets this option active highlighting it to the user. | ||
*/ | ||
set active(value: boolean) { | ||
this._active = value; | ||
this._cdr.markForCheck(); | ||
} | ||
|
||
get active() { | ||
return this._active; | ||
} | ||
|
||
constructor(private readonly _cdr: ChangeDetectorRef, readonly elementRef: ElementRef, private liveAnnouncer: LiveAnnouncer) {} | ||
|
||
setActiveStyles(): void { | ||
this.active = true; | ||
} | ||
|
||
setInactiveStyles(): void { | ||
this.active = false; | ||
} | ||
|
||
@HostListener('click', ['$event']) | ||
_onClick(e: Event) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
this._onSelect(); | ||
} | ||
|
||
_onSelect() { | ||
if (!this.disabled) { | ||
this.selected = !this.selected; | ||
this.selectedAllChange.emit(this.selected); | ||
this.liveAnnouncer.announce(`${this.label} ${this.selected ? 'selected' : 'unselected'}`); | ||
} | ||
} | ||
|
||
/** | ||
* Selects this option as if the user clicked on it. | ||
*/ | ||
selectViaInteraction() { | ||
this._onSelect(); | ||
this._cdr.markForCheck(); | ||
} | ||
} |
193 changes: 193 additions & 0 deletions
193
projects/ng-aquila/src/dropdown/multi-select/multi-select-all.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,193 @@ | ||
import { LiveAnnouncer } from '@angular/cdk/a11y'; | ||
import { ComponentHarness, HarnessLoader, parallel } from '@angular/cdk/testing'; | ||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; | ||
import { Component, Directive, Type, ViewChild } from '@angular/core'; | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { NxDropdownModule } from '../dropdown.module'; | ||
import { NxMultiSelectAllComponent } from './multi-select-all.component'; | ||
|
||
/** @docs-private */ | ||
export class MultiSelectAllHarness extends ComponentHarness { | ||
static hostSelector = 'nx-multi-select-all'; | ||
|
||
getLabel = this.locatorFor('.nx-checkbox__label'); | ||
|
||
getCheckbox = this.locatorFor('.nx-checkbox'); | ||
|
||
getCheckIcon = this.locatorForOptional('nx-icon'); | ||
|
||
async getLabelText() { | ||
const label = await this.getLabel(); | ||
return label.text(); | ||
} | ||
|
||
async isSelected() { | ||
const checkbox = await this.getCheckbox(); | ||
return checkbox.hasClass('is-selected'); | ||
} | ||
|
||
async isActive() { | ||
const checkbox = await this.getCheckbox(); | ||
return checkbox.hasClass('is-active'); | ||
} | ||
|
||
async isOutline() { | ||
const host = await this.host(); | ||
return host.hasClass('is-outline'); | ||
} | ||
|
||
async click() { | ||
const option = await this.host(); | ||
await option.click(); | ||
} | ||
|
||
async getId() { | ||
const host = await this.host(); | ||
return host.getAttribute('id'); | ||
} | ||
} | ||
|
||
describe('NxMultiSelectAllComponent', () => { | ||
let fixture: ComponentFixture<MultiSelectAllTest>; | ||
let testInstance: MultiSelectAllTest; | ||
let multiSelectAllInstance: NxMultiSelectAllComponent<any>; | ||
let loader: HarnessLoader; | ||
let multiSelectAllHarness: MultiSelectAllHarness; | ||
let liveAnnouncer: LiveAnnouncer; | ||
|
||
async function configureTestingModule(declarations: any[]) { | ||
return TestBed.configureTestingModule({ | ||
imports: [NxDropdownModule], | ||
declarations, | ||
providers: [LiveAnnouncer], | ||
}).compileComponents(); | ||
} | ||
|
||
function createTestComponent(component: Type<MultiSelectAllTest>) { | ||
fixture = TestBed.createComponent(component); | ||
fixture.detectChanges(); | ||
testInstance = fixture.componentInstance; | ||
multiSelectAllInstance = testInstance.multiSelectOption; | ||
liveAnnouncer = TestBed.inject(LiveAnnouncer); | ||
|
||
loader = TestbedHarnessEnvironment.loader(fixture); | ||
} | ||
|
||
beforeEach(async () => { | ||
await configureTestingModule([BasicMultiSelectAllComponent, NxMultiSelectAllComponent]); | ||
createTestComponent(BasicMultiSelectAllComponent); | ||
multiSelectAllHarness = await loader.getHarness(MultiSelectAllHarness); | ||
}); | ||
|
||
describe('select all button', () => { | ||
it('has the label', async () => { | ||
const label = await multiSelectAllHarness.getLabelText(); | ||
expect(label).toBe('Select All'); | ||
}); | ||
|
||
it('is not active', async () => { | ||
expect(await multiSelectAllHarness.isActive()).toBeFalse(); | ||
}); | ||
|
||
it('shows no check icon', async () => { | ||
const icon = await multiSelectAllHarness.getCheckIcon(); | ||
expect(icon).toBeNull(); | ||
}); | ||
|
||
it('is not selected', async () => { | ||
expect(await multiSelectAllHarness.isSelected()).toBeFalse(); | ||
}); | ||
|
||
it('has the aria attributes', async () => { | ||
const option = await multiSelectAllHarness.host(); | ||
|
||
const [role, id, ariaSelected, ariaDisabled] = await parallel(() => [ | ||
option.getAttribute('role'), | ||
option.getAttribute('id'), | ||
option.getAttribute('aria-selected'), | ||
option.getAttribute('aria-disabled'), | ||
]); | ||
|
||
expect(role).toBe('option'); | ||
expect(id).toMatch(/^nx-multi-select-all-\d+$/); | ||
expect(ariaSelected).toBeNull(); | ||
expect(ariaDisabled).toBeNull(); | ||
}); | ||
|
||
describe('when selected by click', () => { | ||
beforeEach(async () => { | ||
await multiSelectAllHarness.click(); | ||
}); | ||
|
||
it('is selected', async () => { | ||
expect(await multiSelectAllHarness.isSelected()).toBeTrue(); | ||
}); | ||
|
||
it('shows check icon', async () => { | ||
const icon = await multiSelectAllHarness.getCheckIcon(); | ||
expect(icon).not.toBeNull(); | ||
}); | ||
|
||
it('has the aria attributes', async () => { | ||
const option = await multiSelectAllHarness.host(); | ||
const ariaSelected = await option.getAttribute('aria-selected'); | ||
|
||
expect(ariaSelected).toBe('true'); | ||
}); | ||
|
||
it('triggers the selectedChange event', () => { | ||
expect(testInstance.onSelectAll).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('when set active', () => { | ||
beforeEach(() => { | ||
multiSelectAllInstance.setActiveStyles(); | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('is active', async () => { | ||
expect(await multiSelectAllHarness.isActive()).toBeTrue(); | ||
}); | ||
|
||
describe('and set inactive', () => { | ||
beforeEach(() => { | ||
multiSelectAllInstance.setInactiveStyles(); | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('is active', async () => { | ||
expect(await multiSelectAllHarness.isActive()).toBeFalse(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('accessibility', () => { | ||
it('read out select state', async () => { | ||
createTestComponent(BasicMultiSelectAllComponent); | ||
const announceSpy = spyOn(liveAnnouncer, 'announce'); | ||
await multiSelectAllHarness.click(); | ||
|
||
expect(announceSpy).toHaveBeenCalledWith('Select All selected'); | ||
}); | ||
}); | ||
}); | ||
|
||
@Directive() | ||
abstract class MultiSelectAllTest { | ||
@ViewChild(NxMultiSelectAllComponent) multiSelectOption!: NxMultiSelectAllComponent<any>; | ||
|
||
selected = false; | ||
label = 'Select All'; | ||
onSelectAll = jasmine.createSpy('onSelectAll'); | ||
indeterminate = false; | ||
} | ||
|
||
@Component({ | ||
template: ` | ||
<nx-multi-select-all [selected]="selected" [label]="label" [indeterminate]="indeterminate" (selectedAllChange)="onSelectAll()"> </nx-multi-select-all> | ||
`, | ||
}) | ||
class BasicMultiSelectAllComponent extends MultiSelectAllTest {} |
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
Oops, something went wrong.