'use client' import { useFocusRing } from '@react-aria/focus' import { useHover } from '@react-aria/interactions' import React, { Fragment, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type CSSProperties, type ElementType, type MutableRefObject, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type Ref, } from 'react' import { flushSync } from 'react-dom' import { useActivePress } from '../../hooks/use-active-press' import { useByComparator, type ByComparator } from '../../hooks/use-by-comparator' import { useControllable } from '../../hooks/use-controllable' import { useDefaultValue } from '../../hooks/use-default-value' import { useDidElementMove } from '../../hooks/use-did-element-move' import { useDisposables } from '../../hooks/use-disposables' import { useElementSize } from '../../hooks/use-element-size' import { useEvent } from '../../hooks/use-event' import { useId } from '../../hooks/use-id' import { useInertOthers } from '../../hooks/use-inert-others' import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect' import { useLatestValue } from '../../hooks/use-latest-value' import { useOnDisappear } from '../../hooks/use-on-disappear' import { useOutsideClick } from '../../hooks/use-outside-click' import { useOwnerDocument } from '../../hooks/use-owner' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' import { useScrollLock } from '../../hooks/use-scroll-lock' import { useSyncRefs } from '../../hooks/use-sync-refs' import { useTextValue } from '../../hooks/use-text-value' import { useTrackedPointer } from '../../hooks/use-tracked-pointer' import { transitionDataAttributes, useTransition } from '../../hooks/use-transition' import { useDisabled } from '../../internal/disabled' import { FloatingProvider, useFloatingPanel, useFloatingPanelProps, useFloatingReference, useFloatingReferenceProps, useResolvedAnchor, type AnchorPropsWithSelection, } from '../../internal/floating' import { FormFields } from '../../internal/form-fields' import { useFrozenData } from '../../internal/frozen' import { useProvidedId } from '../../internal/id' import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed' import { useSlice } from '../../react-glue' import type { EnsureArray, Props } from '../../types' import { isDisabledReactIssue7711 } from '../../utils/bugs' import { Focus } from '../../utils/calculate-active-index' import { disposables } from '../../utils/disposables' import { Focus as FocusManagementFocus, FocusableMode, focusFrom, isFocusableElement, } from '../../utils/focus-management' import { attemptSubmit } from '../../utils/form' import { match } from '../../utils/match' import { getOwnerDocument } from '../../utils/owner' import { RenderFeatures, forwardRefWithAs, mergeProps, useRender, type HasDisplayName, type PropsForFeatures, type RefProp, } from '../../utils/render' import { useDescribedBy } from '../description/description' import { Keys } from '../keyboard' import { Label, useLabelledBy, useLabels, type _internal_ComponentLabel } from '../label/label' import { Portal } from '../portal/portal' import { ActionTypes, ActivationTrigger, ListboxStates, ValueMode } from './listbox-machine' import { ListboxContext, useListboxMachine, useListboxMachineContext } from './listbox-machine-glue' type ListboxOptionDataRef = MutableRefObject<{ textValue?: string disabled: boolean value: T domRef: MutableRefObject }> let ListboxDataContext = createContext<{ value: unknown disabled: boolean invalid: boolean mode: ValueMode orientation: 'horizontal' | 'vertical' onChange(value: unknown): void compare(a: unknown, z: unknown): boolean isSelected(value: unknown): boolean optionsPropsRef: MutableRefObject<{ static: boolean hold: boolean }> listRef: MutableRefObject> } | null>(null) ListboxDataContext.displayName = 'ListboxDataContext' function useData(component: string) { let context = useContext(ListboxDataContext) if (context === null) { let err = new Error(`<${component} /> is missing a parent component.`) if (Error.captureStackTrace) Error.captureStackTrace(err, useData) throw err } return context } type _Data = ReturnType // --- let DEFAULT_LISTBOX_TAG = Fragment type ListboxRenderPropArg = { open: boolean disabled: boolean invalid: boolean value: T } export type ListboxProps< TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, TType = string, TActualType = TType, > = Props< TTag, ListboxRenderPropArg, 'value' | 'defaultValue' | 'onChange' | 'by' | 'disabled' | 'horizontal' | 'name' | 'multiple', { value?: TType defaultValue?: TType onChange?(value: TType): void by?: ByComparator disabled?: boolean invalid?: boolean horizontal?: boolean form?: string name?: string multiple?: boolean __demoMode?: boolean } > function ListboxFn< TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, TType = string, TActualType = TType extends (infer U)[] ? U : TType, >(props: ListboxProps, ref: Ref) { let providedDisabled = useDisabled() let { value: controlledValue, defaultValue: _defaultValue, form, name, onChange: controlledOnChange, by, invalid = false, disabled = providedDisabled || false, horizontal = false, multiple = false, __demoMode = false, ...theirProps } = props const orientation = horizontal ? 'horizontal' : 'vertical' let listboxRef = useSyncRefs(ref) let defaultValue = useDefaultValue(_defaultValue) let [value = multiple ? [] : undefined, theirOnChange] = useControllable( controlledValue, controlledOnChange, defaultValue ) let machine = useListboxMachine({ __demoMode }) let optionsPropsRef = useRef<_Data['optionsPropsRef']['current']>({ static: false, hold: false }) let listRef = useRef<_Data['listRef']['current']>(new Map()) let compare = useByComparator(by) let isSelected: (value: TActualType) => boolean = useCallback( (compareValue) => match(data.mode, { [ValueMode.Multi]: () => { return (value as EnsureArray).some((option) => compare(option, compareValue)) }, [ValueMode.Single]: () => { return compare(value as TActualType, compareValue) }, }), [value] ) let data = useMemo<_Data>( () => ({ value, disabled, invalid, mode: multiple ? ValueMode.Multi : ValueMode.Single, orientation, onChange: theirOnChange, compare, isSelected, optionsPropsRef, listRef, }), [ value, disabled, invalid, multiple, orientation, theirOnChange, compare, isSelected, optionsPropsRef, listRef, ] ) useIsoMorphicEffect(() => { machine.state.dataRef.current = data }, [data]) let listboxState = useSlice(machine, (state) => state.listboxState) // Handle outside click let outsideClickEnabled = listboxState === ListboxStates.Open let [buttonElement, optionsElement] = useSlice(machine, (state) => [ state.buttonElement, state.optionsElement, ]) useOutsideClick(outsideClickEnabled, [buttonElement, optionsElement], (event, target) => { machine.send({ type: ActionTypes.CloseListbox }) if (!isFocusableElement(target, FocusableMode.Loose)) { event.preventDefault() buttonElement?.focus() } }) let slot = useMemo(() => { return { open: listboxState === ListboxStates.Open, disabled, invalid, value, } satisfies ListboxRenderPropArg }, [listboxState, disabled, invalid, value]) let [labelledby, LabelProvider] = useLabels({ inherit: true }) let ourProps = { ref: listboxRef } let reset = useCallback(() => { if (defaultValue === undefined) return return theirOnChange?.(defaultValue) }, [theirOnChange, defaultValue]) let render = useRender() return ( {name != null && value != null && ( )} {render({ ourProps, theirProps, slot, defaultTag: DEFAULT_LISTBOX_TAG, name: 'Listbox', })} ) } // --- let DEFAULT_BUTTON_TAG = 'button' as const type ButtonRenderPropArg = { disabled: boolean invalid: boolean hover: boolean focus: boolean autofocus: boolean open: boolean active: boolean value: any } type ButtonPropsWeControl = | 'aria-controls' | 'aria-expanded' | 'aria-haspopup' | 'aria-labelledby' | 'disabled' export type ListboxButtonProps = Props< TTag, ButtonRenderPropArg, ButtonPropsWeControl, { autoFocus?: boolean disabled?: boolean } > function ButtonFn( props: ListboxButtonProps, ref: Ref ) { let internalId = useId() let providedId = useProvidedId() let data = useData('Listbox.Button') let machine = useListboxMachineContext('Listbox.Button') let { id = providedId || `headlessui-listbox-button-${internalId}`, disabled = data.disabled || false, autoFocus = false, ...theirProps } = props let buttonRef = useSyncRefs(ref, useFloatingReference(), machine.actions.setButtonElement) let getFloatingReferenceProps = useFloatingReferenceProps() let handleKeyDown = useEvent((event: ReactKeyboardEvent) => { switch (event.key) { // Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/#keyboard-interaction-13 case Keys.Enter: attemptSubmit(event.currentTarget) break case Keys.Space: case Keys.ArrowDown: event.preventDefault() flushSync(() => machine.actions.openListbox()) if (!data.value) machine.actions.goToOption({ focus: Focus.First }) break case Keys.ArrowUp: event.preventDefault() flushSync(() => machine.actions.openListbox()) if (!data.value) machine.actions.goToOption({ focus: Focus.Last }) break } }) let handleKeyUp = useEvent((event: ReactKeyboardEvent) => { switch (event.key) { case Keys.Space: // Required for firefox, event.preventDefault() in handleKeyDown for // the Space key doesn't cancel the handleKeyUp, which in turn // triggers a *click*. event.preventDefault() break } }) let handleMouseDown = useEvent((event: ReactMouseEvent) => { if (event.button !== 0) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() if (machine.state.listboxState === ListboxStates.Open) { flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) } else { event.preventDefault() machine.actions.openListbox() } }) // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. let handleKeyPress = useEvent((event: ReactKeyboardEvent) => event.preventDefault()) let labelledBy = useLabelledBy([id]) let describedBy = useDescribedBy() let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus }) let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled }) let { pressed: active, pressProps } = useActivePress({ disabled }) let listboxState = useSlice(machine, (state) => state.listboxState) let slot = useMemo(() => { return { open: listboxState === ListboxStates.Open, active: active || listboxState === ListboxStates.Open, disabled, invalid: data.invalid, value: data.value, hover, focus, autofocus: autoFocus, } satisfies ButtonRenderPropArg }, [listboxState, data.value, disabled, hover, focus, active, data.invalid, autoFocus]) let open = useSlice(machine, (state) => state.listboxState === ListboxStates.Open) let [buttonElement, optionsElement] = useSlice(machine, (state) => [ state.buttonElement, state.optionsElement, ]) let ourProps = mergeProps( getFloatingReferenceProps(), { ref: buttonRef, id, type: useResolveButtonType(props, buttonElement), 'aria-haspopup': 'listbox', 'aria-controls': optionsElement?.id, 'aria-expanded': open, 'aria-labelledby': labelledBy, 'aria-describedby': describedBy, disabled: disabled || undefined, autoFocus, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onKeyPress: handleKeyPress, onMouseDown: handleMouseDown, }, focusProps, hoverProps, pressProps ) let render = useRender() return render({ ourProps, theirProps, slot, defaultTag: DEFAULT_BUTTON_TAG, name: 'Listbox.Button', }) } // --- let SelectedOptionContext = createContext(false) let DEFAULT_OPTIONS_TAG = 'div' as const type OptionsRenderPropArg = { open: boolean } type OptionsPropsWeControl = | 'aria-activedescendant' | 'aria-labelledby' | 'aria-multiselectable' | 'aria-orientation' | 'role' | 'tabIndex' let OptionsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static export type ListboxOptionsProps = Props< TTag, OptionsRenderPropArg, OptionsPropsWeControl, { anchor?: AnchorPropsWithSelection portal?: boolean modal?: boolean transition?: boolean } & PropsForFeatures > function OptionsFn( props: ListboxOptionsProps, ref: Ref ) { let internalId = useId() let { id = `headlessui-listbox-options-${internalId}`, anchor: rawAnchor, portal = false, modal = true, transition = false, ...theirProps } = props let anchor = useResolvedAnchor(rawAnchor) // To improve the correctness of transitions (timing related race conditions), // we track the element locally to this component, instead of relying on the // context value. This way, the component can re-render independently of the // parent component when the `useTransition(…)` hook performs a state change. let [localOptionsElement, setLocalOptionsElement] = useState(null) // Always enable `portal` functionality, when `anchor` is enabled if (anchor) { portal = true } let data = useData('Listbox.Options') let machine = useListboxMachineContext('Listbox.Options') let [listboxState, buttonElement, optionsElement, __demoMode] = useSlice(machine, (state) => [ state.listboxState, state.buttonElement, state.optionsElement, state.__demoMode, ]) let portalOwnerDocument = useOwnerDocument(buttonElement) let ownerDocument = useOwnerDocument(optionsElement) let usesOpenClosedState = useOpenClosed() let [visible, transitionData] = useTransition( transition, localOptionsElement, usesOpenClosedState !== null ? (usesOpenClosedState & State.Open) === State.Open : listboxState === ListboxStates.Open ) // Ensure we close the listbox as soon as the button becomes hidden useOnDisappear(visible, buttonElement, machine.actions.closeListbox) // Enable scroll locking when the listbox is visible, and `modal` is enabled let scrollLockEnabled = __demoMode ? false : modal && listboxState === ListboxStates.Open useScrollLock(scrollLockEnabled, ownerDocument) // Mark other elements as inert when the listbox is visible, and `modal` is enabled let inertOthersEnabled = __demoMode ? false : modal && listboxState === ListboxStates.Open useInertOthers(inertOthersEnabled, { allowed: useCallback(() => [buttonElement, optionsElement], [buttonElement, optionsElement]), }) // We keep track whether the button moved or not, we only check this when the menu state becomes // closed. If the button moved, then we want to cancel pending transitions to prevent that the // attached `MenuItems` is still transitioning while the button moved away. // // If we don't cancel these transitions then there will be a period where the `MenuItems` is // visible and moving around because it is trying to re-position itself based on the new position. // // This can be solved by only transitioning the `opacity` instead of everything, but if you _do_ // want to transition the y-axis for example you will run into the same issue again. let didElementMoveEnabled = listboxState !== ListboxStates.Open let didButtonMove = useDidElementMove(didElementMoveEnabled, buttonElement) // Now that we know that the button did move or not, we can either disable the panel and all of // its transitions, or rely on the `visible` state to hide the panel whenever necessary. let panelEnabled = didButtonMove ? false : visible // We should freeze when the listbox is visible but "closed". This means that // a transition is currently happening and the component is still visible (for // the transition) but closed from a functionality perspective. let shouldFreeze = visible && listboxState === ListboxStates.Closed // Frozen state, the selected value will only update visually when the user re-opens the let frozenValue = useFrozenData(shouldFreeze, data.value) let isSelected = useEvent((compareValue: unknown) => data.compare(frozenValue, compareValue)) let selectedOptionIndex = useSlice(machine, (state) => { if (anchor == null) return null if (!anchor?.to?.includes('selection')) return null // Only compute the selected option index when using `selection` in the // `anchor` prop. let idx = state.options.findIndex((option) => isSelected(option.dataRef.current.value)) // Ensure that if no data is selected, we default to the first item. if (idx === -1) idx = 0 return idx }) let anchorOptions = (() => { if (anchor == null) return undefined if (selectedOptionIndex === null) return { ...anchor, inner: undefined } let elements = Array.from(data.listRef.current.values()) return { ...anchor, inner: { listRef: { current: elements }, index: selectedOptionIndex, }, } })() let [floatingRef, style] = useFloatingPanel(anchorOptions) let getFloatingPanelProps = useFloatingPanelProps() let optionsRef = useSyncRefs( ref, anchor ? floatingRef : null, machine.actions.setOptionsElement, setLocalOptionsElement ) let searchDisposables = useDisposables() useEffect(() => { let container = optionsElement if (!container) return if (listboxState !== ListboxStates.Open) return if (container === getOwnerDocument(container)?.activeElement) return container?.focus({ preventScroll: true }) }, [listboxState, optionsElement]) let handleKeyDown = useEvent((event: ReactKeyboardEvent) => { searchDisposables.dispose() switch (event.key) { // Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboard-interaction-12 // @ts-expect-error Fallthrough is expected here case Keys.Space: if (machine.state.searchQuery !== '') { event.preventDefault() event.stopPropagation() return machine.actions.search(event.key) } // When in type ahead mode, fallthrough case Keys.Enter: event.preventDefault() event.stopPropagation() if (machine.state.activeOptionIndex !== null) { let { dataRef } = machine.state.options[machine.state.activeOptionIndex] machine.actions.onChange(dataRef.current.value) } if (data.mode === ValueMode.Single) { flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) } break case match(data.orientation, { vertical: Keys.ArrowDown, horizontal: Keys.ArrowRight, }): event.preventDefault() event.stopPropagation() return machine.actions.goToOption({ focus: Focus.Next }) case match(data.orientation, { vertical: Keys.ArrowUp, horizontal: Keys.ArrowLeft, }): event.preventDefault() event.stopPropagation() return machine.actions.goToOption({ focus: Focus.Previous }) case Keys.Home: case Keys.PageUp: event.preventDefault() event.stopPropagation() return machine.actions.goToOption({ focus: Focus.First }) case Keys.End: case Keys.PageDown: event.preventDefault() event.stopPropagation() return machine.actions.goToOption({ focus: Focus.Last }) case Keys.Escape: event.preventDefault() event.stopPropagation() flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) return case Keys.Tab: event.preventDefault() event.stopPropagation() flushSync(() => machine.actions.closeListbox()) focusFrom( machine.state.buttonElement!, event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next ) break default: if (event.key.length === 1) { machine.actions.search(event.key) searchDisposables.setTimeout(() => machine.actions.clearSearch(), 350) } break } }) let labelledby = useSlice(machine, (state) => state.buttonElement?.id) let slot = useMemo(() => { return { open: listboxState === ListboxStates.Open, } satisfies OptionsRenderPropArg }, [listboxState]) let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, { id, ref: optionsRef, 'aria-activedescendant': useSlice(machine, machine.selectors.activeDescendantId), 'aria-multiselectable': data.mode === ValueMode.Multi ? true : undefined, 'aria-labelledby': labelledby, 'aria-orientation': data.orientation, onKeyDown: handleKeyDown, role: 'listbox', // When the `Listbox` is closed, it should not be focusable. This allows us // to skip focusing the `ListboxOptions` when pressing the tab key on an // open `Listbox`, and go to the next focusable element. tabIndex: listboxState === ListboxStates.Open ? 0 : undefined, style: { ...theirProps.style, ...style, '--button-width': useElementSize(buttonElement, true).width, } as CSSProperties, ...transitionDataAttributes(transitionData), }) let render = useRender() // We want to use the local `isSelected` with frozen values when we are in // single value mode. let newData = useMemo( () => (data.mode === ValueMode.Multi ? data : { ...data, isSelected }), [data, isSelected] ) return ( {render({ ourProps, theirProps, slot, defaultTag: DEFAULT_OPTIONS_TAG, features: OptionsRenderFeatures, visible: panelEnabled, name: 'Listbox.Options', })} ) } // --- let DEFAULT_OPTION_TAG = 'div' as const type OptionRenderPropArg = { /** @deprecated use `focus` instead */ active: boolean focus: boolean selected: boolean disabled: boolean selectedOption: boolean } type OptionPropsWeControl = 'aria-disabled' | 'aria-selected' | 'role' | 'tabIndex' export type ListboxOptionProps< TTag extends ElementType = typeof DEFAULT_OPTION_TAG, TType = string, > = Props< TTag, OptionRenderPropArg, OptionPropsWeControl, { disabled?: boolean value: TType } > function OptionFn< TTag extends ElementType = typeof DEFAULT_OPTION_TAG, // TODO: One day we will be able to infer this type from the generic in Listbox itself. // But today is not that day.. TType = Parameters[0]['value'], >(props: ListboxOptionProps, ref: Ref) { let internalId = useId() let { id = `headlessui-listbox-option-${internalId}`, disabled = false, value, ...theirProps } = props let usedInSelectedOption = useContext(SelectedOptionContext) === true let data = useData('Listbox.Option') let machine = useListboxMachineContext('Listbox.Option') let active = useSlice(machine, (state) => machine.selectors.isActive(state, id)) let selected = data.isSelected(value) let internalOptionRef = useRef(null) let getTextValue = useTextValue(internalOptionRef) let bag = useLatestValue['current']>({ disabled, value, domRef: internalOptionRef, get textValue() { return getTextValue() }, }) let optionRef = useSyncRefs(ref, internalOptionRef, (el) => { if (!el) { data.listRef.current.delete(id) } else { data.listRef.current.set(id, el) } }) let shouldScrollIntoView = useSlice(machine, (state) => machine.selectors.shouldScrollIntoView(state, id) ) useIsoMorphicEffect(() => { if (!shouldScrollIntoView) return return disposables().requestAnimationFrame(() => { internalOptionRef.current?.scrollIntoView?.({ block: 'nearest' }) }) }, [shouldScrollIntoView, internalOptionRef]) useIsoMorphicEffect(() => { if (usedInSelectedOption) return machine.actions.registerOption(id, bag) return () => machine.actions.unregisterOption(id) }, [bag, id, usedInSelectedOption]) let handleClick = useEvent((event: { preventDefault: Function }) => { if (disabled) return event.preventDefault() machine.actions.onChange(value) if (data.mode === ValueMode.Single) { flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) } }) let handleFocus = useEvent(() => { if (disabled) return machine.actions.goToOption({ focus: Focus.Nothing }) machine.actions.goToOption({ focus: Focus.Specific, id }) }) let pointer = useTrackedPointer() let handleEnter = useEvent((evt) => { pointer.update(evt) if (disabled) return if (active) return machine.actions.goToOption({ focus: Focus.Specific, id }, ActivationTrigger.Pointer) }) let handleMove = useEvent((evt) => { if (!pointer.wasMoved(evt)) return if (disabled) return if (active) return machine.actions.goToOption({ focus: Focus.Specific, id }, ActivationTrigger.Pointer) }) let handleLeave = useEvent((evt) => { if (!pointer.wasMoved(evt)) return if (disabled) return if (!active) return machine.actions.goToOption({ focus: Focus.Nothing }) }) let slot = useMemo(() => { return { active, focus: active, selected, disabled, selectedOption: selected && usedInSelectedOption, } satisfies OptionRenderPropArg }, [active, selected, disabled, usedInSelectedOption]) let ourProps = !usedInSelectedOption ? { id, ref: optionRef, role: 'option', tabIndex: disabled === true ? undefined : -1, 'aria-disabled': disabled === true ? true : undefined, // According to the WAI-ARIA best practices, we should use aria-checked for // multi-select,but Voice-Over disagrees. So we use aria-checked instead for // both single and multi-select. 'aria-selected': selected, disabled: undefined, // Never forward the `disabled` prop onClick: handleClick, onFocus: handleFocus, onPointerEnter: handleEnter, onMouseEnter: handleEnter, onPointerMove: handleMove, onMouseMove: handleMove, onPointerLeave: handleLeave, onMouseLeave: handleLeave, } : {} let render = useRender() if (!selected && usedInSelectedOption) { return null } return render({ ourProps, theirProps, slot, defaultTag: DEFAULT_OPTION_TAG, name: 'Listbox.Option', }) } // --- let DEFAULT_SELECTED_OPTION_TAG = Fragment type SelectedOptionRenderPropArg = {} type SelectedOptionPropsWeControl = never export type ListboxSelectedOptionProps< TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TAG, > = Props< TTag, SelectedOptionRenderPropArg, SelectedOptionPropsWeControl, { options: React.ReactNode placeholder?: React.ReactNode } > function SelectedFn( props: ListboxSelectedOptionProps, ref: Ref ) { let { options: children, placeholder, ...theirProps } = props let selectedRef = useSyncRefs(ref) let ourProps = { ref: selectedRef } let data = useData('ListboxSelectedOption') let slot = useMemo(() => ({}) satisfies SelectedOptionRenderPropArg, []) let shouldShowPlaceholder = data.value === undefined || data.value === null || (data.mode === ValueMode.Multi && Array.isArray(data.value) && data.value.length === 0) let render = useRender() return ( {render({ ourProps, theirProps: { ...theirProps, children: <>{placeholder && shouldShowPlaceholder ? placeholder : children}, }, slot, defaultTag: DEFAULT_SELECTED_OPTION_TAG, name: 'ListboxSelectedOption', })} ) } // --- export interface _internal_ComponentListbox extends HasDisplayName { < TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, TType = string, TActualType = TType extends (infer U)[] ? U : TType, >( props: ListboxProps & RefProp ): React.JSX.Element } export interface _internal_ComponentListboxButton extends HasDisplayName { ( props: ListboxButtonProps & RefProp ): React.JSX.Element } export interface _internal_ComponentListboxLabel extends _internal_ComponentLabel {} export interface _internal_ComponentListboxOptions extends HasDisplayName { ( props: ListboxOptionsProps & RefProp ): React.JSX.Element } export interface _internal_ComponentListboxOption extends HasDisplayName { < TTag extends ElementType = typeof DEFAULT_OPTION_TAG, TType = Parameters[0]['value'], >( props: ListboxOptionProps & RefProp ): React.JSX.Element } export interface _internal_ComponentListboxSelectedOption extends HasDisplayName { ( props: ListboxSelectedOptionProps & RefProp ): React.JSX.Element } let ListboxRoot = forwardRefWithAs(ListboxFn) as _internal_ComponentListbox export let ListboxButton = forwardRefWithAs(ButtonFn) as _internal_ComponentListboxButton /** @deprecated use `