Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new data-attribute-based transition API #3273

Merged
merged 10 commits into from
Jun 11, 2024
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
### Added

- Add ability to render multiple `<Dialog />` components at once (without nesting them) ([#3242](https://github.com/tailwindlabs/headlessui/pull/3242))
- Add CSS based transitions using `data-*` attributes ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273))

### Fixed

Expand Down
27 changes: 16 additions & 11 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useScrollLock } from '../../hooks/use-scroll-lock'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
import { useTransitionData, type TransitionData } from '../../hooks/use-transition-data'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { useWatch } from '../../hooks/use-watch'
import { useDisabled } from '../../internal/disabled'
Expand Down Expand Up @@ -446,6 +447,7 @@ type _Actions = ReturnType<typeof useActions>
let VirtualContext = createContext<Virtualizer<any, any> | null>(null)

function VirtualProvider(props: {
slot: OptionsRenderPropArg
children: (data: { option: unknown; open: boolean }) => React.ReactElement
}) {
let data = useData('VirtualProvider')
Expand Down Expand Up @@ -523,8 +525,8 @@ function VirtualProvider(props: {
<Fragment key={item.key}>
{React.cloneElement(
props.children?.({
...props.slot,
option: options[item.index],
open: data.comboboxState === ComboboxState.Open,
}),
{
key: `${baseKey}-${item.key}`,
Expand Down Expand Up @@ -1561,7 +1563,7 @@ let DEFAULT_OPTIONS_TAG = 'div' as const
type OptionsRenderPropArg = {
open: boolean
option: unknown
}
} & TransitionData
type OptionsPropsWeControl = 'aria-labelledby' | 'aria-multiselectable' | 'role' | 'tabIndex'

let OptionsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
Expand All @@ -1575,6 +1577,7 @@ export type ComboboxOptionsProps<TTag extends ElementType = typeof DEFAULT_OPTIO
anchor?: AnchorProps
portal?: boolean
modal?: boolean
transition?: boolean
}
>

Expand All @@ -1589,6 +1592,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
anchor: rawAnchor,
portal = false,
modal = true,
transition = false,
...theirProps
} = props
let data = useData('Combobox.Options')
Expand All @@ -1606,13 +1610,13 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
let ownerDocument = useOwnerDocument(data.optionsRef)

let usesOpenClosedState = useOpenClosed()
let visible = (() => {
if (usesOpenClosedState !== null) {
return (usesOpenClosedState & State.Open) === State.Open
}

return data.comboboxState === ComboboxState.Open
})()
let [visible, transitionData] = useTransitionData(
transition,
data.optionsRef,
usesOpenClosedState !== null
? (usesOpenClosedState & State.Open) === State.Open
: data.comboboxState === ComboboxState.Open
)

// Ensure we close the combobox as soon as the input becomes hidden
useOnDisappear(visible, data.inputRef, actions.closeCombobox)
Expand Down Expand Up @@ -1660,8 +1664,9 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
return {
open: data.comboboxState === ComboboxState.Open,
option: undefined,
...transitionData,
} satisfies OptionsRenderPropArg
}, [data])
}, [data.comboboxState, transitionData])

// 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 @@ -1706,7 +1711,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
if (data.virtual && visible) {
Object.assign(theirProps, {
// @ts-expect-error The `children` prop now is a callback function that receives `{ option }`.
children: <VirtualProvider>{theirProps.children}</VirtualProvider>,
children: <VirtualProvider slot={slot}>{theirProps.children}</VirtualProvider>,
})
}

Expand Down
28 changes: 17 additions & 11 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 { useEvent } from '../../hooks/use-event'
import { useId } from '../../hooks/use-id'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
import { useTransitionData, type TransitionData } from '../../hooks/use-transition-data'
import { CloseProvider } from '../../internal/close-provider'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import type { Props } from '../../types'
Expand Down Expand Up @@ -419,7 +420,7 @@ let DEFAULT_PANEL_TAG = 'div' as const
type PanelRenderPropArg = {
open: boolean
close: (focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => void
}
} & TransitionData
type DisclosurePanelPropsWeControl = never

let PanelRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
Expand All @@ -428,15 +429,19 @@ export type DisclosurePanelProps<TTag extends ElementType = typeof DEFAULT_PANEL
TTag,
PanelRenderPropArg,
DisclosurePanelPropsWeControl,
PropsForFeatures<typeof PanelRenderFeatures>
{ transition?: boolean } & PropsForFeatures<typeof PanelRenderFeatures>
>

function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
props: DisclosurePanelProps<TTag>,
ref: Ref<HTMLElement>
) {
let internalId = useId()
let { id = `headlessui-disclosure-panel-${internalId}`, ...theirProps } = props
let {
id = `headlessui-disclosure-panel-${internalId}`,
transition = false,
...theirProps
} = props
let [state, dispatch] = useDisclosureContext('Disclosure.Panel')
let { close } = useDisclosureAPIContext('Disclosure.Panel')
let mergeRefs = useMergeRefsFn()
Expand All @@ -453,20 +458,21 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
}, [id, dispatch])

