Remove transition state from render props (#3312)

* add function to map transition data to data attributes

* use transition data attributes in props

Instead of in the `slot` because this would also expose this information
as render props but we just want to set it as props without exposing it
as render props.

* rename `slot` to `transitionData` for consistency

* update changelog
This commit is contained in:
Robin Malfait
2024-06-21 15:55:06 +02:00
committed by GitHub
parent 1f1e290a3d
commit f1446664bc
10 changed files with 47 additions and 39 deletions
+1 -1
View File
@@ -10,7 +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 new data-attribute-based transition API ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285), [#3307](https://github.com/tailwindlabs/headlessui/pull/3307), [#3309](https://github.com/tailwindlabs/headlessui/pull/3309))
- Add new data-attribute-based transition API ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285), [#3307](https://github.com/tailwindlabs/headlessui/pull/3307), [#3309](https://github.com/tailwindlabs/headlessui/pull/3309), [#3312](https://github.com/tailwindlabs/headlessui/pull/3312))
- Add `DialogBackdrop` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307), [#3310](https://github.com/tailwindlabs/headlessui/pull/3310))
- Add `PopoverBackdrop` component to replace `PopoverOverlay` ([#3308](https://github.com/tailwindlabs/headlessui/pull/3308))
@@ -41,7 +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 { useTransition, type TransitionData } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { useWatch } from '../../hooks/use-watch'
import { useDisabled } from '../../internal/disabled'
@@ -1564,7 +1564,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
@@ -1665,9 +1665,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
return {
open: data.comboboxState === ComboboxState.Open,
option: undefined,
...transitionData,
} satisfies OptionsRenderPropArg
}, [data.comboboxState, transitionData])
}, [data.comboboxState])
// 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.
@@ -1706,6 +1705,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
} as CSSProperties,
onWheel: data.activationTrigger === ActivationTrigger.Pointer ? undefined : handleWheel,
onMouseDown: handleMouseDown,
...transitionDataAttributes(transitionData),
})
// We should freeze when the combobox is visible but "closed". This means that
@@ -24,7 +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 { useTransition, type TransitionData } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { CloseProvider } from '../../internal/close-provider'
import {
OpenClosedProvider,
@@ -425,7 +425,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
@@ -475,13 +475,13 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
return {
open: state.disclosureState === DisclosureStates.Open,
close,
...transitionData,
} satisfies PanelRenderPropArg
}, [state.disclosureState, close, transitionData])
}, [state.disclosureState, close])
let ourProps = {
ref: panelRef,
id,
...transitionDataAttributes(transitionData),
}
return (
@@ -41,7 +41,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 { useTransition, type TransitionData } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { useDisabled } from '../../internal/disabled'
import {
FloatingProvider,
@@ -870,7 +870,7 @@ let SelectedOptionContext = createContext(false)
let DEFAULT_OPTIONS_TAG = 'div' as const
type OptionsRenderPropArg = {
open: boolean
} & TransitionData
}
type OptionsPropsWeControl =
| 'aria-activedescendant'
| 'aria-labelledby'
@@ -1090,9 +1090,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
let slot = useMemo(() => {
return {
open: data.listboxState === ListboxStates.Open,
...transitionData,
} satisfies OptionsRenderPropArg
}, [data.listboxState, transitionData])
}, [data.listboxState])
let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
id,
@@ -1113,6 +1112,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
...style,
'--button-width': useElementSize(data.buttonRef, true).width,
} as CSSProperties,
...transitionDataAttributes(transitionData),
})
// We should freeze when the listbox is visible but "closed". This means that
@@ -37,7 +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 { useTransition, type TransitionData } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import {
FloatingProvider,
@@ -565,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
@@ -760,9 +760,8 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
let slot = useMemo(() => {
return {
open: state.menuState === MenuStates.Open,
...transitionData,
} satisfies ItemsRenderPropArg
}, [state.menuState, transitionData])
}, [state.menuState])
let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
'aria-activedescendant':
@@ -782,6 +781,7 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
...style,
'--button-width': useElementSize(state.buttonRef, true).width,
} as CSSProperties,
...transitionDataAttributes(transitionData),
})
return (
@@ -36,7 +36,7 @@ import { useMainTreeNode, useRootContainers } from '../../hooks/use-root-contain
import { useScrollLock } from '../../hooks/use-scroll-lock'
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
import { Direction as TabDirection, useTabDirection } from '../../hooks/use-tab-direction'
import { useTransition, type TransitionData } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { CloseProvider } from '../../internal/close-provider'
import {
FloatingProvider,
@@ -732,7 +732,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
let DEFAULT_BACKDROP_TAG = 'div' as const
type BackdropRenderPropArg = {
open: boolean
} & TransitionData
}
type BackdropPropsWeControl = 'aria-hidden'
let BackdropRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
@@ -778,15 +778,15 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
let slot = useMemo(() => {
return {
open: popoverState === PopoverStates.Open,
...transitionData,
} satisfies BackdropRenderPropArg
}, [popoverState, transitionData])
}, [popoverState])
let ourProps = {
ref: backdropRef,
id,
'aria-hidden': true,
onClick: handleClick,
...transitionDataAttributes(transitionData),
}
return render({
@@ -806,7 +806,7 @@ let DEFAULT_PANEL_TAG = 'div' as const
type PanelRenderPropArg = {
open: boolean
close: (focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => void
} & TransitionData
}
let PanelRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
@@ -936,9 +936,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
return {
open: state.popoverState === PopoverStates.Open,
close,
...transitionData,
} satisfies PanelRenderPropArg
}, [state.popoverState, close, transitionData])
}, [state.popoverState, close])
let ourProps: Record<string, any> = mergeProps(anchor ? getFloatingPanelProps() : {}, {
ref: panelRef,
@@ -968,6 +967,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
...style,
'--button-width': useElementSize(state.button, true).width,
} as React.CSSProperties,
...transitionDataAttributes(transitionData),
})
let direction = useTabDirection()
@@ -132,7 +132,6 @@ exports[`Setup API transition classes should be possible to passthrough the tran
class="enter enter-from"
data-closed=""
data-enter=""
data-headlessui-state="closed enter transition"
data-transition=""
style=""
>
@@ -374,7 +374,6 @@ describe('Setup API', () => {
<div
class="foo1
foo2 leave"
data-headlessui-state="leave transition"
data-leave=""
data-transition=""
style=""
@@ -20,7 +20,7 @@ import { useLatestValue } from '../../hooks/use-latest-value'
import { useOnDisappear } from '../../hooks/use-on-disappear'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useTransition } from '../../hooks/use-transition'
import { transitionDataAttributes, useTransition } from '../../hooks/use-transition'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import type { Props, ReactTag } from '../../types'
import { classNames } from '../../utils/class-names'
@@ -437,7 +437,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
// a leave transition on the `<Transition>` is done, but there is still a
// child `<TransitionChild>` busy, then `visible` would be `false`, while
// `state` would still be `TreeStates.Visible`.
let [, slot] = useTransition(enabled, container, show, { start, end })
let [, transitionData] = useTransition(enabled, container, show, { start, end })
let ourProps = compact({
ref: transitionRef,
@@ -451,25 +451,26 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
immediate && enterFrom,
// Map data attributes to `enter`, `enterFrom` and `enterTo` classes
slot.enter && enter,
slot.enter && slot.closed && enterFrom,
slot.enter && !slot.closed && enterTo,
transitionData.enter && enter,
transitionData.enter && transitionData.closed && enterFrom,
transitionData.enter && !transitionData.closed && enterTo,
// Map data attributes to `leave`, `leaveFrom` and `leaveTo` classes
slot.leave && leave,
slot.leave && !slot.closed && leaveFrom,
slot.leave && slot.closed && leaveTo,
transitionData.leave && leave,
transitionData.leave && !transitionData.closed && leaveFrom,
transitionData.leave && transitionData.closed && leaveTo,
// Map data attributes to `entered` class (backwards compatibility)
!slot.transition && show && entered
!transitionData.transition && show && entered
)?.trim() || undefined, // If `className` is an empty string, we can omit it
...transitionDataAttributes(transitionData),
})
let openClosedState = 0
if (state === TreeStates.Visible) openClosedState |= State.Open
if (state === TreeStates.Hidden) openClosedState |= State.Closed
if (slot.enter) openClosedState |= State.Opening
if (slot.leave) openClosedState |= State.Closing
if (transitionData.enter) openClosedState |= State.Opening
if (transitionData.leave) openClosedState |= State.Closing
return (
<NestingContext.Provider value={nesting}>
@@ -477,7 +478,6 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_TRANSITION_CHILD_TAG,
features: TransitionChildRenderFeatures,
visible: state === TreeStates.Visible,
@@ -32,13 +32,23 @@ enum TransitionState {
Leave = 1 << 2,
}
export type TransitionData = {
type TransitionData = {
closed?: boolean
enter?: boolean
leave?: boolean
transition?: boolean
}
export function transitionDataAttributes(data: TransitionData) {
let attributes: Record<string, string> = {}
for (let key in data) {
if (data[key as keyof TransitionData] === true) {
attributes[`data-${key}`] = ''
}
}
return attributes
}
export function useTransition(
enabled: boolean,
elementRef: MutableRefObject<HTMLElement | null>,