Skip to content

Commit 8912019

Browse files
Elliott Marquezcopybara-github
authored andcommitted
feat(select): add keyboard support for arrow end and home
PiperOrigin-RevId: 592643729
1 parent 9c5cff8 commit 8912019

File tree

2 files changed

+35
-7
lines changed

2 files changed

+35
-7
lines changed

select/demo/stories.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ const selects: MaterialStoryInit<StoryKnobs> = {
3636
name: 'Selects',
3737
render(knobs) {
3838
return html`
39-
<div
40-
style="display: flex; gap: 16px;flex-direction: column;align-items:end;border: 1px solid black;">
39+
<div style="display: flex; gap: 16px;">
4140
<md-filled-select
42-
style="min-width: 100px;width: fit-content;"
4341
.label=${knobs.label}
4442
.quick=${knobs.quick}
4543
.required=${knobs.required}

select/internal/select.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {SelectValidator} from '../../labs/behaviors/validators/select-validator.
3434
import {getActiveItem} from '../../list/internal/list-navigation-helpers.js';
3535
import {
3636
CloseMenuEvent,
37+
FocusState,
3738
isElementInSubtree,
3839
isSelectableKey,
3940
} from '../../menu/internal/controllers/shared.js';
@@ -157,7 +158,7 @@ export abstract class Select extends selectBaseClass {
157158
* Whether the menu should be aligned to the start or the end of the select's
158159
* textbox.
159160
*/
160-
@property({attribute: 'menu-align'}) menuAlign: 'start'|'end' = 'start';
161+
@property({attribute: 'menu-align'}) menuAlign: 'start' | 'end' = 'start';
161162

162163
/**
163164
* The value of the currently selected option.
@@ -248,6 +249,7 @@ export abstract class Select extends selectBaseClass {
248249

249250
@state() private focused = false;
250251
@state() private open = false;
252+
@state() private defaultFocus: FocusState = FocusState.NONE;
251253
@query('.field') private readonly field!: Field | null;
252254
@query('md-menu') private readonly menu!: Menu | null;
253255
@query('#label') private readonly labelEl!: HTMLElement;
@@ -466,7 +468,7 @@ export abstract class Select extends selectBaseClass {
466468
return html`<div class="menu-wrapper">
467469
<md-menu
468470
id="listbox"
469-
default-focus="none"
471+
.defaultFocus=${this.defaultFocus}
470472
role="listbox"
471473
tabindex="-1"
472474
aria-label=${ariaLabel || nothing}
@@ -484,8 +486,8 @@ export abstract class Select extends selectBaseClass {
484486
.quick=${this.quick}
485487
.positioning=${this.menuPositioning}
486488
.typeaheadDelay=${this.typeaheadDelay}
487-
.anchorCorner=${this.menuAlign === 'start'? 'end-start' : 'end-end'}
488-
.menuCorner=${this.menuAlign === 'start'? 'start-start' : 'start-end'}
489+
.anchorCorner=${this.menuAlign === 'start' ? 'end-start' : 'end-end'}
490+
.menuCorner=${this.menuAlign === 'start' ? 'start-start' : 'start-end'}
489491
@opening=${this.handleOpening}
490492
@opened=${this.redispatchEvent}
491493
@closing=${this.redispatchEvent}
@@ -515,13 +517,35 @@ export abstract class Select extends selectBaseClass {
515517
const isOpenKey =
516518
event.code === 'Space' ||
517519
event.code === 'ArrowDown' ||
520+
event.code === 'ArrowUp' ||
521+
event.code === 'End' ||
522+
event.code === 'Home' ||
518523
event.code === 'Enter';
519524

520525
// Do not open if currently typing ahead because the user may be typing the
521526
// spacebar to match a word with a space
522527
if (!typeaheadController.isTypingAhead && isOpenKey) {
523528
event.preventDefault();
524529
this.open = true;
530+
531+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/#kbd_label
532+
switch (event.code) {
533+
case 'Space':
534+
case 'ArrowDown':
535+
case 'Enter':
536+
// We will handle focusing last selected item in this.handleOpening()
537+
this.defaultFocus = FocusState.NONE;
538+
break;
539+
case 'End':
540+
this.defaultFocus = FocusState.LAST_ITEM;
541+
break;
542+
case 'ArrowUp':
543+
case 'Home':
544+
this.defaultFocus = FocusState.FIRST_ITEM;
545+
break;
546+
default:
547+
break;
548+
}
525549
return;
526550
}
527551

@@ -634,6 +658,12 @@ export abstract class Select extends selectBaseClass {
634658
this.labelEl?.removeAttribute?.('aria-live');
635659
this.redispatchEvent(e);
636660

661+
// FocusState.NONE means we want to handle focus ourselves and focus the
662+
// last selected item.
663+
if (this.defaultFocus !== FocusState.NONE) {
664+
return;
665+
}
666+
637667
const items = this.menu!.items as SelectOption[];
638668
const activeItem = getActiveItem(items)?.item;
639669
let [selectedItem] = this.lastSelectedOptionRecords[0] ?? [null];

0 commit comments

Comments
 (0)