import React , { useLayoutEffect } from 'react' ; import { GrowthBehavior , ErrorType , StyledType , Themeable , errorRing , styled , useUniqueId , } from '@workday/canvas-kit-react/common' ; import { colors , borderRadius , inputColors , spaceNumbers , type , space , } from '@workday/canvas-kit-react/tokens' ; import { caretDownSmallIcon } from '@workday/canvas-system-icons-web' ; import { SystemIcon } from '@workday/canvas-kit-react/icon' ; import SelectMenu from './SelectMenu' ; import SelectOption from './SelectOption' ; import { scrollIntoViewIfNeeded } from './scrolling' ; import { MenuPlacement , MenuVisibility } from './types' ; import { getCorrectedIndexByValue } from './utils' ; interface OptionData { [ key ] : any ; } export interface Option { data ? : OptionData ; disabled ? : boolean ; id ? : string ; label ? : string ; value : string ; } export interface NormalizedOption extends Option { data : OptionData ; disabled : boolean ; id : string ; label : string ; } export interface RenderableOption extends NormalizedOption { focused : boolean ; selected : boolean ; } export interface RenderOptionFunction { ( option ) : React . ReactNode ; } export interface CoreSelectBaseProps extends Themeable , GrowthBehavior , Pick < React . InputHTMLAttributes < HTMLInputElement > , 'onChange' > , Pick < React . SelectHTMLAttributes < HTMLSelectElement > , 'required' > , Omit < React . ButtonHTMLAttributes < HTMLButtonElement > , 'onChange' > { /**
     * The type of error associated with the Select (if applicable).
     */ error ? : ErrorType ; default renderOption ? : RenderOptionFunction ; /**
     * The value of the Select.
     */ value ? : string ; } export interface SelectBaseProps extends CoreSelectBaseProps , StyledType { /**
     * The ref to be forwarded to the underlying button element. Use this to imperatively manipulate the button.
     */ forwardedButtonRef ? : React . Ref < HTMLButtonElement > ; /**
     * The local ref to the underlying button element. Can be used in situations where RefObject is required (such as the Popper Menu).
     */ localButtonRef ? : React . RefObject < HTMLButtonElement > ; default focusedOptionIndex ? : number ; /**
     * The ref to the underlying (hidden) text input element. Use this to imperatively manipulate the input.
     */ inputRef ? : React . Ref < HTMLInputElement > ; default menuPlacement ? : MenuPlacement ; /**
     * The ref to the underlying menu element. Use this to imperatively manipulate the menu.
     */ menuRef ? : React . RefObject < HTMLUListElement > ; default menuVisibility ? : MenuVisibility ; /**
     * The function called when a key is pressed down while the SelectBase button or menu has focus.
     */ onKeyDown ? : ( event : React . KeyboardEvent < HTMLElement > ) => void ; /**
     * The function called when the menu is closed.
     */ onClose ? : ( ) => void ; /**
     * The function called when an option in the SelectBase is selected via a click or a keyboard shortcut. The `index` passed to the callback function represents the index of the option which was selected.
     */ onOptionSelection ? : ( index : number ) => void ; /**
     * The options of the SelectBase. `options` is an array of objects, each object adhering to the `NormalizedOption` interface:
     *
     * * `value: string` (required, analagous to the `value` attribute of an `<option>`)
     * * `disabled: boolean` (required)
     * * `id: string` (required)
     * * `label: string` (required, analagous to the text content of an `<option>`)
     */ options : NormalizedOption [ ] ; default shouldMenuAutoFlip ? : boolean ; default shouldMenuAutoFocus ? : boolean ; } export const buttonBorderWidth = 1 ; export const buttonDefaultWidth = 280 ; const menuIconSize = 24 ; const buttonPadding = spaceNumbers . xxs - buttonBorderWidth ; const SelectButton = styled ( 'button' ) < Pick < SelectBaseProps , 'error' | 'grow' | 'menuVisibility' | 'theme' > & StyledType > ( { ... type . levels . subtext . large , border : `${ buttonBorderWidth }px solid ${ inputColors . border }` , cursor : 'default' , display : 'block' , backgroundColor : inputColors . background , borderRadius : borderRadius . m , boxSizing : 'border-box' , height : space . xl , outline : 'none' , overflow : 'hidden' , padding : buttonPadding , paddingRight : spaceNumbers . xxs + menuIconSize + buttonPadding , textAlign : 'left' , textOverflow : 'ellipsis' , transition : '0.2s box-shadow, 0.2s border-color' , whiteSpace : 'nowrap' , width : buttonDefaultWidth , '&::placeholder' : { color : inputColors . placeholder , } , '&:disabled' : { backgroundColor : inputColors . disabled . background , borderColor : inputColors . disabled . border , color : inputColors . disabled . text , '&::placeholder' : { color : inputColors . disabled . text , } , } , } , ( { error , menuVisibility , theme } ) => { const themedFocusOutlineColor = theme . canvas . palette . common . focusOutline ; const buttonFocusStyles = { borderColor : themedFocusOutlineColor , boxShadow : `inset 0 0 0 1px ${ themedFocusOutlineColor }` , } ; if ( error === undefined ) { return menuVisibility === 'closed' || menuVisibility === 'closing' ? { '&:focus:not([disabled])' : { ... buttonFocusStyles , } , '&:hover:not([disabled]):not(:focus)' : { borderColor : inputColors . hoverBorder , } , } : { ... buttonFocusStyles } ; } return { ... errorRing ( error , theme ) , } ; } , ( { grow } ) => grow && { width : '100%' , } ) ; const SelectMenuIcon = styled ( SystemIcon ) ( { position : 'absolute' , top : space . xxxs , right : space . xxxs , padding : space . xxxs , pointerEvents : 'none' , '& path' : { transition : '100ms fill' , } , } ) ; const SelectInput = styled ( 'input' ) ( { display : 'none' , } ) ; const SelectWrapper = styled ( 'div' ) < Pick < SelectBaseProps , 'grow' | 'disabled' > > ( { position : 'relative' , } , ( { grow } ) => ( { display : grow ? 'block' : 'inline-block' , } ) , ( { disabled } ) => ( { '&:hover .menu-icon path' : { fill : disabled ? undefined : colors . licorice500 , } , } ) ) ; const defaultRenderOption = option => { return < div > { option . label } < /div>; } ; const SelectBase = ( { 'aria-labelledby' : ariaLabelledBy , 'aria-required' : ariaRequired , as , forwardedButtonRef , localButtonRef , disabled , error , focusedOptionIndex = 0 , grow , inputRef , menuPlacement = 'bottom' , menuRef , menuVisibility = 'closed' , onChange , onKeyDown , onClose , onOptionSelection , options , renderOption = defaultRenderOption , required , shouldMenuAutoFlip = true , shouldMenuAutoFocus = true , value , ... elemProps } ) => { const focusedOptionRef = React . useRef < HTMLLIElement > ( null ) ; const menuId = useUniqueId ( ) ; const renderOptions = ( renderOption ) => { const selectedOptionIndex = getCorrectedIndexByValue ( options , value ) ; return options . map ( ( option , index ) => { const optionProps = { 'aria-disabled' : option . disabled ? true : undefined , 'aria-selected' : selectedOptionIndex === index ? true : undefined , error , focused : focusedOptionIndex === index , id : option . id , interactive : menuVisibility === 'opening' || menuVisibility === 'opened' , key : option . id , optionRef : focusedOptionIndex === index ? focusedOptionRef : undefined , value : option . value , ... ( onOptionSelection ? { onClick : ( event ) => { event . preventDefault ( ) ; onOptionSelection ( index ) ; } , } : { } ) , } ; const normalizedOption = { ... option , focused : optionProps . focused , selected : ! ! optionProps [ 'aria-selected' ] , } ; return < SelectOption { ... optionProps } > { renderOption ( normalizedOption ) } < /SelectOption>; } ) ; } ; useLayoutEffect ( ( ) => { const focusedOption = focusedOptionRef . current ; if ( focusedOption ) { const animateId = requestAnimationFrame ( ( ) => { scrollIntoViewIfNeeded ( focusedOption , false ) ; } ) ; return ( ) => { cancelAnimationFrame ( animateId ) ; } ; } return undefined ; } , [ focusedOptionIndex ] ) ; useLayoutEffect ( ( ) => { const focusedOption = focusedOptionRef . current ; if ( focusedOption && ( menuVisibility === 'opening' || menuVisibility === 'opened' ) ) { const animateId = requestAnimationFrame ( ( ) => { scrollIntoViewIfNeeded ( focusedOption , true ) ; } ) ; return ( ) => { cancelAnimationFrame ( animateId ) ; } ; } return undefined ; } , [ menuVisibility ] ) ; const hasOptions = options . length > 0 ; const selectedOption = hasOptions ? options [ getCorrectedIndexByValue ( options , value ) ] : null ; const selectedOptionLabel = selectedOption ? selectedOption . label : '' ; const selectedOptionValue = selectedOption ? selectedOption . value : '' ; return ( < SelectWrapper grow = { grow } disabled = { disabled } > < SelectButton aria - expanded = { menuVisibility !== 'closed' ? 'true' : undefined } aria - haspopup = "listbox" aria - controls = { menuVisibility !== 'closed' ? menuId : undefined } as = { as } disabled = { disabled } error = { error } grow = { grow } menuVisibility = { menuVisibility } onKeyDown = { onKeyDown } onKeyUp = { e => { e . preventDefault ( ) ; } } ref = { forwardedButtonRef } type = "button" value = { selectedOptionValue } { ... elemProps } > { selectedOptionLabel } < /SelectButton> < SelectInput onChange = { onChange } ref = { inputRef } type = "text" value = { selectedOptionValue } / > { hasOptions && menuVisibility !== 'closed' && ( < SelectMenu aria - activedescendant = { options [ focusedOptionIndex ] . id } aria - labelledby = { ariaLabelledBy } aria - required = { ariaRequired || required ? true : undefined } buttonRef = { localButtonRef } id = { menuId } error = { error } menuRef = { menuRef } onKeyDown = { onKeyDown } onClose = { onClose } placement = { menuPlacement } shouldAutoFlip = { shouldMenuAutoFlip } shouldAutoFocus = { shouldMenuAutoFocus } visibility = { menuVisibility } > { renderOptions ( renderOption ) } < /SelectMenu>)} < SelectMenuIcon className = "menu-icon" icon = { caretDownSmallIcon } color = { disabled ? colors . licorice100 : colors . licorice200 } colorHover = { disabled ? colors . licorice100 : colors . licorice500 } size = { menuIconSize } / > < /SelectWrapper>); } ; export default SelectBase ;