import React , { useState , useEffect , useLayoutEffect , useCallback } from 'react' ; import { CSSObject } from '@emotion/core' ; import { EmotionCanvasTheme , ErrorType , Themeable , styled } from '@workday/canvas-kit-react/common' ; import { Placement , Popper , useCloseOnEscape , useCloseOnOutsideClick , usePopupModel , useReturnFocus , useTransferOnFullscreenExit , } from '@workday/canvas-kit-react/popup' ; import { colors , borderRadius , inputColors } from '@workday/canvas-kit-react/tokens' ; import { SelectProps } from './Select' ; import { buttonBorderWidth } from './SelectBase' ; import { MenuPlacement , MenuVisibility } from './types' ; interface SelectMenuProps extends Themeable , React . HTMLAttributes < HTMLUListElement > , Pick < SelectProps , 'error' > { /**
     * The ref to the anchor button element. Required to anchor the menu.
     */ buttonRef ? : React . RefObject < HTMLButtonElement > ; /**
     * The ref to the underlying menu/listbox element. Use this to imperatively manipulate the menu.
     */ menuRef ? : React . RefObject < HTMLUListElement > ; /**
     * The function called when the menu is closed.
     */ onClose ? : ( ) => void ; default placement ? : MenuPlacement ; default shouldAutoFlip ? : boolean ; default shouldAutoFocus ? : boolean ; default visibility ? : MenuVisibility ; } export const menuAnimationDuration = 200 ; const menuBorderStyles = ( theme , error ? ) : CSSObject => { let borderColor = theme . canvas . palette . common . focusOutline ; let dividerBorderColor = borderColor ; let dividerBorderWidth = 1 ; if ( error === ErrorType . Error ) { borderColor = theme . canvas . palette . error . main ; dividerBorderColor = borderColor ; } else if ( error === ErrorType . Alert ) { borderColor = theme . canvas . palette . alert . darkest ; dividerBorderColor = theme . canvas . palette . alert . main ; dividerBorderWidth = 2 ; } const dividerBorder = `${ dividerBorderWidth }px solid ${ dividerBorderColor }` ; return { borderColor : borderColor , '&:before' : { backgroundColor : colors . soap400 , borderLeft : dividerBorder , borderRight : dividerBorder , boxSizing : 'border-box' , content : '""' , display : 'block' , height : 1 , position : 'absolute' , width : '100%' , '[data-popper-placement="bottom"] &' : { top : 0 , } , '[data-popper-placement="top"] &' : { bottom : 0 , } , } , } ; } ; const menuListBorderStyles = ( theme , error ? ) : CSSObject => { let borderColor = theme . canvas . palette . common . focusOutline ; let borderWidth = 1 ; if ( error === ErrorType . Error ) { borderColor = theme . canvas . palette . error . main ; } else if ( error === ErrorType . Alert ) { borderColor = theme . canvas . palette . alert . main ; borderWidth = 2 ; } const border = `${ borderWidth }px solid ${ borderColor }` ; return { borderLeft : border , borderRight : border , '[data-popper-placement="bottom"] &' : { borderBottom : border , } , '[data-popper-placement="top"] &' : { borderTop : border , } , } ; } ; const Menu = styled ( 'div' ) < Pick < SelectMenuProps , 'error' | 'theme' | 'visibility' > & { width : number ; } > ( { backgroundColor : colors . frenchVanilla100 , border : `1px solid ${ inputColors . border }` , boxSizing : 'border-box' , position : 'relative' , transition : `opacity ${ menuAnimationDuration }ms` , '[data-popper-placement="bottom"] &' : { borderRadius : `0 0 ${ borderRadius . m } ${ borderRadius . m }` , borderTop : 0 , } , '[data-popper-placement="top"] &' : { borderRadius : `${ borderRadius . m } ${ borderRadius . m } 0 0` , borderBottom : 0 , } , } , ( { error , theme } ) => ( { ... menuBorderStyles ( theme , error ) , } ) , ( { visibility } ) => ( { opacity : visibility === 'opening' || visibility === 'opened' || visibility === 'close' ? 1 : 0 , } ) , ( { width } ) => ( { width : width , } ) ) ; const MenuList = styled ( 'ul' ) < Pick < SelectProps , 'error' | 'theme' > > ( { listStyle : 'none' , margin : 0 , maxHeight : 200 , outline : 'none' , overflowY : 'auto' , padding : 0 , } , ( { error , theme } ) => ( { ... menuListBorderStyles ( theme , error ) , } ) ) ; const generatePopperOptions = ( props ) => { const { menuRef , placement , shouldAutoFlip , shouldAutoFocus } = props ; let fallbackPlacements = [ ] ; if ( shouldAutoFlip ) { fallbackPlacements = placement === 'top' ? [ 'bottom' ] : [ 'top' ] ; } const modifiers = [ { name : 'flip' , options : { fallbackPlacements , } , } , { name : 'offset' , options : { offset : ( ) => { const skidding = 0 ; const distance = - parseInt ( borderRadius . m , 10 ) ; return [ skidding , distance ] ; } , } , } , { name : 'preventOverflow' , options : { mainAxis : false , } , } , ] ; return { modifiers , onFirstUpdate : ( ) => { if ( shouldAutoFocus && menuRef && menuRef . current ) { menuRef . current . focus ( ) ; } } , } ; } ; const SelectMenu = ( { buttonRef , children , error , menuRef , onClose , placement = 'bottom' , shouldAutoFlip = true , shouldAutoFocus = true , visibility = 'closed' , ... elemProps } ) => { const model = usePopupModel ( { initialVisibility : 'visible' , returnFocusRef : buttonRef , onHide : onClose , } ) ; const [ width , setWidth ] = useState ( 0 ) ; const handleWidthChange = useCallback ( ( ) => { if ( buttonRef && buttonRef . current && visibility !== 'closed' ) { const newMenuWidth = buttonRef . current . clientWidth + 2 * buttonBorderWidth ; setWidth ( newMenuWidth ) ; } } , [ buttonRef , visibility ] ) ; useLayoutEffect ( ( ) => { handleWidthChange ( ) ; } , [ handleWidthChange ] ) ; useEffect ( ( ) => { window . addEventListener ( 'resize' , handleWidthChange ) ; return ( ) => { window . removeEventListener ( 'resize' , handleWidthChange ) ; } ; } , [ handleWidthChange ] ) ; useCloseOnEscape ( model ) ; useCloseOnOutsideClick ( model ) ; useReturnFocus ( model ) ; useTransferOnFullscreenExit ( model ) ; return ( < Popper placement = { placement } anchorElement = { buttonRef } popperOptions = { generatePopperOptions ( { menuRef , placement , shouldAutoFlip , shouldAutoFocus , } ) } ref = { model . state . stackRef } > < Menu error = { error } visibility = { visibility } width = { width } > < MenuList error = { error } ref = { menuRef } role = "listbox" tabIndex = { - 1 } { ... elemProps } > { children } < /MenuList> < /Menu> < /Popper>); } ; export default SelectMenu ;