Fix crash in ListboxOptions when using as={Fragment} (#3513)

This PR fixes an issue where a `Maximum update depth exceeded` error
occurs if you use `as={Fragment}` in the `ListboxOptions` component.

This PR also includes a refactor to make sure this exact issue cannot
happen anymore in other components.

Fixes: #3507
This commit is contained in:
Robin Malfait
2024-10-09 23:25:17 +02:00
committed by GitHub
parent 3b047fc670
commit a4953a2b11
27 changed files with 164 additions and 48 deletions
+1
View File
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Use `React.JSX` instead of deprecated global `JSX` ([#3511](https://github.com/tailwindlabs/headlessui/pull/3511))
- Fix crash in `ListboxOptions` when using `as={Fragment}` ([#3513](https://github.com/tailwindlabs/headlessui/pull/3513))
## [2.1.9] - 2024-10-03
@@ -9,8 +9,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -42,7 +41,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
ref: Ref<HTMLElement>
) {
let providedDisabled = useDisabled()
let mergeRefs = useMergeRefsFn()
let { disabled = providedDisabled || false, autoFocus = false, ...theirProps } = props
let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
@@ -65,8 +63,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
return { disabled, hover, focus, active, autofocus: autoFocus } satisfies ButtonRenderPropArg
}, [disabled, hover, focus, active, autoFocus])
let render = useRender()
return render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -26,7 +26,7 @@ import { attemptSubmit } from '../../utils/form'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -176,6 +176,8 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
return onChange?.(defaultChecked)
}, [onChange, defaultChecked])
let render = useRender()
return (
<>
{name != null && (
@@ -69,8 +69,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -949,6 +948,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
return theirOnChange?.(defaultValue)
}, [theirOnChange, defaultValue])
let render = useRender()
return (
<LabelProvider
value={labelledby}
@@ -1444,6 +1445,8 @@ function InputFn<
hoverProps
)
let render = useRender()
return render({
ourProps,
theirProps,
@@ -1489,7 +1492,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
let data = useData('Combobox.Button')
let actions = useActions('Combobox.Button')
let buttonRef = useSyncRefs(ref, actions.setButtonElement)
let mergeRefs = useMergeRefsFn()
let internalId = useId()
let {
@@ -1610,8 +1612,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)
let render = useRender()
return render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -1813,6 +1816,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
})
}
let render = useRender()
return (
<Portal enabled={portal ? props.static || visible : false}>
<ComboboxDataContext.Provider
@@ -2037,6 +2042,8 @@ function OptionFn<
onMouseLeave: handleLeave,
}
let render = useRender()
return render({
ourProps,
theirProps,
@@ -8,7 +8,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -47,6 +47,8 @@ function DataInteractiveFn<TTag extends ElementType = typeof DEFAULT_DATA_INTERA
[hover, focus, active]
)
let render = useRender()
return render({
ourProps,
theirProps,
@@ -15,7 +15,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useDisabled } from '../../internal/disabled'
import type { Props } from '../../types'
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
// ---
@@ -121,6 +121,8 @@ function DescriptionFn<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG
let slot = useMemo(() => ({ ...context.slot, disabled }), [context.slot, disabled])
let ourProps = { ref: descriptionRef, ...context.props, id }
let render = useRender()
return render({
ourProps,
theirProps,
@@ -41,7 +41,7 @@ import { match } from '../../utils/match'
import {
RenderFeatures,
forwardRefWithAs,
render,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -286,6 +286,8 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
}
}
let render = useRender()
return (
<ResetOpenClosedProvider>
<ForcePortalRoot force={true}>
@@ -450,6 +452,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let Wrapper = transition ? TransitionChild : Fragment
let wrapperProps = transition ? { unmount } : {}
let render = useRender()
return (
<Wrapper {...wrapperProps}>
{render({
@@ -494,6 +498,8 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
let Wrapper = transition ? TransitionChild : Fragment
let wrapperProps = transition ? { unmount } : {}
let render = useRender()
return (
<Wrapper {...wrapperProps}>
{render({
@@ -541,6 +547,8 @@ function TitleFn<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
let ourProps = { ref: titleRef, id }
let render = useRender()
return render({
ourProps,
theirProps,
@@ -41,8 +41,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -233,6 +232,8 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
ref: disclosureRef,
}
let render = useRender()
return (
<DisclosureContext.Provider value={reducerBag}>
<DisclosureAPIContext.Provider value={api}>
@@ -304,7 +305,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
return dispatch({ type: ActionTypes.SetButtonElement, element })
})
)
let mergeRefs = useMergeRefsFn()
useEffect(() => {
if (isWithinPanel) return
@@ -411,8 +411,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)
let render = useRender()
return render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -451,7 +452,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
} = props
let [state, dispatch] = useDisclosureContext('Disclosure.Panel')
let { close } = useDisclosureAPIContext('Disclosure.Panel')
let mergeRefs = useMergeRefsFn()
// To improve the correctness of transitions (timing related race conditions),
// we track the element locally to this component, instead of relying on the
@@ -496,11 +496,12 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
...transitionDataAttributes(transitionData),
}
let render = useRender()
return (
<ResetOpenClosedProvider>
<DisclosurePanelContext.Provider value={state.panelId}>
{render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -6,7 +6,7 @@ import { DisabledProvider, useDisabled } from '../../internal/disabled'
import { FormFieldsProvider } from '../../internal/form-fields'
import { IdProvider } from '../../internal/id'
import type { Props } from '../../types'
import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
import { useDescriptions } from '../description/description'
import { useLabels } from '../label/label'
@@ -44,6 +44,8 @@ function FieldFn<TTag extends ElementType = typeof DEFAULT_FIELD_TAG>(
'aria-disabled': disabled || undefined,
}
let render = useRender()
return (
<DisabledProvider value={disabled}>
<LabelProvider value={labelledby}>
@@ -5,7 +5,7 @@ import { useResolvedTag } from '../../hooks/use-resolved-tag'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { DisabledProvider, useDisabled } from '../../internal/disabled'
import type { Props } from '../../types'
import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
import { useLabels } from '../label/label'
let DEFAULT_FIELDSET_TAG = 'fieldset' as const
@@ -50,6 +50,8 @@ function FieldsetFn<TTag extends ElementType = typeof DEFAULT_FIELDSET_TAG>(
'aria-disabled': disabled || undefined,
}
let render = useRender()
return (
<DisabledProvider value={disabled}>
<LabelProvider>
@@ -24,7 +24,7 @@ import { history } from '../../utils/active-element-history'
import { Focus, FocusResult, focusElement, focusIn } from '../../utils/focus-management'
import { match } from '../../utils/match'
import { microTask } from '../../utils/micro-task'
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
type Containers =
// Lazy resolved containers
@@ -197,6 +197,8 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
},
}
let render = useRender()
return (
<>
{tabLockEnabled && (
@@ -10,7 +10,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -78,6 +78,8 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies InputRenderPropArg
}, [disabled, invalid, hover, focus, autoFocus])
let render = useRender()
return render({
ourProps,
theirProps,
@@ -17,7 +17,7 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useDisabled } from '../../internal/disabled'
import { useProvidedId } from '../../internal/id'
import type { Props } from '../../types'
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
// ---
@@ -203,6 +203,8 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
}
}
let render = useRender()
return render({
ourProps,
theirProps,
@@ -74,8 +74,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -698,6 +697,8 @@ function ListboxFn<
return theirOnChange?.(defaultValue)
}, [theirOnChange, defaultValue])
let render = useRender()
return (
<LabelProvider
value={labelledby}
@@ -786,7 +787,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
autoFocus = false,
...theirProps
} = props
let mergeRefs = useMergeRefsFn()
let buttonRef = useSyncRefs(ref, useFloatingReference(), actions.setButtonElement)
let getFloatingReferenceProps = useFloatingReferenceProps()
@@ -881,8 +881,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)
let render = useRender()
return render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -1159,6 +1160,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
...transitionDataAttributes(transitionData),
})
let render = useRender()
return (
<Portal enabled={portal ? props.static || visible : false}>
<ListboxDataContext.Provider
@@ -1336,6 +1339,8 @@ function OptionFn<
}
: {}
let render = useRender()
if (!selected && usedInSelectedOption) {
return null
}
@@ -1383,6 +1388,8 @@ function SelectedFn<TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TA
data.value === null ||
(data.mode === ValueMode.Multi && Array.isArray(data.value) && data.value.length === 0)
let render = useRender()
return (
<SelectedOptionContext.Provider value={true}>
{render({
@@ -67,8 +67,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -426,6 +425,8 @@ function MenuFn<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
let ourProps = { ref: menuRef }
let render = useRender()
return (
<FloatingProvider>
<MenuContext.Provider value={reducerBag}>
@@ -484,7 +485,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
} = props
let [state, dispatch] = useMenuContext('Menu.Button')
let getFloatingReferenceProps = useFloatingReferenceProps()
let mergeRefs = useMergeRefsFn()
let buttonRef = useSyncRefs(
ref,
useFloatingReference(),
@@ -571,8 +571,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)
let render = useRender()
return render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -820,6 +821,8 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
...transitionDataAttributes(transitionData),
})
let render = useRender()
return (
<Portal enabled={portal ? props.static || visible : false}>
{render({
@@ -982,6 +985,8 @@ function ItemFn<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
onMouseLeave: handleLeave,
}
let render = useRender()
return (
<LabelProvider>
<DescriptionProvider>
@@ -1018,6 +1023,8 @@ function SectionFn<TTag extends ElementType = typeof DEFAULT_SECTION_TAG>(
let theirProps = props
let ourProps = { ref, 'aria-labelledby': labelledby, role: 'group' }
let render = useRender()
return (
<LabelProvider>
{render({
@@ -1055,6 +1062,8 @@ function HeadingFn<TTag extends ElementType = typeof DEFAULT_HEADING_TAG>(
let ourProps = { id, ref, role: 'presentation', ...context.props }
let render = useRender()
return render({
ourProps,
theirProps,
@@ -1083,6 +1092,8 @@ function SeparatorFn<TTag extends ElementType = typeof DEFAULT_SEPARATOR_TAG>(
let theirProps = props
let ourProps = { ref, role: 'separator' }
let render = useRender()
return render({
ourProps,
theirProps,
@@ -74,8 +74,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -419,6 +418,8 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
let ourProps = { ref: popoverRef }
let render = useRender()
return (
<MainTreeProvider node={mainTreeNode}>
<FloatingProvider>
@@ -707,6 +708,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
}
})
let render = useRender()
return (
<>
{render({
@@ -799,6 +802,8 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
...transitionDataAttributes(transitionData),
}
let render = useRender()
return render({
ourProps,
theirProps,
@@ -884,7 +889,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
setLocalPanelElement
)
let ownerDocument = useOwnerDocument(internalPanelRef)
let mergeRefs = useMergeRefsFn()
useIsoMorphicEffect(() => {
dispatch({ type: ActionTypes.SetPanelId, panelId: id })
@@ -1070,6 +1074,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
}
})
let render = useRender()
return (
<ResetOpenClosedProvider>
<PopoverPanelContext.Provider value={id}>
@@ -1087,7 +1093,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
/>
)}
{render({
mergeRefs,
ourProps,
theirProps,
slot,
@@ -1188,6 +1193,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
let theirProps = props
let ourProps = { ref: groupRef }
let render = useRender()
return (
<MainTreeProvider>
<PopoverGroupContext.Provider value={contextBag}>
@@ -23,7 +23,7 @@ import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
import { usePortalRoot } from '../../internal/portal-force-root'
import type { Props } from '../../types'
import { env } from '../../utils/env'
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
function usePortalTarget(ref: MutableRefObject<HTMLElement | null>): HTMLElement | null {
let forceInRoot = usePortalRoot()
@@ -129,6 +129,7 @@ let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
}
})
let render = useRender()
if (!ready) return null
let ourProps = { ref: portalRef }
@@ -154,6 +155,9 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
let portalRef = useSyncRefs(ref)
let { enabled = true, ...theirProps } = props
let render = useRender()
return enabled ? (
<InternalPortalFn {...theirProps} ref={portalRef} />
) : (
@@ -193,6 +197,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
let ourProps = { ref: groupRef }
let render = useRender()
return (
<PortalGroupContext.Provider value={target}>
{render({
@@ -35,7 +35,7 @@ import { getOwnerDocument } from '../../utils/owner'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -309,6 +309,8 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
return triggerChange(defaultValue)
}, [triggerChange, defaultValue])
let render = useRender()
return (
<DescriptionProvider name="RadioGroup.Description">
<LabelProvider name="RadioGroup.Label">
@@ -444,6 +446,8 @@ function OptionFn<
} satisfies OptionRenderPropArg
}, [checked, disabled, hover, focus, autoFocus])
let render = useRender()
return (
<DescriptionProvider name="RadioGroup.Description">
<LabelProvider name="RadioGroup.Label">
@@ -557,6 +561,8 @@ function RadioFn<
return { checked, disabled, hover, focus, autofocus: autoFocus } satisfies RadioRenderPropArg
}, [checked, disabled, hover, focus, autoFocus])
let render = useRender()
return render({
ourProps,
theirProps,
@@ -11,7 +11,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -89,6 +89,8 @@ function SelectFn<TTag extends ElementType = typeof DEFAULT_SELECT_TAG>(
} satisfies SelectRenderPropArg
}, [disabled, invalid, hover, focus, active, autoFocus])
let render = useRender()
return render({
ourProps,
theirProps,
@@ -32,7 +32,7 @@ import { attemptSubmit } from '../../utils/form'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -74,6 +74,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
let ourProps = {}
let theirProps = props
let render = useRender()
return (
<DescriptionProvider name="Switch.Description" value={describedby}>
<LabelProvider
@@ -244,6 +246,8 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
return onChange?.(defaultChecked)
}, [onChange, defaultChecked])
let render = useRender()
return (
<>
{name != null && (
@@ -33,7 +33,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -320,6 +320,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_TABS_TAG>(
let ourProps = { ref: tabsRef }
let render = useRender()
return (
<StableCollection>
<TabsActionsContext.Provider value={tabsActions}>
@@ -384,6 +386,8 @@ function ListFn<TTag extends ElementType = typeof DEFAULT_LIST_TAG>(
'aria-orientation': orientation,
}
let render = useRender()
return render({
ourProps,
theirProps,
@@ -556,6 +560,8 @@ function TabFn<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
pressProps
)
let render = useRender()
return render({
ourProps,
theirProps,
@@ -589,6 +595,8 @@ function PanelsFn<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>(
let theirProps = props
let ourProps = { ref: panelsRef }
let render = useRender()
return render({
ourProps,
theirProps,
@@ -650,6 +658,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
focusProps
)
let render = useRender()
if (!selected && (theirProps.unmount ?? true) && !(theirProps.static ?? false)) {
return <Hidden aria-hidden="true" {...ourProps} />
}
@@ -10,7 +10,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
@@ -78,6 +78,8 @@ function TextareaFn<TTag extends ElementType = typeof DEFAULT_TEXTAREA_TAG>(
return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies TextareaRenderPropArg
}, [disabled, invalid, hover, focus, autoFocus])
let render = useRender()
return render({
ourProps,
theirProps,
@@ -33,7 +33,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -290,6 +290,8 @@ function TooltipFn<TTag extends ElementType = typeof DEFAULT_TOOLTIP_TAG>(
)
let actions = useMemo<_Actions>(() => ({ showTooltip, hideTooltip }), [showTooltip, hideTooltip])
let render = useRender()
return (
<DescriptionProvider value={describedBy}>
<FloatingProvider>
@@ -395,6 +397,8 @@ function TriggerFn<TTag extends ElementType = typeof DEFAULT_TRIGGER_TAG>(
hoverProps
)
let render = useRender()
return render({
ourProps,
theirProps,
@@ -448,6 +452,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let slot = useMemo(() => ({}) satisfies PanelRenderPropArg, [])
let render = useRender()
return render({
ourProps: {
...ourProps,
@@ -29,7 +29,7 @@ import {
RenderStrategy,
compact,
forwardRefWithAs,
render,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
@@ -475,6 +475,8 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
if (transitionData.enter) openClosedState |= State.Opening
if (transitionData.leave) openClosedState |= State.Closing
let render = useRender()
return (
<NestingContext.Provider value={nesting}>
<OpenClosedProvider value={openClosedState}>
@@ -571,6 +573,8 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
props.beforeLeave?.()
})
let render = useRender()
return (
<NestingContext.Provider value={nestingBag}>
<TransitionContext.Provider value={transitionBag}>
@@ -1,6 +1,6 @@
import type { ElementType, Ref } from 'react'
import type { Props } from '../types'
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../utils/render'
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../utils/render'
let DEFAULT_VISUALLY_HIDDEN_TAG = 'span' as const
@@ -55,6 +55,8 @@ function VisuallyHidden<TTag extends ElementType = typeof DEFAULT_VISUALLY_HIDDE
},
}
let render = useRender()
return render({
ourProps,
theirProps,
@@ -2,7 +2,7 @@ import { getByTestId, prettyDOM, render as testRender } from '@testing-library/r
import React, { Fragment, createRef, type ElementType, type Ref } from 'react'
import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
import type { Expand, Props } from '../types'
import { RenderFeatures, render, type PropsForFeatures } from './render'
import { RenderFeatures, useRender, type PropsForFeatures } from './render'
function contents(id = 'wrapper') {
return prettyDOM(getByTestId(document.body, id), undefined, {
@@ -15,6 +15,7 @@ describe('Default functionality', () => {
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -31,6 +32,7 @@ describe('Default functionality', () => {
function DummyWithClassName<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ className: string | (() => string) }>
) {
let render = useRender()
return (
<div data-testid="wrapper-with-class">
{render({
@@ -97,6 +99,7 @@ describe('Default functionality', () => {
}
function OtherDummy<TTag extends ElementType = 'div'>(props: Props<TTag>) {
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -157,6 +160,7 @@ describe('Default functionality', () => {
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -179,6 +183,7 @@ describe('Default functionality', () => {
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -305,6 +310,7 @@ describe('Features.Static', () => {
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -380,6 +386,7 @@ describe('Features.RenderStrategy', () => {
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
let render = useRender()
return (
<div data-testid="wrapper">
{render({
@@ -408,6 +415,7 @@ describe('Features.Static | Features.RenderStrategy', () => {
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
let render = useRender()
return (
<div data-testid="wrapper">
{render({
+12 -3
View File
@@ -57,7 +57,16 @@ export type PropsForFeatures<T extends RenderFeatures> = Expand<
>
>
export function render<TFeature extends RenderFeatures, TTag extends ElementType, TSlot>({
export function useRender() {
let mergeRefs = useMergeRefsFn()
return useCallback(
(args: Parameters<typeof render>[0]) => render({ mergeRefs, ...args }),
[mergeRefs]
) as typeof render
}
function render<TFeature extends RenderFeatures, TTag extends ElementType, TSlot>({
ourProps,
theirProps,
slot,
@@ -77,7 +86,7 @@ export function render<TFeature extends RenderFeatures, TTag extends ElementType
visible?: boolean
name: string
mergeRefs?: ReturnType<typeof useMergeRefsFn>
}) {
}): ReturnType<typeof _render> | null {
mergeRefs = mergeRefs ?? defaultMergeRefs
let props = mergePropsAdvanced(theirProps, ourProps)
@@ -281,7 +290,7 @@ function _render<TTag extends ElementType, TSlot>(
* the `function` that updates these refs and can only do
* so once the ref that contains the list is updated.
*/
export function useMergeRefsFn() {
function useMergeRefsFn() {
type MaybeRef<T> = MutableRefObject<T> | ((value: T) => void) | null | undefined
let currentRefs = useRef<MaybeRef<any>[]>([])
let mergedRef = useCallback((value: any) => {