Skip to content

Commit

Permalink
Prefer incoming data-* attributes, over the ones set by Headless UI (
Browse files Browse the repository at this point in the history
…#3035)

* prefer `data-*` props that already exist

If the component already had `data-foo`, and we set `data-foo` then the
`data-foo` that was already there should stay.

* update changelog
  • Loading branch information
RobinMalfait authored Mar 14, 2024
1 parent 8a9867c commit 8c1c42b
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Switch />` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ exports[`Default functionality should forward all the props to the first child w
</div>"
`;

exports[`Default functionality should forward boolean values from \`slot\` as data attributes 1`] = `
"<div
data-testid=\\"wrapper\\"
>
<div
data-a=\\"\\"
data-c=\\"\\"
data-headlessui-state=\\"a c\\"
>
<span>
Contents
</span>
</div>
</div>"
`;

exports[`Default functionality should not error when we are rendering a Fragment with multiple children when we don't passthrough additional props 1`] = `
"<div
data-testid=\\"wrapper\\"
Expand All @@ -122,6 +138,19 @@ exports[`Default functionality should not error when we are rendering a Fragment
</div>"
`;

exports[`Default functionality should prefer user provided data attributes over the ones we set automatically 1`] = `
"<div
data-testid=\\"wrapper\\"
>
<span
data-accept=\\"always\\"
data-headlessui-state=\\"accept\\"
>
Contents
</span>
</div>"
`;

exports[`Features.RenderStrategy Hidden render strategy should be possible to render an \`unmount={false}\` dummy component (show = false) 1`] = `
"<div
data-testid=\\"wrapper\\"
Expand Down
44 changes: 44 additions & 0 deletions packages/@headlessui-react/src/utils/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,50 @@ describe('Default functionality', () => {
expect(contents()).toMatchSnapshot()
})

it('should forward boolean values from `slot` as data attributes', () => {
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: props,
slot: { a: true, b: false, c: true },
defaultTag: 'div',
name: 'Dummy',
})}
</div>
)
}

testRender(<Dummy>{() => <span>Contents</span>}</Dummy>)

expect(contents()).toMatchSnapshot()
})

it('should prefer user provided data attributes over the ones we set automatically', () => {
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: props,
slot: { accept: true },
defaultTag: 'div',
name: 'Dummy',
})}
</div>
)
}

testRender(<Dummy as={Fragment}>{() => <span data-accept="always">Contents</span>}</Dummy>)

expect(contents()).toMatchSnapshot()
})

it(
'should error when we are rendering a Fragment with multiple children',
suppressConsoleLogs(() => {
Expand Down
27 changes: 25 additions & 2 deletions packages/@headlessui-react/src/utils/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,35 @@ function _render<TTag extends ElementType, TSlot>(

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) },
Expand Down

0 comments on commit 8c1c42b

Please sign in to comment.