import { State } from '@popperjs/core' ; import { UseSelectPropGetters } from 'downshift' ; import React , { forwardRef , Fragment , HTMLAttributes , memo , Ref , useCallback , useMemo , useRef } from 'react' ; import { useIsomorphicLayoutEffect , useWindowSize } from '../../hooks' ; import { typedMemo } from '../../utils' ; import { Box } from '../Box' ; import { DropdownItem , DropdownItemGroup , DropdownLinkItem , DropdownProps } from '../Dropdown' ; import { SelectAction , SelectOption , SelectOptionGroup , SelectProps } from '../Select' ; import { ListGroupHeader } from './GroupHeader' ; import { ListGroupSeparator } from './GroupSeparator' ; import { ListItem } from './Item' ; import { StyledList } from './styled' ; export interface ListProps < T > extends HTMLAttributes < HTMLUListElement > { action ? : SelectAction ; autoWidth : boolean ; filteredItems ? : Array < SelectOption < T > | SelectAction > ; highlightedIndex : number ; isOpen : boolean ; items : DropdownProps [ 'items' ] | SelectProps < T > [ 'options' ] ; isDropdown ? : boolean ; maxHeight ? : number ; selectedItem ? : SelectOption < T > | null ; selectedItems ? : Array < SelectOption < T > > | null ; addItem ? ( item ) : void ; getItemProps : UseSelectPropGetters < any > [ 'getItemProps' ] ; getMenuProps : UseSelectPropGetters < any > [ 'getMenuProps' ] ; update : ( ( ) => Promise < Partial < State > > ) | null ; removeItem ? ( item ) : void ; } interface PrivateProps { forwardedRef : Ref < HTMLUListElement > ; } const StyleableList = typedMemo ( < T extends unknown > ( { action , addItem , autoWidth , filteredItems , forwardedRef , getItemProps , getMenuProps , highlightedIndex , isDropdown = false , isOpen , items , maxHeight = 250 , selectedItem , selectedItems , update , removeItem , ... props } ) : ReturnType < React . FC < ListProps < T > & PrivateProps > > => { const itemKey = useRef ( 0 ) ; const { height , width } = useWindowSize ( ) ; useIsomorphicLayoutEffect ( ( ) => { async function scheduleUpdate ( ) { if ( update && isOpen ) { await update ( ) ; } } scheduleUpdate ( ) ; } , [ isOpen , height , width , selectedItems ?. length ] ) ; const renderAction = useCallback ( ( action ) => { const key = itemKey . current ; return ( < Box borderTop = "box" marginTop = "xSmall" paddingTop = "xSmall" > < ListItem actionType = { action . actionType } autoWidth = { autoWidth } getItemProps = { getItemProps } index = { key } isAction = { true } isHighlighted = { highlightedIndex === key } isSelected = { false } item = { action } key = "action" / > < /Box>); } , [ getItemProps , autoWidth , highlightedIndex ] ) ; const renderItems = useCallback ( ( listItems ) => { return ( Array . isArray ( listItems ) && listItems . map ( ( item ) => { if ( filteredItems && isOption ( item ) && ! filteredItems . find ( ( filteredItem ) => isOption ( filteredItem ) && filteredItem . value === item . value ) ) { return null ; } const key = itemKey . current ; itemKey . current += 1 ; const isChecked = isOption ( item ) && selectedItems && Boolean ( selectedItems . find ( ( selected ) => selected . value === item . value ) ) ; const hasActionType = ( item ) : item is DropdownItem | DropdownLinkItem => 'actionType' in item ; return ( < ListItem addItem = { addItem } autoWidth = { autoWidth } actionType = { hasActionType ( item ) ? item . actionType : 'normal' } getItemProps = { getItemProps } index = { key } isAction = { isDropdown } isChecked = { isChecked || false } isHighlighted = { highlightedIndex === key } isSelected = { ! isDropdown && isOption ( item ) && selectedItem ?. value === item . value } item = { item } key = { `${ key }-${ item . content }` } removeItem = { removeItem } / > ) ; } ) ) ; } , [ addItem , autoWidth , filteredItems , getItemProps , highlightedIndex , isDropdown , removeItem , selectedItem , selectedItems , ] ) ; const renderGroup = useCallback ( ( group ) => { return ( < > { group . separated && < ListGroupSeparator />} { group . label && < ListGroupHeader > { group . label } < /ListGroupHeader>} { isItemGroup ( group ) && renderItems ( group . items ) } { isOptionGroup ( group ) && renderItems ( group . options ) } < />); } , [ renderItems ] ) ; const renderChildren = useMemo ( ( ) => { itemKey . current = 0 ; const groupFragment = ( items ) => items . map ( ( group , index ) => < Fragment key = { index } > { renderGroup ( group ) } < /Fragment>); if ( Array . isArray ( items ) && isGroups ( items ) ) { return ( < > { groupFragment ( items ) } { action && renderAction ( action ) } < />); } if ( Array . isArray ( items ) && isItems ( items ) ) { return ( < > { renderItems ( items ) } { action && renderAction ( action ) } < />); } } , [ action , items , renderAction , renderGroup , renderItems ] ) ; return ( < StyledList { ... getMenuProps ( { ... props , onKeyDown : ( event ) => { if ( event . key === 'Enter' ) { const element = event . currentTarget . children [ highlightedIndex ] ; const link = element . querySelector ( 'a' ) ; if ( link && ! link . getAttribute ( 'disabled' ) ) { link . click ( ) ; } } } , ref : forwardedRef } ) } maxHeight = { maxHeight } > { isOpen && renderChildren } < /StyledList>); } ) ; export const List = memo ( forwardRef < HTMLUListElement , ListProps < unknown > > ( ( props , ref ) => < StyleableList { ... props } forwardedRef = { ref } / > ) ) ; type Items = DropdownItem | DropdownLinkItem | SelectOption < unknown > | SelectAction | DropdownItemGroup | SelectOptionGroup < unknown > ; const isGroups = ( items ) : items is Array < DropdownItemGroup > | Array < SelectOptionGroup < unknown > > => items . every ( ( item ) => isItemGroup ( item ) || isOptionGroup ( item ) ) ; const isItems = ( items ) : items is Array < DropdownItem | DropdownLinkItem > | Array < SelectOption < unknown > > => items . every ( ( item ) => isItem ( item ) || isOption ( item ) ) ; const isOption = ( item ) : item is SelectOption < unknown > => 'value' in item ; const isItem = ( item ) : item is DropdownItem | DropdownLinkItem => 'content' in item && ! ( 'items' in item ) ; const isItemGroup = ( item ) : item is DropdownItemGroup => 'items' in item && ! ( 'content' in item ) ; const isOptionGroup = ( item ) : item is SelectOptionGroup < unknown > => 'options' in item && ! ( 'value' in item ) ;