Repository: tailwindlabs/headlessui
Pull request #3312 (2024-06-21 13:29:30)

Remove transition state from render props
This PR makes sure that the transition data, such as `closed`, `enter`, `leave` and `transition` is only mapped as data attributes e.g.: `data-closed`.

This PR removes the information from the render prop.

```tsx
<Menu>
  <MenuItems>
    {({ ... /* Won't include `closed`, `enter`, `leave`, or `transition` */ }) => (
      <>
        <MenuItem as="a" href="#">
          Alice
        </MenuItem>
        <MenuItem as="a" href="#">
          Bob
        </MenuItem>
        <MenuItem as="a" href="#">
          Charlie
        </MenuItem>
      </>
    )}
  </MenuItems>
</Menu>
```


vercel[bot]: [vc]: #E7GnZPSDs5ws+rg/b8+F7UF97uBlJFNT+HAiuMKf8SQ=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0L0JKZjlORGQ0Z0h5YThyeVhaUFg5Wno5Tml5WG8iLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZmVhdC1yZW1vdmUtdHJhbnNpdGlvbi01YzU1NmMtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXJlYWN0LWdpdC1mZWF0LXJlbW92ZS10cmFuc2l0aW9uLTVjNTU2Yy10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGFpbHdpbmRsYWJzL2hlYWRsZXNzdWktdnVlL0VWZ01VSFV3VGdEWW16c1dpM0Q5Z3JwQ3NKcTkiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZlYXQtcmVtb3ZlLXRyYW5zaXRpb24tZC1lYzk4N2ItdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXZ1ZS1naXQtZmVhdC1yZW1vdmUtdHJhbnNpdGlvbi1kLWVjOTg3Yi10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/BJf9NDd4gHya8ryXZPX9Zz9NiyXo)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-react-git-feat-remove-transition-5c556c-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-feat-remove-transition-5c556c-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 21, 2024 1:29pm |
| **headlessui-vue** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/EVgMUHUwTgDYmzsWi3D9grpCsJq9)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-vue-git-feat-remove-transition-d-ec987b-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-feat-remove-transition-d-ec987b-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 21, 2024 1:29pm |




Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 72c1d6d..bf54840 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -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))
 
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index f89e514..ba62f11 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -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
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index a173ff6..66f5938 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -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 (
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 6f84675..2d551b8 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -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
diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx
index 7fb21a2..804e4e1 100644
--- a/packages/@headlessui-react/src/components/menu/menu.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.tsx
@@ -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 (
diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx
index 3c99182..c86ea41 100644
--- a/packages/@headlessui-react/src/components/popover/popover.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.tsx
@@ -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()
diff --git a/packages/@headlessui-react/src/components/transition/transition.tsx b/packages/@headlessui-react/src/components/transition/transition.tsx
index 9a7281c..d2d4189 100644
--- a/packages/@headlessui-react/src/components/transition/transition.tsx
+++ b/packages/@headlessui-react/src/components/transition/transition.tsx
@@ -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,
diff --git a/packages/@headlessui-react/src/hooks/use-transition.ts b/packages/@headlessui-react/src/hooks/use-transition.ts
index 7977184..42b3f57 100644
--- a/packages/@headlessui-react/src/hooks/use-transition.ts
+++ b/packages/@headlessui-react/src/hooks/use-transition.ts
@@ -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>,


Test patch:
diff --git a/packages/@headlessui-react/src/components/transition/__snapshots__/transition.test.tsx.snap b/packages/@headlessui-react/src/components/transition/__snapshots__/transition.test.tsx.snap
index 1bc2914..7d2fa9f 100644
--- a/packages/@headlessui-react/src/components/transition/__snapshots__/transition.test.tsx.snap
+++ b/packages/@headlessui-react/src/components/transition/__snapshots__/transition.test.tsx.snap
@@ -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=""
 >
diff --git a/packages/@headlessui-react/src/components/transition/transition.test.tsx b/packages/@headlessui-react/src/components/transition/transition.test.tsx
index 045f474..5911fbc 100644
--- a/packages/@headlessui-react/src/components/transition/transition.test.tsx
+++ b/packages/@headlessui-react/src/components/transition/transition.test.tsx
@@ -374,7 +374,6 @@ describe('Setup API', () => {
            <div
              class="foo1
          foo2 leave"
-             data-headlessui-state="leave transition"
              data-leave=""
              data-transition=""
              style=""