import { FocusTarget , Keys , arrayify , isNil , isNumber , mergeProps , useAutoFocus , useChainedEventCallback , useControllableState , useEventCallback , useId , useMergedRefs , useRawSlots , useRefState } from "../../shared" ; import { KeyboardEvent , ReactNode , Ref , SyntheticEvent , useCallback , useMemo } from "react" ; import { OverlayProps , PopupAlignment , PopupDirection , PopupPosition , usePopup , useTriggerWidth } from "../../overlay" ; import { useCollection , useOnlyCollectionItems } from "../../collection" ; import { OptionKeyProp } from "../../listbox" ; import { ValidationState } from "../../input" ; export interface UseSelectOptions { align ? : PopupAlignment ; allowFlip ? : boolean ; allowPreventOverflow ? : boolean ; allowResponsiveMenuWidth ? : boolean ; ariaDescribedBy ? : string ; ariaLabel ? : string ; ariaLabelledBy ? : string ; autoFocus ? : boolean | number ; defaultOpen ? : boolean ; defaultSelectedKey ? : string ; direction ? : PopupDirection ; disabled ? : boolean ; id ? : string ; onOpenChange ? : ( event : SyntheticEvent , isOpen : boolean ) => void ; onSelectionChange ? : ( event : SyntheticEvent , selectedKey : string ) => void ; open ? : boolean | null ; overlayProps ? : Partial < OverlayProps > ; readOnly ? : boolean ; ref ? : Ref < any > ; selectedKey ? : string | null ; validationState ? : ValidationState ; } export function useSelect ( children , { align = "start" , allowFlip , allowPreventOverflow , allowResponsiveMenuWidth = true , ariaDescribedBy , ariaLabel , ariaLabelledBy , autoFocus , defaultOpen , defaultSelectedKey , direction = "bottom" , disabled , id , onOpenChange , onSelectionChange , open : openProp , overlayProps : { id : menuId , width : menuWidth , ... menuProps } = { } , readOnly , ref , selectedKey : selectedKeyProp , validationState } = { } ) { const [ selectedKey , setSelectedKey ] = useControllableState ( selectedKeyProp , defaultSelectedKey , null ) ; const [ autoFocusTargetRef , setAutoFocusTarget ] = useRefState < string > ( FocusTarget . first ) ; const handleOpenChange = useChainedEventCallback ( onOpenChange , ( event , isVisible ) => { if ( ! isVisible ) { setAutoFocusTarget ( FocusTarget . first ) ; } } ) ; const { focusScope , isOpen , overlayProps , setIsOpen , triggerProps : { ref : popupTriggerRef , ... triggerProps } } = usePopup ( "listbox" , { allowFlip , allowPreventOverflow , defaultOpen , disabled : disabled || readOnly , hideOnEscape : true , hideOnLeave : true , hideOnOutsideClick : false , id : menuId , keyProp : OptionKeyProp , offset : [ 0 , 4 ] , onOpenChange : handleOpenChange , open : openProp , position : `${ direction }-${ align }` as PopupPosition , trigger : "click" } ) ; const [ triggerWidthRef , triggerWidth ] = useTriggerWidth ( { isDisabled : ! allowResponsiveMenuWidth || ! isNil ( menuWidth ) } ) ; const triggerRef = useMergedRefs ( ref , popupTriggerRef , triggerWidthRef ) ; const updateSelectedKey = useCallback ( ( event , newKeys ) => { const newKey = newKeys [ 0 ] ?? null ; if ( newKey !== selectedKey ) { setSelectedKey ( newKey ) ; if ( ! isNil ( onSelectionChange ) ) { onSelectionChange ( event , newKey ) ; } } } , [ selectedKey , setSelectedKey , onSelectionChange ] ) ; const open = useCallback ( ( event , focusTarget ) => { setAutoFocusTarget ( focusTarget ) ; setIsOpen ( event , true ) ; } , [ setIsOpen , setAutoFocusTarget ] ) ; const close = useCallback ( ( event ) => { setIsOpen ( event , false ) ; } , [ setIsOpen ] ) ; const handleTriggerKeyDown = useEventCallback ( ( event ) => { switch ( event . key ) { case Keys . arrowDown : event . preventDefault ( ) ; open ( event , FocusTarget . first ) ; break ; case Keys . arrowUp : event . preventDefault ( ) ; open ( event , FocusTarget . last ) ; break ; } } ) ; const handleListboxSelectionChange = useEventCallback ( ( event , newValue ) => { updateSelectedKey ( event , newValue ) ; close ( event ) ; } ) ; useAutoFocus ( triggerRef , { delay : isNumber ( autoFocus ) ? autoFocus : undefined , isDisabled : ! autoFocus || isOpen } ) ; const nodes = useCollection ( children ) ; const items = useOnlyCollectionItems ( nodes ) ; const selectedItem = useMemo ( ( ) => items . find ( x => x . key === selectedKey ) , [ items , selectedKey ] ) ; const { avatar , "end-icon" : endIcon , icon , stringValue , text } = useRawSlots ( selectedItem ?. content , [ "icon" , "avatar" , "text" , "end-icon" ] ) ; const triggerId = useId ( id , "o-ui-select-trigger" ) ; const valueId = useId ( id , "o-ui-select-value" ) ; return { close , focusScope , isOpen , listboxProps : { "aria-describedby" : ariaDescribedBy , "aria-label" : ariaLabel , "aria-labelledby" : isNil ( ariaLabel ) ? ( ariaLabelledBy ?? triggerId ) : undefined , autoFocus : isOpen , autoFocusTarget : autoFocusTargetRef . current , fluid : true , focusOnHover : true , nodes , onSelectionChange : handleListboxSelectionChange , selectedKeys : useMemo ( ( ) => arrayify ( selectedKey ) , [ selectedKey ] ) , validationState } , open , overlayProps : mergeProps ( { className : "o-ui-select-menu" , width : menuWidth ?? triggerWidth ?? undefined } , menuProps , overlayProps ) , selectedItem : isNil ( selectedItem ) ? undefined : { avatar , endIcon , icon , text : text ?. props ?. children ?? stringValue ?? "" } , selectedKey , setSelectedKey : updateSelectedKey , triggerProps : mergeProps ( { "aria-describedby" : ariaDescribedBy , "aria-label" : ariaLabel , "aria-labelledby" : isNil ( ariaLabel ) ? isNil ( ariaLabelledBy ) ? valueId : `${ ariaLabelledBy } ${ valueId }` : undefined , disabled , id : triggerId , onKeyDown : ! isOpen ? handleTriggerKeyDown : undefined , ref : triggerRef } , triggerProps ) , valueProps : { id : valueId } } ; }