@@ -34,6 +34,7 @@ import {SelectValidator} from '../../labs/behaviors/validators/select-validator.
34
34
import { getActiveItem } from '../../list/internal/list-navigation-helpers.js' ;
35
35
import {
36
36
CloseMenuEvent ,
37
+ FocusState ,
37
38
isElementInSubtree ,
38
39
isSelectableKey ,
39
40
} from '../../menu/internal/controllers/shared.js' ;
@@ -157,7 +158,7 @@ export abstract class Select extends selectBaseClass {
157
158
* Whether the menu should be aligned to the start or the end of the select's
158
159
* textbox.
159
160
*/
160
- @property ( { attribute : 'menu-align' } ) menuAlign : 'start' | 'end' = 'start' ;
161
+ @property ( { attribute : 'menu-align' } ) menuAlign : 'start' | 'end' = 'start' ;
161
162
162
163
/**
163
164
* The value of the currently selected option.
@@ -248,6 +249,7 @@ export abstract class Select extends selectBaseClass {
248
249
249
250
@state ( ) private focused = false ;
250
251
@state ( ) private open = false ;
252
+ @state ( ) private defaultFocus : FocusState = FocusState . NONE ;
251
253
@query ( '.field' ) private readonly field ! : Field | null ;
252
254
@query ( 'md-menu' ) private readonly menu ! : Menu | null ;
253
255
@query ( '#label' ) private readonly labelEl ! : HTMLElement ;
@@ -466,7 +468,7 @@ export abstract class Select extends selectBaseClass {
466
468
return html `< div class ="menu-wrapper ">
467
469
< md-menu
468
470
id ="listbox "
469
- default-focus =" none "
471
+ .defaultFocus = ${ this . defaultFocus }
470
472
role ="listbox"
471
473
tabindex="-1"
472
474
aria-label=${ ariaLabel || nothing }
@@ -484,8 +486,8 @@ export abstract class Select extends selectBaseClass {
484
486
.quick=${ this . quick }
485
487
.positioning=${ this . menuPositioning }
486
488
.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' }
489
491
@opening=${ this . handleOpening }
490
492
@opened=${ this . redispatchEvent }
491
493
@closing=${ this . redispatchEvent }
@@ -515,13 +517,35 @@ export abstract class Select extends selectBaseClass {
515
517
const isOpenKey =
516
518
event . code === 'Space' ||
517
519
event . code === 'ArrowDown' ||
520
+ event . code === 'ArrowUp' ||
521
+ event . code === 'End' ||
522
+ event . code === 'Home' ||
518
523
event . code === 'Enter' ;
519
524
520
525
// Do not open if currently typing ahead because the user may be typing the
521
526
// spacebar to match a word with a space
522
527
if ( ! typeaheadController . isTypingAhead && isOpenKey ) {
523
528
event . preventDefault ( ) ;
524
529
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
+ }
525
549
return ;
526
550
}
527
551
@@ -634,6 +658,12 @@ export abstract class Select extends selectBaseClass {
634
658
this . labelEl ?. removeAttribute ?.( 'aria-live' ) ;
635
659
this . redispatchEvent ( e ) ;
636
660
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
+
637
667
const items = this . menu ! . items as SelectOption [ ] ;
638
668
const activeItem = getActiveItem ( items ) ?. item ;
639
669
let [ selectedItem ] = this . lastSelectedOptionRecords [ 0 ] ?? [ null ] ;
0 commit comments