diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index a3bd3dd..8ce1ce1 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Attempt form submission when pressing `Enter` on `Checkbox` component ([#2962](https://github.com/tailwindlabs/headlessui/pull/2962))
- Allow setting custom `tabIndex` on the `` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))
- Forward `disabled` state to hidden inputs in form-like components ([#3004](https://github.com/tailwindlabs/headlessui/pull/3004))
+- Prefer incoming `data-*` attributes, over the ones set by Headless UI ([#3035](https://github.com/tailwindlabs/headlessui/pull/3035))
### Changed
diff --git a/packages/@headlessui-react/src/utils/__snapshots__/render.test.tsx.snap b/packages/@headlessui-react/src/utils/__snapshots__/render.test.tsx.snap
index ea55f73..fc97ee6 100644
--- a/packages/@headlessui-react/src/utils/__snapshots__/render.test.tsx.snap
+++ b/packages/@headlessui-react/src/utils/__snapshots__/render.test.tsx.snap
@@ -109,6 +109,22 @@ exports[`Default functionality should forward all the props to the first child w
"
`;
+exports[`Default functionality should forward boolean values from \`slot\` as data attributes 1`] = `
+"
"
+`;
+
exports[`Default functionality should not error when we are rendering a Fragment with multiple children when we don't passthrough additional props 1`] = `
""
`;
+exports[`Default functionality should prefer user provided data attributes over the ones we set automatically 1`] = `
+"
+
+ Contents
+
+
"
+`;
+
exports[`Features.RenderStrategy Hidden render strategy should be possible to render an \`unmount={false}\` dummy component (show = false) 1`] = `
"
{
expect(contents()).toMatchSnapshot()
})
+ it('should forward boolean values from `slot` as data attributes', () => {
+ function Dummy
(
+ props: Props & Partial<{ a: any; b: any; c: any }>
+ ) {
+ return (
+
+ {render({
+ ourProps: {},
+ theirProps: props,
+ slot: { a: true, b: false, c: true },
+ defaultTag: 'div',
+ name: 'Dummy',
+ })}
+
+ )
+ }
+
+ testRender({() => Contents})
+
+ expect(contents()).toMatchSnapshot()
+ })
+
+ it('should prefer user provided data attributes over the ones we set automatically', () => {
+ function Dummy(
+ props: Props & Partial<{ a: any; b: any; c: any }>
+ ) {
+ return (
+
+ {render({
+ ourProps: {},
+ theirProps: props,
+ slot: { accept: true },
+ defaultTag: 'div',
+ name: 'Dummy',
+ })}
+
+ )
+ }
+
+ testRender({() => Contents})
+
+ expect(contents()).toMatchSnapshot()
+ })
+
it(
'should error when we are rendering a Fragment with multiple children',
suppressConsoleLogs(() => {
diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts
index 56895f6..85357b8 100644
--- a/packages/@headlessui-react/src/utils/render.ts
+++ b/packages/@headlessui-react/src/utils/render.ts
@@ -213,12 +213,35 @@ function _render(
let classNameProps = newClassName ? { className: newClassName } : {}
+ // Merge props from the existing element with the incoming props
+ let mergedProps = mergePropsAdvanced(
+ resolvedChildren.props as any,
+ // Filter out undefined values so that they don't override the existing values
+ compact(omit(rest, ['ref']))
+ )
+
+ // Make sure that `data-*` that already exist in the `mergedProps` are
+ // skipped.
+ //
+ // Typically we want to keep the props we set in each component because
+ // they are required to make the component work correctly. However, in
+ // case of `data-*` attributes, these are attributes that help the end
+ // user.
+ //
+ // This means that since the props are not required for the component to
+ // work, that we can safely prefer the `data-*` attributes from the
+ // component that the end user provided.
+ for (let key in dataAttributes) {
+ if (key in mergedProps) {
+ delete dataAttributes[key]
+ }
+ }
+
return cloneElement(
resolvedChildren,
Object.assign(
{},
- // Filter out undefined values so that they don't override the existing values
- mergePropsAdvanced(resolvedChildren.props as any, compact(omit(rest, ['ref']))),
+ mergedProps,
dataAttributes,
refRelatedProps,
{ ref: mergeRefs((resolvedChildren as any).ref, refRelatedProps.ref) },