import { CSSResultArray , html , PropertyValues , TemplateResult , } from '@spectrum-web-components/base' ; import { property } from '@spectrum-web-components/base/src/decorators.js' ; import sidenavStyles from './sidenav.css.js' ; import { Focusable , getActiveElement } from '@spectrum-web-components/shared' ; import { SideNavItem } from './SidenavItem.js' ; import { SideNavHeading } from './SidenavHeading.js' ; export interface SidenavSelectDetail { value : string ; } element slot fires export class SideNav extends Focusable { public static get styles ( ) : CSSResultArray { return [ sidenavStyles ] ; } private items = new Set < SideNavItem > ( ) ; public startTrackingSelectionForItem ( item ) { this . items . add ( item ) ; } public stopTrackingSelectionForItem ( item ) { this . items . delete ( item ) ; } @ property ( { type : Boolean , reflect : true , attribute : 'manage-tab-index' } ) public manageTabIndex = false ; @ property ( { reflect : true } ) public value : string | undefined = undefined ; private handleSelect ( event ) { event . stopPropagation ( ) ; if ( this . value === event . detail . value ) { return ; } const oldValue = this . value ; this . value = event . detail . value ; const applyDefault = this . dispatchEvent ( new Event ( 'change' , { bubbles : true , composed : true , cancelable : true , } ) ) ; if ( ! applyDefault ) { this . value = oldValue ; event . target . selected = false ; event . preventDefault ( ) ; } else { this . items . forEach ( ( item ) => item . handleSideNavSelect ( event ) ) ; } } public constructor ( ) { super ( ) ; this . addEventListener ( 'focusin' , this . startListeningToKeyboard ) ; } public focus ( ) { if ( this . focusElement === this ) { return ; } super . focus ( ) ; } public blur ( ) { if ( this . focusElement === this ) { return ; } super . blur ( ) ; } public click ( ) { if ( this . focusElement === this ) { return ; } super . click ( ) ; } public get focusElement ( ) : SideNavItem | SideNav { const selected = this . querySelector ( '[selected]' ) as SideNavItem ; if ( selected && ! this . isDisabledChild ( selected ) ) { return selected ; } const items = [ ... this . querySelectorAll ( 'sp-sidenav-item' ) ] ; let index = 0 ; while ( index < items . length && items [ index ] && this . isDisabledChild ( items [ index ] ) ) { index += 1 ; } if ( items [ index ] ) { return items [ index ] ; } return this ; } private startListeningToKeyboard ( ) { this . addEventListener ( 'keydown' , this . handleKeydown ) ; this . addEventListener ( 'focusout' , this . stopListeningToKeyboard ) ; if ( this . value && this . manageTabIndex ) { const selected = this . querySelector ( `[value="${ this . value }"]` ) as SideNavItem ; if ( selected ) { selected . tabIndex = - 1 ; } } } private stopListeningToKeyboard ( ) { this . removeEventListener ( 'keydown' , this . handleKeydown ) ; this . removeEventListener ( 'focusout' , this . stopListeningToKeyboard ) ; if ( this . value && this . manageTabIndex ) { const selected = this . querySelector ( `[value="${ this . value }"]` ) as SideNavItem ; if ( selected ) { selected . tabIndex = 0 ; } } } private handleKeydown ( event ) { const { code } = event ; if ( code !== 'ArrowDown' && code !== 'ArrowUp' ) { return ; } event . preventDefault ( ) ; const direction = code === 'ArrowDown' ? 1 : - 1 ; this . focusItemByOffset ( direction ) ; } private focusItemByOffset ( direction ) { const items = [ ... this . querySelectorAll ( 'sp-sidenav-item' ) ] ; const focused = items . indexOf ( getActiveElement ( this ) as SideNavItem ) ; let next = focused ; next = ( items . length + next + direction ) % items . length ; let nextItem = items [ next ] ; while ( nextItem && this . isDisabledChild ( nextItem ) ) { next = ( items . length + next + direction ) % items . length ; nextItem = items [ next ] ; } if ( ! nextItem || this . isDisabledChild ( nextItem ) ) { return ; } nextItem . focus ( ) ; } private isDisabledChild ( child ) { if ( child . disabled ) { return true ; } let parent = child . parentElement as SideNavItem | SideNav | SideNavHeading ; while ( parent instanceof SideNavHeading || ( ! ( parent as SideNavItem ) . disabled && parent instanceof SideNavItem && parent . expanded ) ) { parent = parent . parentElement as SideNavItem | SideNav | SideNavHeading ; } return parent !== this ; } private handleSlotchange ( ) { this . manageTabIndexes ( ) ; } private async manageTabIndexes ( ) { if ( ! this . value && this . manageTabIndex ) { const managed = this . querySelector ( 'sp-sidenav-item:not([tabindex])' ) as SideNavItem ; if ( managed ) { managed . tabIndex = - 1 ; } const first = this . querySelector ( 'sp-sidenav-item' ) ; if ( first ) { await first . updateComplete ; first . tabIndex = 0 ; } } } protected render ( ) { return html `
            <nav @sidenav-select=${ this . handleSelect }>
                <slot
                    name="descendant"
                    @slotchange=${ this . handleSlotchange }
                ></slot>
            </nav>
        ` ; } protected firstUpdated ( changes ) { super . firstUpdated ( changes ) ; const selectedChild = this . querySelector ( '[selected]' ) as SideNavItem ; if ( selectedChild ) { this . value = selectedChild . value ; } else { this . manageTabIndexes ( ) ; } } protected updated ( changes ) { super . updated ( changes ) ; if ( changes . has ( 'manageTabIndex' ) ) { const items = [ ... this . querySelectorAll ( 'sp-sidenav-item' ) ] ; items . map ( ( item ) => ( item . manageTabIndex = this . manageTabIndex ) ) ; } } } declare global { interface GlobalEventHandlersEventMap { 'sp-sidenav:select' : CustomEvent < SidenavSelectDetail > ; } }