Skip to content

Commit

Permalink
Fix crash in ListboxOptions when using as={Fragment} (#3513)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RobinMalfait authored Oct 9, 2024
1 parent 3b047fc commit a4953a2
Show file tree
Hide file tree
Showing 27 changed files with 164 additions and 48 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 3 additions & 4 deletions packages/@headlessui-react/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
Expand Down Expand Up @@ -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 })
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { attemptSubmit } from '../../utils/form'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
Expand Down Expand Up @@ -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 && (
Expand Down
15 changes: 11 additions & 4 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -1444,6 +1445,8 @@ function InputFn<
hoverProps
)
let render = useRender()
return render({
ourProps,
theirProps,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1610,8 +1612,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)

let render = useRender()

return render({
mergeRefs,
ourProps,
theirProps,
slot,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2037,6 +2042,8 @@ function OptionFn<
onMouseLeave: handleLeave,
}

let render = useRender()

return render({
ourProps,
theirProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
Expand Down Expand Up @@ -47,6 +47,8 @@ function DataInteractiveFn<TTag extends ElementType = typeof DEFAULT_DATA_INTERA
[hover, focus, active]
)

let render = useRender()

return render({
ourProps,
theirProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

// ---

Expand Down Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { match } from '../../utils/match'
import {
RenderFeatures,
forwardRefWithAs,
render,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
Expand Down Expand Up @@ -286,6 +286,8 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
}
}

let render = useRender()

return (
<ResetOpenClosedProvider>
<ForcePortalRoot force={true}>
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ import {
RenderFeatures,
forwardRefWithAs,
mergeProps,
render,
useMergeRefsFn,
useRender,
type HasDisplayName,
type PropsForFeatures,
type RefProp,
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -411,8 +411,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)

let render = useRender()

return render({
mergeRefs,
ourProps,
theirProps,
slot,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/@headlessui-react/src/components/field/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -197,6 +197,8 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
},
}

let render = useRender()

return (
<>
{tabLockEnabled && (
Expand Down
4 changes: 3 additions & 1 deletion packages/@headlessui-react/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Props } from '../../types'
import {
forwardRefWithAs,
mergeProps,
render,
useRender,
type HasDisplayName,
type RefProp,
} from '../../utils/render'
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/@headlessui-react/src/components/label/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

// ---

Expand Down Expand Up @@ -203,6 +203,8 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
}
}

let render = useRender()

return render({
ourProps,
theirProps,
Expand Down
Loading

0 comments on commit a4953a2

Please sign in to comment.