From a50fb0b9c0b638bf78ae2f917fa6e9ad86dbfcf0 Mon Sep 17 00:00:00 2001 From: Martin Forstner Date: Sun, 21 Aug 2022 16:44:16 +0200 Subject: [PATCH] feat(material/select): add page down/up button functionality --- src/cdk/a11y/key-manager/list-key-manager.ts | 33 +++ src/material/select/select.spec.ts | 219 +++++++++++++++++++ src/material/select/select.ts | 1 + tools/public_api_guard/cdk/a11y.md | 1 + 4 files changed, 254 insertions(+) diff --git a/src/cdk/a11y/key-manager/list-key-manager.ts b/src/cdk/a11y/key-manager/list-key-manager.ts index 8f64379dcbe3..e291934d4acd 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.ts @@ -21,6 +21,8 @@ import { hasModifierKey, HOME, END, + PAGE_UP, + PAGE_DOWN, } from '@angular/cdk/keycodes'; import {debounceTime, filter, map, tap} from 'rxjs/operators'; @@ -50,6 +52,7 @@ export class ListKeyManager { private _horizontal: 'ltr' | 'rtl' | null; private _allowedModifierKeys: ListKeyManagerModifierKey[] = []; private _homeAndEnd = false; + private _pageUpAndDown = {enabled: false, delta: 10}; /** * Predicate function that can be used to check whether an item should be skipped @@ -194,6 +197,17 @@ export class ListKeyManager { return this; } + /** + * Configures the key manager to activate every 10th, configured or first/last element in up/down direction + * respectively when the Page-Up or Page-Down key is pressed. + * @param enabled Whether pressing the Page-Up or Page-Down key activates the first/last item. + * @param delta Whether pressing the Home or End key activates the first/last item. + */ + withPageUpDown(enabled: boolean = true, delta: number = 10): this { + this._pageUpAndDown = {enabled, delta}; + return this; + } + /** * Sets the active item to the item at the index specified. * @param index The index of the item to be set as active. @@ -280,6 +294,25 @@ export class ListKeyManager { return; } + case PAGE_UP: + if (this._pageUpAndDown.enabled && isModifierAllowed) { + const targetIndex = this._activeItemIndex - this._pageUpAndDown.delta; + this._setActiveItemByIndex(targetIndex > 0 ? targetIndex : 0, 1); + break; + } else { + return; + } + + case PAGE_DOWN: + if (this._pageUpAndDown.enabled && isModifierAllowed) { + const targetIndex = this._activeItemIndex + this._pageUpAndDown.delta; + const itemsLength = this._getItemsArray().length; + this._setActiveItemByIndex(targetIndex < itemsLength ? targetIndex : itemsLength - 1, -1); + break; + } else { + return; + } + default: if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) { // Attempt to use the `event.key` which also maps it to the user's keyboard language, diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts index 58b6490a2e9d..c7faf671d50e 100644 --- a/src/material/select/select.spec.ts +++ b/src/material/select/select.spec.ts @@ -11,6 +11,8 @@ import { UP_ARROW, A, ESCAPE, + PAGE_DOWN, + PAGE_UP, } from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; @@ -421,6 +423,39 @@ describe('MDC-based MatSelect', () => { flush(); })); + it('should select first/last options via the PAGE_DOWN/PAGE_UP keys on a closed select with less than 10 options', fakeAsync(() => { + const formControl = fixture.componentInstance.control; + const firstOption = fixture.componentInstance.options.first; + const lastOption = fixture.componentInstance.options.last; + + expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(-1); + + const endEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_DOWN); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); + expect(endEvent.defaultPrevented).toBe(true); + expect(lastOption.selected) + .withContext('Expected last option to be selected.') + .toBe(true); + expect(formControl.value) + .withContext('Expected value from last option to have been set on the model.') + .toBe(lastOption.value); + + const homeEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_UP); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); + expect(homeEvent.defaultPrevented).toBe(true); + expect(firstOption.selected) + .withContext('Expected first option to be selected.') + .toBe(true); + expect(formControl.value) + .withContext('Expected value from first option to have been set on the model.') + .toBe(firstOption.value); + + flush(); + })); + it('should resume focus from selected item after selecting via click', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); @@ -1492,6 +1527,37 @@ describe('MDC-based MatSelect', () => { expect(event.defaultPrevented).toBe(true); })); + it('should focus the last option when pressing PAGE_DOWN with less than 10 options', fakeAsync(() => { + fixture.componentInstance.control.setValue('pizza-1'); + fixture.detectChanges(); + + trigger.click(); + fixture.detectChanges(); + flush(); + + const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_DOWN); + fixture.detectChanges(); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); + expect(event.defaultPrevented).toBe(true); + })); + + it('should focus the first option when pressing PAGE_UP with index < 10', fakeAsync(() => { + fixture.componentInstance.control.setValue('pizza-1'); + fixture.detectChanges(); + + trigger.click(); + fixture.detectChanges(); + flush(); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBeLessThan(10); + const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_UP); + fixture.detectChanges(); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); + expect(event.defaultPrevented).toBe(true); + })); + it('should be able to set extra classes on the panel', fakeAsync(() => { trigger.click(); fixture.detectChanges(); @@ -2349,6 +2415,66 @@ describe('MDC-based MatSelect', () => { .toBe(1173); })); + it('should scroll 10 to the top or to first element when pressing PAGE_UP', fakeAsync(() => { + for (let i = 0; i < 18; i++) { + dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + } + + expect(panel.scrollTop) + .withContext('Expected panel to be scrolled down.') + .toBeGreaterThan(0); + + expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(18); + + dispatchKeyboardEvent(host, 'keydown', PAGE_UP); + fixture.detectChanges(); + + // +