let usesOpenClosedState = useOpenClosed()
let visible = (() => {
if (usesOpenClosedState !== null) {
return (usesOpenClosedState & State.Open) === State.Open
}

return state.disclosureState === DisclosureStates.Open
})()
let [visible, transitionData] = useTransitionData(
transition,
state.panelRef,
usesOpenClosedState !== null
? (usesOpenClosedState & State.Open) === State.Open
: state.disclosureState === DisclosureStates.Open
)

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

let ourProps = {
ref: panelRef,
Expand Down
29 changes: 17 additions & 12 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ 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 { useTransitionData, type TransitionData } from '../../hooks/use-transition-data'
import { useDisabled } from '../../internal/disabled'
import {
FloatingProvider,
Expand Down Expand Up @@ -863,7 +864,7 @@ let SelectedOptionContext = createContext(false)
let DEFAULT_OPTIONS_TAG = 'div' as const
type OptionsRenderPropArg = {
open: boolean
}
} & TransitionData
type OptionsPropsWeControl =
| 'aria-activedescendant'
| 'aria-labelledby'
Expand All @@ -882,6 +883,7 @@ export type ListboxOptionsProps<TTag extends ElementType = typeof DEFAULT_OPTION
anchor?: AnchorPropsWithSelection
portal?: boolean
modal?: boolean
transition?: boolean
} & PropsForFeatures<typeof OptionsRenderFeatures>
>

Expand All @@ -895,6 +897,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
anchor: rawAnchor,
portal = false,
modal = true,
transition = false,
...theirProps
} = props
let anchor = useResolvedAnchor(rawAnchor)
Expand All @@ -910,13 +913,13 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
let ownerDocument = useOwnerDocument(data.optionsRef)

let usesOpenClosedState = useOpenClosed()
let visible = (() => {
if (usesOpenClosedState !== null) {
return (usesOpenClosedState & State.Open) === State.Open
}

return data.listboxState === ListboxStates.Open
})()
let [visible, transitionData] = useTransitionData(
transition,
data.optionsRef,
usesOpenClosedState !== null
? (usesOpenClosedState & State.Open) === State.Open
: data.listboxState === ListboxStates.Open
)

// Ensure we close the listbox as soon as the button becomes hidden
useOnDisappear(visible, data.buttonRef, actions.closeListbox)
Expand Down Expand Up @@ -1073,10 +1076,12 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
})

let labelledby = useComputed(() => data.buttonRef.current?.id, [data.buttonRef.current])
let slot = useMemo(
() => ({ open: data.listboxState === ListboxStates.Open }) satisfies OptionsRenderPropArg,
[data]
)
let slot = useMemo(() => {
return {
open: data.listboxState === ListboxStates.Open,
...transitionData,
} satisfies OptionsRenderPropArg
}, [data.listboxState, transitionData])

let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
id,
Expand Down
32 changes: 18 additions & 14 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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 { useTransitionData, type TransitionData } from '../../hooks/use-transition-data'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import {
FloatingProvider,
Expand Down Expand Up @@ -564,7 +565,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
let DEFAULT_ITEMS_TAG = 'div' as const
type ItemsRenderPropArg = {
open: boolean
}
} & TransitionData
type ItemsPropsWeControl = 'aria-activedescendant' | 'aria-labelledby' | 'role' | 'tabIndex'

let ItemsRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
Expand All @@ -577,6 +578,7 @@ export type MenuItemsProps<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>
anchor?: AnchorProps
portal?: boolean
modal?: boolean
transition?: boolean

// ItemsRenderFeatures
static?: boolean
Expand All @@ -594,6 +596,7 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
anchor: rawAnchor,
portal = false,
modal = true,
transition = false,
...theirProps
} = props
let anchor = useResolvedAnchor(rawAnchor)
Expand All @@ -608,16 +611,14 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
portal = true
}

let searchDisposables = useDisposables()

let usesOpenClosedState = useOpenClosed()
let visible = (() => {
if (usesOpenClosedState !== null) {
return (usesOpenClosedState & State.Open) === State.Open
}

return state.menuState === MenuStates.Open
})()
let [visible, transitionData] = useTransitionData(
transition,
state.itemsRef,
usesOpenClosedState !== null
? (usesOpenClosedState & State.Open) === State.Open
: state.menuState === MenuStates.Open
)

// Ensure we close the menu as soon as the button becomes hidden
useOnDisappear(visible, state.buttonRef, () => {
Expand Down Expand Up @@ -671,6 +672,7 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
},
})

let searchDisposables = useDisposables()
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLElement>) => {
searchDisposables.dispose()

Expand Down Expand Up @@ -755,10 +757,12 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
}
})

let slot = useMemo(
() => ({ open: state.menuState === MenuStates.Open }) satisfies ItemsRenderPropArg,
[state]
)
let slot = useMemo(() => {
return {
open: state.menuState === MenuStates.Open,
...transitionData,
} satisfies ItemsRenderPropArg
}, [state, transitionData])

let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
'aria-activedescendant':
Expand Down
Loading
Loading