license import { MDCFoundation } from '@material/base/foundation' ; import { normalizeKey } from '@material/dom/keyboard' ; import { numbers , strings } from '@material/list/constants' ; import { MDCListAdapter } from './mwc-list-adapter' ; export type MWCListIndex = number | Set < number > ; export interface IndexDiff { added : number [ ] ; removed : number [ ] ; } export interface SelectedDetail < T extends MWCListIndex = MWCListIndex > { index : T ; diff : T extends Set < number > ? IndexDiff : undefined ; } export interface ActionDetail { index : number ; } const integerSort = ( a , b ) => { return a - b ; } ; export type SingleSelectedEvent = CustomEvent < SelectedDetail < number > > ; export type MultiSelectedEvent = CustomEvent < SelectedDetail < Set < number > > > ; export type SelectedEvent = SingleSelectedEvent | MultiSelectedEvent ; const findIndexDiff = ( oldSet , newSet ) : IndexDiff => { const oldArr = Array . from ( oldSet ) ; const newArr = Array . from ( newSet ) ; const diff = { added : [ ] , removed : [ ] } ; const oldSorted = oldArr . sort ( integerSort ) ; const newSorted = newArr . sort ( integerSort ) ; let i = 0 ; let j = 0 ; while ( i < oldSorted . length || j < newSorted . length ) { const oldVal = oldSorted [ i ] ; const newVal = newSorted [ j ] ; if ( oldVal === newVal ) { i ++ ; j ++ ; continue ; } if ( oldVal !== undefined && ( newVal === undefined || oldVal < newVal ) ) { diff . removed . push ( oldVal ) ; i ++ ; continue ; } if ( newVal !== undefined && ( oldVal === undefined || newVal < oldVal ) ) { diff . added . push ( newVal ) ; j ++ ; continue ; } } return diff ; } ; const ELEMENTS_KEY_ALLOWED_IN = [ 'input' , 'button' , 'textarea' , 'select' ] ; export function isIndexSet ( selectedIndex ) { return selectedIndex instanceof Set ; } export function isEventMulti ( evt ) { return isIndexSet ( evt . detail . index ) ; } export const createSetFromIndex = ( index ) => { const entry = index === numbers . UNSET_INDEX ? new Set < number > ( ) : index ; return isIndexSet ( entry ) ? new Set ( entry ) : new Set ( [ entry ] ) ; } ; export class MDCListFoundation extends MDCFoundation < MDCListAdapter > { static override get strings ( ) { return strings ; } static override get numbers ( ) { return numbers ; } static override get defaultAdapter ( ) : MDCListAdapter { return { focusItemAtIndex : ( ) => undefined , getFocusedElementIndex : ( ) => 0 , getListItemCount : ( ) => 0 , isFocusInsideList : ( ) => false , isRootFocused : ( ) => false , notifyAction : ( ) => undefined , notifySelected : ( ) => undefined , getSelectedStateForElementIndex : ( ) => false , setDisabledStateForElementIndex : ( ) => undefined , getDisabledStateForElementIndex : ( ) => false , setSelectedStateForElementIndex : ( ) => undefined , setActivatedStateForElementIndex : ( ) => undefined , setTabIndexForElementIndex : ( ) => undefined , setAttributeForElementIndex : ( ) => undefined , getAttributeForElementIndex : ( ) => null , } ; } protected isMulti_ = false ; private wrapFocus_ = false ; private isVertical_ = true ; private selectedIndex_ : MWCListIndex = numbers . UNSET_INDEX ; private focusedItemIndex_ = numbers . UNSET_INDEX ; private useActivatedClass_ = false ; private ariaCurrentAttrValue_ : string | null = null ; constructor ( adapter ? ) { super ( { ... MDCListFoundation . defaultAdapter , ... adapter } ) ; } /**
     * Sets the private wrapFocus_ variable.
     */ setWrapFocus ( value ) { this . wrapFocus_ = value ; } /**
     * Sets the private wrapFocus_ variable.
     */ setMulti ( value ) { this . isMulti_ = value ; const currentIndex = this . selectedIndex_ ; if ( value ) { if ( ! isIndexSet ( currentIndex ) ) { const isUnset = currentIndex === numbers . UNSET_INDEX ; this . selectedIndex_ = isUnset ? new Set ( ) : new Set ( [ currentIndex ] ) ; } } else { if ( isIndexSet ( currentIndex ) ) { if ( currentIndex . size ) { const vals = Array . from ( currentIndex ) . sort ( integerSort ) ; this . selectedIndex_ = vals [ 0 ] ; } else { this . selectedIndex_ = numbers . UNSET_INDEX ; } } } } /**
     * Sets the isVertical_ private variable.
     */ setVerticalOrientation ( value ) { this . isVertical_ = value ; } /**
     * Sets the useActivatedClass_ private variable.
     */ setUseActivatedClass ( useActivated ) { this . useActivatedClass_ = useActivated ; } getSelectedIndex ( ) { return this . selectedIndex_ ; } setSelectedIndex ( index ) { if ( ! this . isIndexValid_ ( index ) ) { return ; } if ( this . isMulti_ ) { this . setMultiSelectionAtIndex_ ( createSetFromIndex ( index ) ) ; } else { this . setSingleSelectionAtIndex_ ( index as number ) ; } } /**
     * Focus in handler for the list items.
     */ handleFocusIn ( _ , listItemIndex ) { if ( listItemIndex >= 0 ) { this . adapter . setTabIndexForElementIndex ( listItemIndex , 0 ) ; } } /**
     * Focus out handler for the list items.
     */ handleFocusOut ( _ , listItemIndex ) { if ( listItemIndex >= 0 ) { this . adapter . setTabIndexForElementIndex ( listItemIndex , - 1 ) ; } /**
         * Between Focusout & Focusin some browsers do not have focus on any
         * element. Setting a delay to wait till the focus is moved to next element.
         */ setTimeout ( ( ) => { if ( ! this . adapter . isFocusInsideList ( ) ) { this . setTabindexToFirstSelectedItem_ ( ) ; } } , 0 ) ; } /**
     * Key handler for the list.
     */ handleKeydown ( event , isRootListItem , listItemIndex ) { const isArrowLeft = normalizeKey ( event ) === 'ArrowLeft' ; const isArrowUp = normalizeKey ( event ) === 'ArrowUp' ; const isArrowRight = normalizeKey ( event ) === 'ArrowRight' ; const isArrowDown = normalizeKey ( event ) === 'ArrowDown' ; const isHome = normalizeKey ( event ) === 'Home' ; const isEnd = normalizeKey ( event ) === 'End' ; const isEnter = normalizeKey ( event ) === 'Enter' ; const isSpace = normalizeKey ( event ) === 'Spacebar' ; if ( this . adapter . isRootFocused ( ) ) { if ( isArrowUp || isEnd ) { event . preventDefault ( ) ; this . focusLastElement ( ) ; } else if ( isArrowDown || isHome ) { event . preventDefault ( ) ; this . focusFirstElement ( ) ; } return ; } let currentIndex = this . adapter . getFocusedElementIndex ( ) ; if ( currentIndex === - 1 ) { currentIndex = listItemIndex ; if ( currentIndex < 0 ) { return ; } } let nextIndex ; if ( ( this . isVertical_ && isArrowDown ) || ( ! this . isVertical_ && isArrowRight ) ) { this . preventDefaultEvent ( event ) ; nextIndex = this . focusNextElement ( currentIndex ) ; } else if ( ( this . isVertical_ && isArrowUp ) || ( ! this . isVertical_ && isArrowLeft ) ) { this . preventDefaultEvent ( event ) ; nextIndex = this . focusPrevElement ( currentIndex ) ; } else if ( isHome ) { this . preventDefaultEvent ( event ) ; nextIndex = this . focusFirstElement ( ) ; } else if ( isEnd ) { this . preventDefaultEvent ( event ) ; nextIndex = this . focusLastElement ( ) ; } else if ( isEnter || isSpace ) { if ( isRootListItem ) { const target = event . target as Element | null ; if ( target && target . tagName === 'A' && isEnter ) { return ; } this . preventDefaultEvent ( event ) ; this . setSelectedIndexOnAction_ ( currentIndex , true ) ; } } this . focusedItemIndex_ = currentIndex ; if ( nextIndex !== undefined ) { this . setTabindexAtIndex_ ( nextIndex ) ; this . focusedItemIndex_ = nextIndex ; } } /**
     * Click handler for the list.
     */ handleSingleSelection ( index , isInteraction , force ? ) { if ( index === numbers . UNSET_INDEX ) { return ; } this . setSelectedIndexOnAction_ ( index , isInteraction , force ) ; this . setTabindexAtIndex_ ( index ) ; this . focusedItemIndex_ = index ; } /**
     * Focuses the next element on the list.
     */ focusNextElement ( index ) { const count = this . adapter . getListItemCount ( ) ; let nextIndex = index + 1 ; if ( nextIndex >= count ) { if ( this . wrapFocus_ ) { nextIndex = 0 ; } else { return index ; } } this . adapter . focusItemAtIndex ( nextIndex ) ; return nextIndex ; } /**
     * Focuses the previous element on the list.
     */ focusPrevElement ( index ) { let prevIndex = index - 1 ; if ( prevIndex < 0 ) { if ( this . wrapFocus_ ) { prevIndex = this . adapter . getListItemCount ( ) - 1 ; } else { return index ; } } this . adapter . focusItemAtIndex ( prevIndex ) ; return prevIndex ; } focusFirstElement ( ) { this . adapter . focusItemAtIndex ( 0 ) ; return 0 ; } focusLastElement ( ) { const lastIndex = this . adapter . getListItemCount ( ) - 1 ; this . adapter . focusItemAtIndex ( lastIndex ) ; return lastIndex ; } param itemIndex param isEnabled setEnabled ( itemIndex , isEnabled ) { if ( ! this . isIndexValid_ ( itemIndex ) ) { return ; } this . adapter . setDisabledStateForElementIndex ( itemIndex , ! isEnabled ) ; } /**
     * Ensures that preventDefault is only called if the containing element
     * doesn't consume the event, and it will cause an unintended scroll.
     */ private preventDefaultEvent ( evt ) { const target = evt . target as Element ; const tagName = `${ target . tagName }` . toLowerCase ( ) ; if ( ELEMENTS_KEY_ALLOWED_IN . indexOf ( tagName ) === - 1 ) { evt . preventDefault ( ) ; } } private setSingleSelectionAtIndex_ ( index , isInteraction = true ) { if ( this . selectedIndex_ === index ) { return ; } if ( this . selectedIndex_ !== numbers . UNSET_INDEX ) { this . adapter . setSelectedStateForElementIndex ( this . selectedIndex_ as number , false ) ; if ( this . useActivatedClass_ ) { this . adapter . setActivatedStateForElementIndex ( this . selectedIndex_ as number , false ) ; } } if ( isInteraction ) { this . adapter . setSelectedStateForElementIndex ( index , true ) ; } if ( this . useActivatedClass_ ) { this . adapter . setActivatedStateForElementIndex ( index , true ) ; } this . setAriaForSingleSelectionAtIndex_ ( index ) ; this . selectedIndex_ = index ; this . adapter . notifySelected ( index ) ; } private setMultiSelectionAtIndex_ ( newIndex , isInteraction = true ) { const oldIndex = createSetFromIndex ( this . selectedIndex_ ) ; const diff = findIndexDiff ( oldIndex , newIndex ) ; if ( ! diff . removed . length && ! diff . added . length ) { return ; } for ( const removed of diff . removed ) { if ( isInteraction ) { this . adapter . setSelectedStateForElementIndex ( removed , false ) ; } if ( this . useActivatedClass_ ) { this . adapter . setActivatedStateForElementIndex ( removed , false ) ; } } for ( const added of diff . added ) { if ( isInteraction ) { this . adapter . setSelectedStateForElementIndex ( added , true ) ; } if ( this . useActivatedClass_ ) { this . adapter . setActivatedStateForElementIndex ( added , true ) ; } } this . selectedIndex_ = newIndex ; this . adapter . notifySelected ( newIndex , diff ) ; } /**
     * Sets aria attribute for single selection at given index.
     */ private setAriaForSingleSelectionAtIndex_ ( index ) { if ( this . selectedIndex_ === numbers . UNSET_INDEX ) { this . ariaCurrentAttrValue_ = this . adapter . getAttributeForElementIndex ( index , strings . ARIA_CURRENT ) ; } const isAriaCurrent = this . ariaCurrentAttrValue_ !== null ; const ariaAttribute = isAriaCurrent ? strings . ARIA_CURRENT : strings . ARIA_SELECTED ; if ( this . selectedIndex_ !== numbers . UNSET_INDEX ) { this . adapter . setAttributeForElementIndex ( this . selectedIndex_ as number , ariaAttribute , 'false' ) ; } const ariaAttributeValue = isAriaCurrent ? this . ariaCurrentAttrValue_ : 'true' ; this . adapter . setAttributeForElementIndex ( index , ariaAttribute , ariaAttributeValue as string ) ; } private setTabindexAtIndex_ ( index ) { if ( this . focusedItemIndex_ === numbers . UNSET_INDEX && index !== 0 ) { this . adapter . setTabIndexForElementIndex ( 0 , - 1 ) ; } else if ( this . focusedItemIndex_ >= 0 && this . focusedItemIndex_ !== index ) { this . adapter . setTabIndexForElementIndex ( this . focusedItemIndex_ , - 1 ) ; } this . adapter . setTabIndexForElementIndex ( index , 0 ) ; } private setTabindexToFirstSelectedItem_ ( ) { let targetIndex = 0 ; if ( typeof this . selectedIndex_ === 'number' && this . selectedIndex_ !== numbers . UNSET_INDEX ) { targetIndex = this . selectedIndex_ ; } else if ( isIndexSet ( this . selectedIndex_ ) && this . selectedIndex_ . size > 0 ) { targetIndex = Math . min ( ... this . selectedIndex_ ) ; } this . setTabindexAtIndex_ ( targetIndex ) ; } private isIndexValid_ ( index ) { if ( index instanceof Set ) { if ( ! this . isMulti_ ) { throw new Error ( 'MDCListFoundation: Array of index is only supported for checkbox based list' ) ; } if ( index . size === 0 ) { return true ; } else { let isOneInRange = false ; for ( const entry of index ) { isOneInRange = this . isIndexInRange_ ( entry ) ; if ( isOneInRange ) { break ; } } return isOneInRange ; } } else if ( typeof index === 'number' ) { if ( this . isMulti_ ) { throw new Error ( 'MDCListFoundation: Expected array of index for checkbox based list but got number: ' + index ) ; } return index === numbers . UNSET_INDEX || this . isIndexInRange_ ( index ) ; } else { return false ; } } private isIndexInRange_ ( index ) { const listSize = this . adapter . getListItemCount ( ) ; return index >= 0 && index < listSize ; } /**
     * Sets selected index on user action, toggles checkbox / radio based on
     * toggleCheckbox value. User interaction should not toggle list item(s) when
     * disabled.
     */ private setSelectedIndexOnAction_ ( index , isInteraction , force ? ) { if ( this . adapter . getDisabledStateForElementIndex ( index ) ) { return ; } let checkedIndex = index ; if ( this . isMulti_ ) { checkedIndex = new Set ( [ index ] ) ; } if ( ! this . isIndexValid_ ( checkedIndex ) ) { return ; } if ( this . isMulti_ ) { this . toggleMultiAtIndex ( index , force , isInteraction ) ; } else { if ( isInteraction || force ) { this . setSingleSelectionAtIndex_ ( index , isInteraction ) ; } else { const isDeselection = this . selectedIndex_ === index ; if ( isDeselection ) { this . setSingleSelectionAtIndex_ ( numbers . UNSET_INDEX ) ; } } } if ( isInteraction ) { this . adapter . notifyAction ( index ) ; } } toggleMultiAtIndex ( index , force ? , isInteraction = true ) { let newSelectionValue = false ; if ( force === undefined ) { newSelectionValue = ! this . adapter . getSelectedStateForElementIndex ( index ) ; } else { newSelectionValue = force ; } const newSet = createSetFromIndex ( this . selectedIndex_ ) ; if ( newSelectionValue ) { newSet . add ( index ) ; } else { newSet . delete ( index ) ; } this . setMultiSelectionAtIndex_ ( newSet , isInteraction ) ; } } export default MDCListFoundation ;