@@ -195,6 +195,7 @@ export class Menu {
195195 private list : HTMLLimelMenuListElement ;
196196 private searchInput : HTMLLimelInputFieldElement ;
197197 private portalId : string ;
198+ private breadcrumbs : HTMLLimelBreadcrumbsElement ;
198199 private triggerElement : HTMLSlotElement ;
199200 private selectedMenuItem ?: MenuItem ;
200201
@@ -322,11 +323,13 @@ export class Menu {
322323
323324 return (
324325 < limel-breadcrumbs
326+ ref = { this . setBreadcrumbsElement }
325327 style = { {
326328 'border-bottom' : 'solid 1px rgb(var(--contrast-500))' ,
327329 'flex-shrink' : '0' ,
328330 } }
329331 onSelect = { this . handleBreadcrumbsSelect }
332+ onKeyDown = { this . handleBreadcrumbsKeyDown }
330333 items = { breadcrumbsItems }
331334 />
332335 ) ;
@@ -445,8 +448,8 @@ export class Menu {
445448 } ;
446449
447450 // Key handler for the input search field
448- // Will change focus to the first/last item in the dropdown
449- // list to enable selection with the keyboard
451+ // Will change focus to breadcrumbs (if present) or the first/last item
452+ // in the dropdown list to enable selection with the keyboard
450453 private handleInputKeyDown = ( event : KeyboardEvent ) => {
451454 const isForwardTab =
452455 event . key === TAB &&
@@ -460,37 +463,71 @@ export class Menu {
460463 return ;
461464 }
462465
463- if ( ! this . list ) {
464- return ;
465- }
466-
467466 event . stopPropagation ( ) ;
468467 event . preventDefault ( ) ;
469468
470469 if ( isForwardTab || isDown ) {
471- const listItems =
472- this . list . shadowRoot . querySelectorAll < HTMLElement > (
473- '.mdc-deprecated-list-item'
474- ) ;
475- const listElement = listItems [ 0 ] ;
476- listElement ?. focus ( ) ;
470+ if ( this . focusBreadcrumbs ( ) ) {
471+ return ;
472+ }
473+
474+ this . focusFirstListItem ( ) ;
477475
478476 return ;
479477 }
480478
481479 if ( isUp ) {
482- const listItems =
483- this . list . shadowRoot . querySelectorAll < HTMLElement > (
484- '.mdc-deprecated-list-item'
485- ) ;
486- const listElement = [ ...listItems ] . at ( - 1 ) ;
487- listElement ?. focus ( ) ;
480+ // Focus the last list item (wrapping behavior)
481+ this . focusLastListItem ( ) ;
488482 }
489483 } ;
490484
491- // Key handler for the menu list
485+ // Key handler for the menu list (capture phase)
486+ // Handles Up arrow on first item and Down arrow on last item
487+ // Must run in capture phase to intercept before MDC Menu wraps focus
488+ // Only intercepts when there's a search input or breadcrumbs to navigate to
489+ private readonly handleListKeyDownCapture = ( event : KeyboardEvent ) => {
490+ const isUp = event . key === ARROW_UP ;
491+ const isDown = event . key === ARROW_DOWN ;
492+
493+ if ( ! isUp && ! isDown ) {
494+ return ;
495+ }
496+
497+ // Up on first item: go to breadcrumbs or search input (if they exist)
498+ if ( isUp && this . isFirstListItemFocused ( ) ) {
499+ // Try to focus breadcrumbs first
500+ if ( this . focusBreadcrumbs ( ) ) {
501+ event . stopPropagation ( ) ;
502+ event . preventDefault ( ) ;
503+
504+ return ;
505+ }
506+
507+ // Then try search input
508+ if ( this . searchInput ) {
509+ event . stopPropagation ( ) ;
510+ event . preventDefault ( ) ;
511+ this . searchInput . focus ( ) ;
512+ }
513+
514+ // If neither exists, let MDC Menu handle wrap-around
515+ return ;
516+ }
517+
518+ // Down on last item: go to search input (if it exists)
519+ if ( isDown && this . isLastListItemFocused ( ) && this . searchInput ) {
520+ event . stopPropagation ( ) ;
521+ event . preventDefault ( ) ;
522+ this . searchInput . focus ( ) ;
523+ }
524+
525+ // If no search input, let MDC Menu handle wrap-around
526+ } ;
527+
528+ // Key handler for the menu list (bubble phase)
492529 // Will change focus to the search field if using shift+tab
493- // And can go forward/back with righ /left arrow keys
530+ // And can go forward/back with right /left arrow keys
494531 private handleMenuKeyDown = ( event : KeyboardEvent ) => {
495532 const isBackwardTab =
496533 event . key === TAB &&
@@ -499,7 +536,6 @@ export class Menu {
499536 event . shiftKey ;
500537
501538 const isLeft = event . key === ARROW_LEFT ;
502-
503539 const isRight = event . key === ARROW_RIGHT ;
504540
505541 if ( ! isBackwardTab && ! isLeft && ! isRight ) {
@@ -510,7 +546,11 @@ export class Menu {
510546 event . stopPropagation ( ) ;
511547 event . preventDefault ( ) ;
512548 this . searchInput ?. focus ( ) ;
513- } else if ( ! this . gridLayout ) {
549+
550+ return ;
551+ }
552+
553+ if ( ! this . gridLayout && ( isLeft || isRight ) ) {
514554 const currentItem = this . getCurrentItem ( ) ;
515555
516556 event . stopPropagation ( ) ;
@@ -523,20 +563,60 @@ export class Menu {
523563 }
524564 } ;
525565
566+ // Key handler for breadcrumbs
567+ // Up arrow: focus search input
568+ // Down arrow: focus first list item
569+ private handleBreadcrumbsKeyDown = ( event : KeyboardEvent ) => {
570+ const isUp = event . key === ARROW_UP ;
571+ const isDown = event . key === ARROW_DOWN ;
572+
573+ if ( ! isUp && ! isDown ) {
574+ return ;
575+ }
576+
577+ event . stopPropagation ( ) ;
578+ event . preventDefault ( ) ;
579+
580+ if ( isUp ) {
581+ this . searchInput ?. focus ( ) ;
582+
583+ return ;
584+ }
585+
586+ if ( isDown ) {
587+ this . focusFirstListItem ( ) ;
588+ }
589+ } ;
590+
526591 private clearSearch = ( ) => {
527592 this . searchValue = '' ;
528593 this . searchResults = null ;
529594 this . loadingSubItems = false ;
530595 } ;
531596
532- private getCurrentItem = ( ) : MenuItem => {
533- const activeItem = this . list ?. shadowRoot ?. querySelector (
534- '[role="menuitem"][tabindex="0"]'
597+ private readonly getCurrentItem = ( ) : MenuItem => {
598+ let menuElement =
599+ ( this . list ?. shadowRoot ?. activeElement as HTMLElement | null ) ??
600+ null ;
601+
602+ if ( menuElement && menuElement . getAttribute ( 'role' ) !== 'menuitem' ) {
603+ menuElement = menuElement . closest < HTMLElement > ( '[role="menuitem"]' ) ;
604+ }
605+
606+ if ( ! menuElement ) {
607+ menuElement = this . list ?. shadowRoot ?. querySelector < HTMLElement > (
608+ '[role="menuitem"][tabindex="0"]'
609+ ) ;
610+ }
611+
612+ const dataIndex = Number . parseInt (
613+ menuElement ?. dataset . index ?? '0' ,
614+ 10
535615 ) ;
536- const attrIndex = activeItem ?. attributes ?. getNamedItem ( 'data-index' ) ;
537- const dataIndex = Number . parseInt ( attrIndex ?. value || '0' , 10 ) ;
538616
539- return this . visibleItems [ dataIndex ] as MenuItem ;
617+ const item = this . visibleItems [ dataIndex ] ;
618+
619+ return ( item ?? this . visibleItems [ 0 ] ) as MenuItem ;
540620 } ;
541621
542622 private goForward = ( currentItem : MenuItem ) => {
@@ -673,7 +753,23 @@ export class Menu {
673753 }
674754
675755 private setListElement = ( element : HTMLLimelMenuListElement ) => {
756+ if ( this . list ) {
757+ this . list . removeEventListener (
758+ 'keydown' ,
759+ this . handleListKeyDownCapture ,
760+ true
761+ ) ;
762+ }
763+
676764 this . list = element ;
765+
766+ if ( this . list ) {
767+ this . list . addEventListener (
768+ 'keydown' ,
769+ this . handleListKeyDownCapture ,
770+ true
771+ ) ;
772+ }
677773 } ;
678774
679775 private setFocus = ( ) => {
@@ -702,7 +798,88 @@ export class Menu {
702798 this . searchInput = element ;
703799 } ;
704800
705- private focusMenuItem = ( ) => {
801+ private readonly setBreadcrumbsElement = (
802+ element : HTMLLimelBreadcrumbsElement
803+ ) => {
804+ this . breadcrumbs = element ;
805+ } ;
806+
807+ /**
808+ * Focuses the first focusable element inside breadcrumbs.
809+ * Returns true if breadcrumbs exist and were focused,
810+ * false otherwise.
811+ */
812+ private readonly focusBreadcrumbs = ( ) : boolean => {
813+ if ( ! this . breadcrumbs ) {
814+ return false ;
815+ }
816+
817+ const focusableElement =
818+ this . breadcrumbs . shadowRoot ?. querySelector < HTMLElement > (
819+ 'button, a'
820+ ) ;
821+ if ( focusableElement ) {
822+ focusableElement . focus ( ) ;
823+
824+ return true ;
825+ }
826+
827+ return false ;
828+ } ;
829+
830+ private readonly focusFirstListItem = ( ) => {
831+ const listItems = this . getListItems ( ) ;
832+ const firstItem = listItems ?. [ 0 ] ;
833+ firstItem ?. focus ( ) ;
834+ } ;
835+
836+ private readonly focusLastListItem = ( ) => {
837+ const listItems = this . getListItems ( ) ;
838+ const lastItem = listItems ?. at ( - 1 ) ;
839+ lastItem ?. focus ( ) ;
840+ } ;
841+
842+ private readonly isFirstListItemFocused = ( ) : boolean => {
843+ const listItems = this . getListItems ( ) ;
844+ if ( ! listItems ) {
845+ return false ;
846+ }
847+
848+ const firstItem = listItems [ 0 ] ;
849+ const activeElement = this . list . shadowRoot ?. activeElement ;
850+
851+ return firstItem === activeElement ;
852+ } ;
853+
854+ private readonly isLastListItemFocused = ( ) : boolean => {
855+ const listItems = this . getListItems ( ) ;
856+ if ( ! listItems ) {
857+ return false ;
858+ }
859+
860+ const lastItem = listItems . at ( - 1 ) ;
861+ const activeElement = this . list . shadowRoot ?. activeElement ;
862+
863+ return lastItem === activeElement ;
864+ } ;
865+
866+ private readonly getListItems = ( ) : HTMLElement [ ] | null => {
867+ if ( ! this . list ) {
868+ return null ;
869+ }
870+
871+ const items = this . list . shadowRoot ?. querySelectorAll < HTMLElement > (
872+ '.mdc-deprecated-list-item'
873+ ) ;
874+
875+ if ( ! items ?. length ) {
876+ return null ;
877+ }
878+
879+ return [ ...items ] ;
880+ } ;
881+
882+ private readonly focusMenuItem = ( ) => {
706883 if ( ! this . list ) {
707884 return ;
708885 }
0 commit comments