Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure `element` in `ref` callback is always connected when rendering in a `Portal` ([#3789](https://github.com/tailwindlabs/headlessui/pull/3789))
- Ensure form state is up to date when using uncontrolled components ([#3790](https://github.com/tailwindlabs/headlessui/pull/3790))
- Ensure `data-open` on `ComboboxInput` is up to date ([#3791](https://github.com/tailwindlabs/headlessui/pull/3791))
- Ensure changing the `immediate` prop value on the `Combobox` component works as expected ([#3792](https://github.com/tailwindlabs/headlessui/pull/3792))

## [2.2.7] - 2025-07-30

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 @@ -2,8 +2,9 @@

import { useFocusRing } from '@react-aria/focus'
import { useHover } from '@react-aria/interactions'
import { useMemo, type ElementType, type Ref } from 'react'
import { type ElementType, type Ref } from 'react'
import { useActivePress } from '../../hooks/use-active-press'
import { useSlot } from '../../hooks/use-slot'
import { useDisabled } from '../../internal/disabled'
import type { Props } from '../../types'
import {
Expand Down Expand Up @@ -59,9 +60,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
pressProps
)

let slot = useMemo(() => {
return { disabled, hover, focus, active, autofocus: autoFocus } satisfies ButtonRenderPropArg
}, [disabled, hover, focus, active, autoFocus])
let slot = useSlot<ButtonRenderPropArg>({ disabled, hover, focus, active, autofocus: autoFocus })

let render = useRender()

Expand Down
24 changes: 11 additions & 13 deletions packages/@headlessui-react/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useFocusRing } from '@react-aria/focus'
import { useHover } from '@react-aria/interactions'
import React, {
useCallback,
useMemo,
useState,
type ElementType,
type KeyboardEvent as ReactKeyboardEvent,
Expand All @@ -17,6 +16,7 @@ import { useDefaultValue } from '../../hooks/use-default-value'
import { useDisposables } from '../../hooks/use-disposables'
import { useEvent } from '../../hooks/use-event'
import { useId } from '../../hooks/use-id'
import { useSlot } from '../../hooks/use-slot'
import { useDisabled } from '../../internal/disabled'
import { FormFields } from '../../internal/form-fields'
import { useProvidedId } from '../../internal/id'
Expand Down Expand Up @@ -159,18 +159,16 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
pressProps
)

let slot = useMemo(() => {
return {
checked,
disabled,
hover,
focus,
active,
indeterminate,
changing,
autofocus: autoFocus,
} satisfies CheckboxRenderPropArg
}, [checked, indeterminate, disabled, hover, focus, active, changing, autoFocus])
let slot = useSlot<CheckboxRenderPropArg>({
checked,
disabled,
hover,
focus,
active,
indeterminate,
changing,
autofocus: autoFocus,
})

let reset = useCallback(() => {
if (defaultChecked === undefined) return
Expand Down
88 changes: 43 additions & 45 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { Action as QuickReleaseAction, useQuickRelease } from '../../hooks/use-q
import { useRefocusableInput } from '../../hooks/use-refocusable-input'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useScrollLock } from '../../hooks/use-scroll-lock'
import { useSlot } from '../../hooks/use-slot'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
Expand Down Expand Up @@ -369,17 +370,20 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
onClose,
}),
[
__demoMode,
immediate,
optionsPropsRef,
value,
defaultValue,
disabled,
invalid,
multiple,
theirOnChange,
isSelected,
__demoMode,
machine,
virtual,
virtualSlice,
theirOnChange,
isSelected,
calculateIndex,
compare,
onClose,
]
)
Expand Down Expand Up @@ -418,16 +422,14 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
let activeOptionIndex = useSlice(machine, machine.selectors.activeOptionIndex)
let activeOption = useSlice(machine, machine.selectors.activeOption)

let slot = useMemo(() => {
return {
open: comboboxState === ComboboxState.Open,
disabled,
invalid,
activeIndex: activeOptionIndex,
activeOption,
value,
} satisfies ComboboxRenderPropArg<unknown>
}, [data, disabled, value, invalid, activeOption, comboboxState])
let slot = useSlot<ComboboxRenderPropArg<unknown>>({
open: comboboxState === ComboboxState.Open,
disabled,
invalid,
activeIndex: activeOptionIndex,
activeOption,
value,
})

let [labelledby, LabelProvider] = useLabels()

Expand Down Expand Up @@ -903,12 +905,14 @@ function InputFn<

let optionsElement = useSlice(machine, (state) => state.optionsElement)

let open = comboboxState === ComboboxState.Open
let invalid = data.invalid
let autofocus = autoFocus
let slot = useMemo(() => {
return { open, disabled, invalid, hover, focus, autofocus } satisfies InputRenderPropArg
}, [open, disabled, invalid, hover, focus, autofocus])
let slot = useSlot<InputRenderPropArg>({
open: comboboxState === ComboboxState.Open,
disabled,
invalid: data.invalid,
hover,
focus,
autofocus: autoFocus,
})

let ourProps = mergeProps(
{
Expand Down Expand Up @@ -1116,17 +1120,15 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
let { pressed: active, pressProps } = useActivePress({ disabled })

let slot = useMemo(() => {
return {
open: comboboxState === ComboboxState.Open,
active: active || comboboxState === ComboboxState.Open,
disabled,
invalid: data.invalid,
value: data.value,
hover,
focus,
} satisfies ButtonRenderPropArg
}, [data, hover, focus, active, disabled, comboboxState])
let slot = useSlot<ButtonRenderPropArg>({
open: comboboxState === ComboboxState.Open,
active: active || comboboxState === ComboboxState.Open,
disabled,
invalid: data.invalid,
value: data.value,
hover,
focus,
})
let ourProps = mergeProps(
{
ref: buttonRef,
Expand Down Expand Up @@ -1294,12 +1296,10 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(

let labelledBy = useLabelledBy([buttonElement?.id])

let slot = useMemo(() => {
return {
open: comboboxState === ComboboxState.Open,
option: undefined,
} satisfies OptionsRenderPropArg
}, [comboboxState])
let slot = useSlot<OptionsRenderPropArg>({
open: comboboxState === ComboboxState.Open,
option: undefined,
})

// When the user scrolls **using the mouse** (so scroll event isn't appropriate)
// we want to make sure that the current activation trigger is set to pointer.
Expand Down Expand Up @@ -1580,14 +1580,12 @@ function OptionFn<
machine.actions.goToOption({ focus: Focus.Nothing })
})

let slot = useMemo(() => {
return {
active,
focus: active,
selected,
disabled,
} satisfies OptionRenderPropArg
}, [active, selected, disabled])
let slot = useSlot<OptionRenderPropArg>({
active,
focus: active,
selected,
disabled,
})

let ourProps = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import { useFocusRing } from '@react-aria/focus'
import { useHover } from '@react-aria/interactions'
import { Fragment, useMemo, type ElementType, type Ref } from 'react'
import { Fragment, type ElementType, type Ref } from 'react'
import { useActivePress } from '../../hooks/use-active-press'
import { useSlot } from '../../hooks/use-slot'
import type { Props } from '../../types'
import {
forwardRefWithAs,
Expand Down Expand Up @@ -42,10 +43,7 @@ function DataInteractiveFn<TTag extends ElementType = typeof DEFAULT_DATA_INTERA

let ourProps = mergeProps({ ref }, focusProps, hoverProps, pressProps)

let slot = useMemo(
() => ({ hover, focus, active }) satisfies DataInteractiveRenderPropArg,
[hover, focus, active]
)
let slot = useSlot<DataInteractiveRenderPropArg>({ hover, focus, active })

let render = useRender()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
import { useEvent } from '../../hooks/use-event'
import { useId } from '../../hooks/use-id'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useSlot } from '../../hooks/use-slot'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useDisabled } from '../../internal/disabled'
import type { Props } from '../../types'
Expand Down Expand Up @@ -117,8 +118,7 @@ function DescriptionFn<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG

useIsoMorphicEffect(() => context.register(id), [id, context.register])

let disabled = providedDisabled || false
let slot = useMemo(() => ({ ...context.slot, disabled }), [context.slot, disabled])
let slot = useSlot({ ...context.slot, disabled: providedDisabled || false })
let ourProps = { ref: descriptionRef, ...context.props, id }

let render = useRender()
Expand Down
23 changes: 6 additions & 17 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '../../hooks/use-root-containers'
import { useScrollLock } from '../../hooks/use-scroll-lock'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { useSlot } from '../../hooks/use-slot'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { CloseProvider } from '../../internal/close-provider'
import { ResetOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
Expand Down Expand Up @@ -274,13 +275,10 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<

let contextBag = useMemo<ContextType<typeof DialogContext>>(
() => [{ dialogState, close, setTitleId, unmount }, state],
[dialogState, state, close, setTitleId, unmount]
[dialogState, close, setTitleId, unmount, state]
)

let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies DialogRenderPropArg,
[dialogState]
)
let slot = useSlot<DialogRenderPropArg>({ open: dialogState === DialogStates.Open })

let ourProps = {
ref: dialogRef,
Expand Down Expand Up @@ -455,10 +453,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let [{ dialogState, unmount }, state] = useDialogContext('Dialog.Panel')
let panelRef = useSyncRefs(ref, state.panelRef)

let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies PanelRenderPropArg,
[dialogState]
)
let slot = useSlot<PanelRenderPropArg>({ open: dialogState === DialogStates.Open })

// Prevent the click events inside the Dialog.Panel from bubbling through the React Tree which
// could submit wrapping <form> elements even if we portalled the Dialog.
Expand Down Expand Up @@ -511,10 +506,7 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
let { transition = false, ...theirProps } = props
let [{ dialogState, unmount }] = useDialogContext('Dialog.Backdrop')

let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg,
[dialogState]
)
let slot = useSlot<BackdropRenderPropArg>({ open: dialogState === DialogStates.Open })

let ourProps = { ref, 'aria-hidden': true }

Expand Down Expand Up @@ -563,10 +555,7 @@ function TitleFn<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
return () => setTitleId(null)
}, [id, setTitleId])

let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies TitleRenderPropArg,
[dialogState]
)
let slot = useSlot<TitleRenderPropArg>({ open: dialogState === DialogStates.Open })

let ourProps = { ref: titleRef, id }

Expand Down
39 changes: 17 additions & 22 deletions packages/@headlessui-react/src/components/disclosure/disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useActivePress } from '../../hooks/use-active-press'
import { useEvent } from '../../hooks/use-event'
import { useId } from '../../hooks/use-id'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useSlot } from '../../hooks/use-slot'
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { CloseProvider } from '../../internal/close-provider'
Expand Down Expand Up @@ -225,12 +226,10 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(

let api = useMemo<ContextType<typeof DisclosureAPIContext>>(() => ({ close }), [close])

let slot = useMemo(() => {
return {
open: disclosureState === DisclosureStates.Open,
close,
} satisfies DisclosureRenderPropArg
}, [disclosureState, close])
let slot = useSlot<DisclosureRenderPropArg>({
open: disclosureState === DisclosureStates.Open,
close,
})

let ourProps = {
ref: disclosureRef,
Expand Down Expand Up @@ -371,16 +370,14 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
let { pressed: active, pressProps } = useActivePress({ disabled })

let slot = useMemo(() => {
return {
open: state.disclosureState === DisclosureStates.Open,
hover,
active,
disabled,
focus,
autofocus: autoFocus,
} satisfies ButtonRenderPropArg
}, [state, hover, active, focus, disabled, autoFocus])
let slot = useSlot<ButtonRenderPropArg>({
open: state.disclosureState === DisclosureStates.Open,
hover,
active,
disabled,
focus,
autofocus: autoFocus,
})

let type = useResolveButtonType(props, state.buttonElement)
let ourProps = isWithinPanel
Expand Down Expand Up @@ -487,12 +484,10 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
: state.disclosureState === DisclosureStates.Open
)

let slot = useMemo(() => {
return {
open: state.disclosureState === DisclosureStates.Open,
close,
} satisfies PanelRenderPropArg
}, [state.disclosureState, close])
let slot = useSlot<PanelRenderPropArg>({
open: state.disclosureState === DisclosureStates.Open,
close,
})

let ourProps = {
ref: panelRef,
Expand Down
Loading