From 44138935d59e89cf7ab89a21568f17c8ed16e71a Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 15 Jun 2018 11:18:06 +0200 Subject: [PATCH] fix(select): support ctrl+a shortcut for multi-select [Based on the accessibility guidelines](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox), multi-selection lists should support selecting/deselecting all using ctrl+a. --- src/lib/select/select.spec.ts | 91 +++++++++++++++++++++++++++++++++++ src/lib/select/select.ts | 5 ++ 2 files changed, 96 insertions(+) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index ceb58b2de00c..a5dcf1e2ede7 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -9,6 +9,7 @@ import { SPACE, TAB, UP_ARROW, + A, } from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import {Platform} from '@angular/cdk/platform'; @@ -3869,6 +3870,96 @@ describe('MatSelect', () => { expect(testInstance.control.value).toEqual([null, 'pizza-1', null]); })); + it('should select all options when pressing ctrl + a', () => { + const selectElement = fixture.nativeElement.querySelector('mat-select'); + const options = fixture.componentInstance.options.toArray(); + + expect(testInstance.control.value).toBeFalsy(); + expect(options.every(option => option.selected)).toBe(false); + + fixture.componentInstance.select.open(); + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', A, selectElement); + Object.defineProperty(event, 'ctrlKey', {get: () => true}); + dispatchEvent(selectElement, event); + fixture.detectChanges(); + + expect(options.every(option => option.selected)).toBe(true); + expect(testInstance.control.value).toEqual([ + 'steak-0', + 'pizza-1', + 'tacos-2', + 'sandwich-3', + 'chips-4', + 'eggs-5', + 'pasta-6', + 'sushi-7' + ]); + }); + + it('should select all options when pressing ctrl + a when some options are selected', () => { + const selectElement = fixture.nativeElement.querySelector('mat-select'); + const options = fixture.componentInstance.options.toArray(); + + options[0].select(); + fixture.detectChanges(); + + expect(testInstance.control.value).toEqual(['steak-0']); + expect(options.some(option => option.selected)).toBe(true); + + fixture.componentInstance.select.open(); + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', A, selectElement); + Object.defineProperty(event, 'ctrlKey', {get: () => true}); + dispatchEvent(selectElement, event); + fixture.detectChanges(); + + expect(options.every(option => option.selected)).toBe(true); + expect(testInstance.control.value).toEqual([ + 'steak-0', + 'pizza-1', + 'tacos-2', + 'sandwich-3', + 'chips-4', + 'eggs-5', + 'pasta-6', + 'sushi-7' + ]); + }); + + it('should deselect all options with ctrl + a if all options are selected', () => { + const selectElement = fixture.nativeElement.querySelector('mat-select'); + const options = fixture.componentInstance.options.toArray(); + + options.forEach(option => option.select()); + fixture.detectChanges(); + + expect(testInstance.control.value).toEqual([ + 'steak-0', + 'pizza-1', + 'tacos-2', + 'sandwich-3', + 'chips-4', + 'eggs-5', + 'pasta-6', + 'sushi-7' + ]); + expect(options.every(option => option.selected)).toBe(true); + + fixture.componentInstance.select.open(); + fixture.detectChanges(); + + const event = createKeyboardEvent('keydown', A, selectElement); + Object.defineProperty(event, 'ctrlKey', {get: () => true}); + dispatchEvent(selectElement, event); + fixture.detectChanges(); + + expect(options.some(option => option.selected)).toBe(false); + expect(testInstance.control.value).toEqual([]); + }); + }); }); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 513477de2cee..53b35ae620f2 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -19,6 +19,7 @@ import { RIGHT_ARROW, SPACE, UP_ARROW, + A, } from '@angular/cdk/keycodes'; import { CdkConnectedOverlay, @@ -695,6 +696,10 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, } else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem) { event.preventDefault(); manager.activeItem._selectViaInteraction(); + } else if (this._multiple && keyCode === A && event.ctrlKey) { + event.preventDefault(); + const hasDeselectedOptions = this.options.some(option => !option.selected); + this.options.forEach(option => hasDeselectedOptions ? option.select() : option.deselect()); } else { const previouslyFocusedIndex = manager.activeItemIndex;