import React from 'react' ; import classnames from 'classnames' ; import { MDCListFoundation } from '@material/list/foundation' ; import { MDCListIndex } from '@material/list/types' ; import { MDCListAdapter } from '@material/list/adapter' ; import memoizeOne from 'memoize-one/dist/memoize-one.cjs.js' ; import ListItem , { ListItemProps } from './ListItem' ; import ListItemGraphic from './ListItemGraphic' ; import ListItemText from './ListItemText' ; import ListItemMeta from './ListItemMeta' ; import ListDivider from './ListDivider' ; import ListGroup from './ListGroup' ; import ListGroupSubheader from './ListGroupSubheader' ; const HORIZONTAL = 'horizontal' ; export interface ListProps extends React . HTMLProps < HTMLElement > { className ? : string ; checkboxList ? : boolean ; radioList ? : boolean ; nonInteractive ? : boolean ; dense ? : boolean ; avatarList ? : boolean ; twoLine ? : boolean ; singleSelection ? : boolean ; selectedIndex ? : MDCListIndex ; handleSelect ? : ( activatedItemIndex : number , selected : MDCListIndex ) => void ; wrapFocus ? : boolean ; tag ? : string ; ref ? : React . Ref < any > ; orientation ? : 'vertical' | 'horizontal' ; } interface ListState { listItemClassNames : { [ listItemIndex ] : string [ ] ; } ; } export interface ListItemContextShape { checkboxList ? : boolean ; radioList ? : boolean ; handleClick ? : ( e : React . MouseEvent < any > , index : number ) => void ; handleKeyDown ? : ( e : React . KeyboardEvent < any > , index : number ) => void ; handleBlur ? : ( e : React . FocusEvent < any > , index : number ) => void ; handleFocus ? : ( e : React . FocusEvent < any > , index : number ) => void ; onDestroy ? : ( index : number ) => void ; getListItemInitialTabIndex ? : ( index : number ) => number ; getClassNamesFromList ? : ( ) => ListState [ 'listItemClassNames' ] ; tabIndex ? : number ; } function isSelectedIndexType ( selectedIndex ) { return ( ( typeof selectedIndex === 'number' && ! isNaN ( selectedIndex ) ) || Array . isArray ( selectedIndex ) ) ; } export const defaultListItemContext = { handleClick : ( ) => { } , handleKeyDown : ( ) => { } , handleBlur : ( ) => { } , handleFocus : ( ) => { } , onDestroy : ( ) => { } , getListItemInitialTabIndex : ( ) => - 1 , getClassNamesFromList : ( ) => ( { } ) , } ; export const ListItemContext = React . createContext ( defaultListItemContext ) ; export default class List extends React . Component < ListProps , ListState > { foundation ! : MDCListFoundation ; hasInitializedListItemTabIndex = false ; private listElement = React . createRef < HTMLElement > ( ) ; state : ListState = { listItemClassNames : { } , } ; static defaultProps : Partial < ListProps > = { className : '' , checkboxList : false , radioList : false , nonInteractive : false , dense : false , avatarList : false , twoLine : false , singleSelection : false , selectedIndex : - 1 , handleSelect : ( ) => { } , wrapFocus : true , tag : 'ul' , } ; componentDidMount ( ) { const { singleSelection , wrapFocus , selectedIndex } = this . props ; this . foundation = new MDCListFoundation ( this . adapter ) ; this . foundation . init ( ) ; this . foundation . setSingleSelection ( singleSelection ! ) ; this . foundation . layout ( ) ; if ( isSelectedIndexType ( selectedIndex ) ) { this . foundation . setSelectedIndex ( selectedIndex ) ; } this . foundation . setWrapFocus ( wrapFocus ! ) ; this . foundation . setVerticalOrientation ( this . props . orientation !== HORIZONTAL ) ; this . initializeListType ( ) ; } componentDidUpdate ( prevProps ) { const { singleSelection , wrapFocus , selectedIndex } = this . props ; const hasSelectedIndexUpdated = selectedIndex !== prevProps . selectedIndex ; if ( singleSelection !== prevProps . singleSelection ) { this . foundation . setSingleSelection ( singleSelection ! ) ; } if ( hasSelectedIndexUpdated && isSelectedIndexType ( selectedIndex ) ) { this . foundation . setSelectedIndex ( selectedIndex ) ; } if ( wrapFocus !== prevProps . wrapFocus ) { this . foundation . setWrapFocus ( wrapFocus ! ) ; } if ( this . props . orientation !== prevProps . orientation ) { this . foundation . setVerticalOrientation ( this . props . orientation !== HORIZONTAL ) ; } } componentWillUnmount ( ) { this . foundation . destroy ( ) ; } initializeListType = ( ) => { const { singleSelection } = this . props ; const { cssClasses , strings } = MDCListFoundation ; if ( ! this . listElement . current ) return ; const checkboxListItems = this . listElement . current . querySelectorAll ( strings . ARIA_ROLE_CHECKBOX_SELECTOR ) ; const radioSelectedListItem = this . listElement . current . querySelector ( strings . ARIA_CHECKED_RADIO_SELECTOR ) ; if ( checkboxListItems . length ) { const preselectedItems = this . listElement . current . querySelectorAll ( strings . ARIA_CHECKED_CHECKBOX_SELECTOR ) ; const selectedIndex = [ ] . map . call ( preselectedItems , ( listItem ) => this . listElements . indexOf ( listItem ) ) as number [ ] ; this . foundation . setSelectedIndex ( selectedIndex ) ; } else if ( singleSelection ) { const isActivated = this . listElement . current . querySelector ( cssClasses . LIST_ITEM_ACTIVATED_CLASS ) ; if ( isActivated ) { this . foundation . setUseActivatedClass ( true ) ; } } else if ( radioSelectedListItem ) { this . foundation . setSelectedIndex ( this . listElements . indexOf ( radioSelectedListItem ) ) ; } } ; get listElements ( ) : Element [ ] { if ( this . listElement . current ) { return [ ] . slice . call ( this . listElement . current . querySelectorAll ( MDCListFoundation . strings . ENABLED_ITEMS_SELECTOR ) ) ; } return [ ] ; } get classes ( ) { const { className , nonInteractive , dense , avatarList , twoLine } = this . props ; return classnames ( 'mdc-list' , className , { 'mdc-list--non-interactive' : nonInteractive , 'mdc-list--dense' : dense , 'mdc-list--avatar-list' : avatarList , 'mdc-list--two-line' : twoLine , } ) ; } get adapter ( ) : MDCListAdapter { return { getListItemCount : ( ) => this . listElements . length , getFocusedElementIndex : ( ) => this . listElements . indexOf ( document . activeElement as HTMLLIElement ) , getAttributeForElementIndex : ( index , attr ) => { const listItem = this . listElements [ index ] ; return listItem . getAttribute ( attr ) ; } , setAttributeForElementIndex : ( index , attr , value ) => { const listItem = this . listElements [ index ] ; if ( listItem ) { listItem . setAttribute ( attr , value ) ; } } , /**
             * Pushes class name to state.listItemClassNames[listItemIndex] if it doesn't yet exist.
             */ addClassForElementIndex : ( index , className ) => { const { listItemClassNames } = this . state ; if ( listItemClassNames [ index ] && listItemClassNames [ index ] . indexOf ( className ) === - 1 ) { listItemClassNames [ index ] . push ( className ) ; } else { listItemClassNames [ index ] = [ className ] ; } this . setState ( { listItemClassNames } ) ; } , /**
             * Finds the className within state.listItemClassNames[listItemIndex], and removes it
             * from the array.
             */ removeClassForElementIndex : ( index , className ) => { const { listItemClassNames } = this . state ; if ( listItemClassNames [ index ] ) { const removalIndex = listItemClassNames [ index ] . indexOf ( className ) ; if ( removalIndex !== - 1 ) { listItemClassNames [ index ] . splice ( removalIndex , 1 ) ; this . setState ( { listItemClassNames } ) ; } } } , setTabIndexForListItemChildren : ( listItemIndex , tabIndexValue ) => { const listItem = this . listElements [ listItemIndex ] ; const selector = MDCListFoundation . strings . CHILD_ELEMENTS_TO_TOGGLE_TABINDEX ; const listItemChildren = [ ] . slice . call ( listItem . querySelectorAll ( selector ) ) ; listItemChildren . forEach ( ( el ) => el . setAttribute ( 'tabindex' , tabIndexValue ) ) ; } , focusItemAtIndex : ( index ) => { const element = this . listElements [ index ] as HTMLElement | undefined ; if ( element ) { element . focus ( ) ; } } , setCheckedCheckboxOrRadioAtIndex : ( ) => { } , hasCheckboxAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; return ! ! listItem . querySelector ( MDCListFoundation . strings . CHECKBOX_SELECTOR ) ; } , hasRadioAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; return ! ! listItem . querySelector ( MDCListFoundation . strings . RADIO_SELECTOR ) ; } , isCheckboxCheckedAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; const selector = MDCListFoundation . strings . CHECKBOX_SELECTOR ; const toggleEl = listItem . querySelector < HTMLInputElement > ( selector ) ; return toggleEl ! . checked ; } , isFocusInsideList : ( ) => { if ( ! this . listElement . current ) return false ; return this . listElement . current . contains ( document . activeElement ) ; } , notifyAction : ( index ) => { this . props . handleSelect ! ( index , this . foundation . getSelectedIndex ( ) ) ; } , } ; } get role ( ) { const { checkboxList , radioList , role } = this . props ; if ( role ) return role ; if ( checkboxList ) { return 'group' ; } else if ( radioList ) { return 'radiogroup' ; } return null ; } /**
     * Called from ListItem.
     * Initializes the tabIndex prop for the listItems. tabIndex is determined by:
     * 1. if selectedIndex is an array, and the index === selectedIndex[0]
     * 2. if selectedIndex is a number, and the the index === selectedIndex
     * 3. if there is no selectedIndex
     */ getListItemInitialTabIndex = ( index ) => { const { selectedIndex } = this . props ; let tabIndex = - 1 ; if ( ! this . hasInitializedListItemTabIndex ) { const isSelectedIndexArray = Array . isArray ( selectedIndex ) && selectedIndex . length > 0 && index === selectedIndex [ 0 ] ; const isSelected = selectedIndex === index ; if ( isSelectedIndexArray || isSelected || selectedIndex === - 1 ) { tabIndex = 0 ; this . hasInitializedListItemTabIndex = true ; } } return tabIndex ; } ; /**
     * Method checks if the list item at `index` contains classes. If true,
     * method merges state.listItemClassNames[index] with listItem.props.className.
     * The return value is used as the listItem's className.
     */ private getListItemClassNames = ( ) => { const { listItemClassNames } = this . state ; return listItemClassNames ; } ; handleKeyDown = ( e , index ) => { e . persist ( ) ; this . foundation . handleKeydown ( e . nativeEvent , true , index ) ; } ; handleClick = ( _e , index ) => { this . foundation . handleClick ( index , false ) ; } ; handleFocus = ( e , index ) => { this . foundation . handleFocusIn ( e . nativeEvent , index ) ; } ; handleBlur = ( e , index ) => { this . foundation . handleFocusOut ( e . nativeEvent , index ) ; } ; onDestroy = ( index ) => { const { listItemClassNames } = this . state ; delete listItemClassNames [ index ] ; this . setState ( { listItemClassNames } ) ; } ; private getListProps = ( checkboxList ? , radioList ? ) => ( { checkboxList : Boolean ( checkboxList ) , radioList : Boolean ( radioList ) , handleKeyDown : this . handleKeyDown , handleClick : this . handleClick , handleFocus : this . handleFocus , handleBlur : this . handleBlur , onDestroy : this . onDestroy , getClassNamesFromList : this . getListItemClassNames , getListItemInitialTabIndex : this . getListItemInitialTabIndex , } ) ; getListPropsMemoized = memoizeOne ( this . getListProps ) ; render ( ) { const { className , checkboxList , radioList , nonInteractive , dense , avatarList , twoLine , singleSelection , role , selectedIndex , handleSelect , wrapFocus , children , tag : Tag , orientation , ... otherProps } = this . props ; return ( < Tag className = { this . classes } ref = { this . listElement } role = { this . role } aria - orientation = { orientation === HORIZONTAL && ! role && this . role ? HORIZONTAL : undefined } { ... otherProps } > < ListItemContext . Provider value = { this . getListPropsMemoized ( checkboxList , radioList ) } > { children } < /ListItemContext.Provider> < /Tag>); } } export { ListItem , ListItemGraphic , ListItemText , ListItemMeta , ListDivider , ListGroup , ListGroupSubheader , ListItemProps , } ;