license import './mwc-list-item' ; import { BaseElement } from '@material/mwc-base/base-element' ; import { observer } from '@material/mwc-base/observer' ; import { deepActiveElementPath , doesElementContainFocus , isNodeElement } from '@material/mwc-base/utils' ; import { html } from 'lit' ; import { property , query , queryAssignedNodes } from 'lit/decorators.js' ; import { ifDefined } from 'lit/directives/if-defined.js' ; import { MDCListAdapter } from './mwc-list-adapter' ; import MDCListFoundation , { ActionDetail , isIndexSet , SelectedDetail } from './mwc-list-foundation' ; import { MWCListIndex } from './mwc-list-foundation' ; import { Layoutable , ListItemBase , RequestSelectedDetail } from './mwc-list-item-base' ; export { ActionDetail , createSetFromIndex , isEventMulti , isIndexSet , MultiSelectedEvent , MWCListIndex , SelectedDetail , SelectedEvent , SingleSelectedEvent } from './mwc-list-foundation' ; function debounceLayout ( callback , waitInMS = 50 ) { let timeoutId ; return function ( updateItems = true ) { clearTimeout ( timeoutId ) ; timeoutId = setTimeout ( ( ) => { callback ( updateItems ) ; } , waitInMS ) as unknown as number ; } ; } const isListItem = ( element ) : element is ListItemBase => { return element . hasAttribute ( 'mwc-list-item' ) ; } ; function clearAndCreateItemsReadyPromise ( this ) { const oldResolver = this . itemsReadyResolver ; this . itemsReady = new Promise ( ( res ) => { return this . itemsReadyResolver = res as any ; } ) ; oldResolver ( ) ; } fires fires fires export abstract class ListBase extends BaseElement implements Layoutable { protected mdcFoundation ! : MDCListFoundation ; protected mdcAdapter : MDCListAdapter | null = null ; protected readonly mdcFoundationClass = MDCListFoundation ; @ property ( { type : String } ) emptyMessage : string | undefined ; @ query ( '.mdc-deprecated-list' ) protected mdcRoot ! : HTMLElement ; @ queryAssignedNodes ( '' , true , '*' ) protected assignedElements ! : HTMLElement [ ] | null ; @ queryAssignedNodes ( '' , true , '[tabindex="0"]' ) protected tabbableElements ! : HTMLElement [ ] | null ; @ property ( { type : Boolean } ) @ observer ( function ( this , value ) { if ( this . mdcFoundation ) { this . mdcFoundation . setUseActivatedClass ( value ) ; } } ) activatable = false ; @ property ( { type : Boolean } ) @ observer ( function ( this , newValue , oldValue ) { if ( this . mdcFoundation ) { this . mdcFoundation . setMulti ( newValue ) ; } if ( oldValue !== undefined ) { this . layout ( ) ; } } ) multi = false ; @ property ( { type : Boolean } ) @ observer ( function ( this , value ) { if ( this . mdcFoundation ) { this . mdcFoundation . setWrapFocus ( value ) ; } } ) wrapFocus = false ; @ property ( { type : String } ) @ observer ( function ( this , _newValue , oldValue ) { if ( oldValue !== undefined ) { this . updateItems ( ) ; } } ) itemRoles : string | null = null ; @ property ( { type : String } ) innerRole : string | null = null ; @ property ( { type : String } ) innerAriaLabel : string | null = null ; @ property ( { type : Boolean } ) rootTabbable = false ; protected previousTabindex : Element | null = null ; @ property ( { type : Boolean , reflect : true } ) @ observer ( function ( this , value ) { if ( value ) { const tabbable = this . tabbableElements ?. [ 0 ] ?? null ; this . previousTabindex = tabbable ; if ( tabbable ) { tabbable . setAttribute ( 'tabindex' , '-1' ) ; } } else if ( ! value && this . previousTabindex ) { this . previousTabindex . setAttribute ( 'tabindex' , '0' ) ; this . previousTabindex = null ; } } ) noninteractive = false ; debouncedLayout : ( updateItems ? : boolean ) => void | undefined ; protected itemsReadyResolver : ( value ? : ( PromiseLike < never [ ] > | never [ ] | undefined ) ) => void = ( ( ) => { } ) as ( value ? : ( PromiseLike < unknown [ ] > | unknown [ ] ) ) => void ; constructor ( ) { super ( ) ; const debouncedFunction = debounceLayout ( this . layout . bind ( this ) ) ; this . debouncedLayout = ( updateItems = true ) => { clearAndCreateItemsReadyPromise . call ( this ) ; debouncedFunction ( updateItems ) ; } ; } itemsReady = Promise . resolve ( [ ] ) ; protected override async getUpdateComplete ( ) { const result = await super . getUpdateComplete ( ) ; await this . itemsReady ; return result ; } protected items_ : ListItemBase [ ] = [ ] ; get items ( ) : ListItemBase [ ] { return this . items_ ; } protected updateItems ( ) { const nodes = this . assignedElements ?? [ ] ; const listItems = [ ] ; for ( const node of nodes ) { if ( isListItem ( node ) ) { listItems . push ( node ) ; node . _managingList = this ; } if ( node . hasAttribute ( 'divider' ) && ! node . hasAttribute ( 'role' ) ) { node . setAttribute ( 'role' , 'separator' ) ; } } this . items_ = listItems ; const selectedIndices = new Set < number > ( ) ; this . items_ . forEach ( ( item , index ) => { if ( this . itemRoles ) { item . setAttribute ( 'role' , this . itemRoles ) ; } else { item . removeAttribute ( 'role' ) ; } if ( item . selected ) { selectedIndices . add ( index ) ; } } ) ; if ( this . multi ) { this . select ( selectedIndices ) ; } else { const index = selectedIndices . size ? selectedIndices . entries ( ) . next ( ) . value [ 1 ] : - 1 ; this . select ( index ) ; } const itemsUpdatedEv = new Event ( 'items-updated' , { bubbles : true , composed : true } ) ; this . dispatchEvent ( itemsUpdatedEv ) ; } get selected ( ) : ListItemBase | ListItemBase [ ] | null { const index = this . index ; if ( ! isIndexSet ( index ) ) { if ( index === - 1 ) { return null ; } return this . items [ index ] ; } const selected = [ ] ; for ( const entry of index ) { selected . push ( this . items [ entry ] ) ; } return selected ; } get index ( ) : MWCListIndex { if ( this . mdcFoundation ) { return this . mdcFoundation . getSelectedIndex ( ) ; } return - 1 ; } override render ( ) { const role = this . innerRole === null ? undefined : this . innerRole ; const ariaLabel = this . innerAriaLabel === null ? undefined : this . innerAriaLabel ; const tabindex = this . rootTabbable ? '0' : '-1' ; return html `
      <!-- @ts-ignore -->
      <ul
          tabindex=${ tabindex }
          role="${ ifDefined ( role ) }"
          aria-label="${ ifDefined ( ariaLabel ) }"
          class="mdc-deprecated-list"
          @keydown=${ this . onKeydown }
          @focusin=${ this . onFocusIn }
          @focusout=${ this . onFocusOut }
          @request-selected=${ this . onRequestSelected }
          @list-item-rendered=${ this . onListItemConnected }>
        <slot></slot>
        ${ this . renderPlaceholder ( ) }
      </ul>
    ` ; } renderPlaceholder ( ) { const nodes = this . assignedElements ?? [ ] ; if ( this . emptyMessage !== undefined && nodes . length === 0 ) { return html `
        <mwc-list-item noninteractive>${ this . emptyMessage }</mwc-list-item>
      ` ; } return null ; } override firstUpdated ( ) { super . firstUpdated ( ) ; if ( ! this . items . length ) { this . mdcFoundation . setMulti ( this . multi ) ; this . layout ( ) ; } } protected onFocusIn ( evt ) { if ( this . mdcFoundation && this . mdcRoot ) { const index = this . getIndexOfTarget ( evt ) ; this . mdcFoundation . handleFocusIn ( evt , index ) ; } } protected onFocusOut ( evt ) { if ( this . mdcFoundation && this . mdcRoot ) { const index = this . getIndexOfTarget ( evt ) ; this . mdcFoundation . handleFocusOut ( evt , index ) ; } } protected onKeydown ( evt ) { if ( this . mdcFoundation && this . mdcRoot ) { const index = this . getIndexOfTarget ( evt ) ; const target = evt . target as Element ; const isRootListItem = isListItem ( target ) ; this . mdcFoundation . handleKeydown ( evt , isRootListItem , index ) ; } } protected onRequestSelected ( evt ) { if ( this . mdcFoundation ) { let index = this . getIndexOfTarget ( evt ) ; if ( index === - 1 ) { this . layout ( ) ; index = this . getIndexOfTarget ( evt ) ; if ( index === - 1 ) { return ; } } const element = this . items [ index ] ; if ( element . disabled ) { return ; } const selected = evt . detail . selected ; const source = evt . detail . source ; this . mdcFoundation . handleSingleSelection ( index , source === 'interaction' , selected ) ; evt . stopPropagation ( ) ; } } protected getIndexOfTarget ( evt ) { const elements = this . items ; const path = evt . composedPath ( ) ; for ( const pathItem of path as Node [ ] ) { let index = - 1 ; if ( isNodeElement ( pathItem ) && isListItem ( pathItem ) ) { index = elements . indexOf ( pathItem ) ; } if ( index !== - 1 ) { return index ; } } return - 1 ; } protected createAdapter ( ) { this . mdcAdapter = { getListItemCount : ( ) => { if ( this . mdcRoot ) { return this . items . length ; } return 0 ; } , getFocusedElementIndex : this . getFocusedItemIndex , getAttributeForElementIndex : ( index , attr ) => { const listElement = this . mdcRoot ; if ( ! listElement ) { return '' ; } const element = this . items [ index ] ; return element ? element . getAttribute ( attr ) : '' ; } , setAttributeForElementIndex : ( index , attr , val ) => { if ( ! this . mdcRoot ) { return ; } const element = this . items [ index ] ; if ( element ) { element . setAttribute ( attr , val ) ; } } , focusItemAtIndex : ( index ) => { const element = this . items [ index ] ; if ( element ) { element . focus ( ) ; } } , setTabIndexForElementIndex : ( index , value ) => { const item = this . items [ index ] ; if ( item ) { item . tabindex = value ; } } , notifyAction : ( index ) => { const init = { bubbles : true , composed : true } ; init . detail = { index } ; const ev = new CustomEvent < ActionDetail > ( 'action' , init ) ; this . dispatchEvent ( ev ) ; } , notifySelected : ( index , diff ) => { const init = { bubbles : true , composed : true } ; init . detail = { index , diff } ; const ev = new CustomEvent < SelectedDetail > ( 'selected' , init ) ; this . dispatchEvent ( ev ) ; } , isFocusInsideList : ( ) => { return doesElementContainFocus ( this ) ; } , isRootFocused : ( ) => { const mdcRoot = this . mdcRoot ; const root = mdcRoot . getRootNode ( ) as unknown as DocumentOrShadowRoot ; return root . activeElement === mdcRoot ; } , setDisabledStateForElementIndex : ( index , value ) => { const item = this . items [ index ] ; if ( ! item ) { return ; } item . disabled = value ; } , getDisabledStateForElementIndex : ( index ) => { const item = this . items [ index ] ; if ( ! item ) { return false ; } return item . disabled ; } , setSelectedStateForElementIndex : ( index , value ) => { const item = this . items [ index ] ; if ( ! item ) { return ; } item . selected = value ; } , getSelectedStateForElementIndex : ( index ) => { const item = this . items [ index ] ; if ( ! item ) { return false ; } return item . selected ; } , setActivatedStateForElementIndex : ( index , value ) => { const item = this . items [ index ] ; if ( ! item ) { return ; } item . activated = value ; } , } ; return this . mdcAdapter ; } protected selectUi ( index , activate = false ) { const item = this . items [ index ] ; if ( item ) { item . selected = true ; item . activated = activate ; } } protected deselectUi ( index ) { const item = this . items [ index ] ; if ( item ) { item . selected = false ; item . activated = false ; } } select ( index ) { if ( ! this . mdcFoundation ) { return ; } this . mdcFoundation . setSelectedIndex ( index ) ; } toggle ( index , force ? ) { if ( this . multi ) { this . mdcFoundation . toggleMultiAtIndex ( index , force ) ; } } protected onListItemConnected ( e ) { const target = e . target as ListItemBase ; this . layout ( this . items . indexOf ( target ) === - 1 ) ; } layout ( updateItems = true ) { if ( updateItems ) { this . updateItems ( ) ; } const first = this . items [ 0 ] ; for ( const item of this . items ) { item . tabindex = - 1 ; } if ( first ) { if ( this . noninteractive ) { if ( ! this . previousTabindex ) { this . previousTabindex = first ; } } else { first . tabindex = 0 ; } } this . itemsReadyResolver ( ) ; } getFocusedItemIndex ( ) { if ( ! this . mdcRoot ) { return - 1 ; } if ( ! this . items . length ) { return - 1 ; } const activeElementPath = deepActiveElementPath ( ) ; if ( ! activeElementPath . length ) { return - 1 ; } for ( let i = activeElementPath . length - 1 ; i >= 0 ; i -- ) { const activeItem = activeElementPath [ i ] ; if ( isListItem ( activeItem ) ) { return this . items . indexOf ( activeItem ) ; } } return - 1 ; } focusItemAtIndex ( index ) { for ( const item of this . items ) { if ( item . tabindex === 0 ) { item . tabindex = - 1 ; break ; } } this . items [ index ] . tabindex = 0 ; this . items [ index ] . focus ( ) ; } override focus ( ) { const root = this . mdcRoot ; if ( root ) { root . focus ( ) ; } } override blur ( ) { const root = this . mdcRoot ; if ( root ) { root . blur ( ) ; } } }