diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx
index 14a2d3109..bb68b31a6 100644
--- a/packages/@headlessui-react/src/components/menu/menu.test.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx
@@ -76,6 +76,30 @@ describe('Safe guards', () => {
describe('Rendering', () => {
describe('Menu', () => {
+ it(
+ 'should not crash when Menu renders as a Fragment with multiple children and data attributes from router tooling',
+ suppressConsoleLogs(async () => {
+ render(
+
+ )
+
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
+
+ await click(getMenuButton())
+
+ assertMenuButton({ state: MenuState.Visible })
+ assertMenu({ state: MenuState.Visible })
+ })
+ )
+
it(
'should be possible to render a Menu using a render prop',
suppressConsoleLogs(async () => {
diff --git a/packages/@headlessui-react/src/utils/render.test.tsx b/packages/@headlessui-react/src/utils/render.test.tsx
index b6fc15268..4b8c95c98 100644
--- a/packages/@headlessui-react/src/utils/render.test.tsx
+++ b/packages/@headlessui-react/src/utils/render.test.tsx
@@ -244,6 +244,25 @@ describe('Default functionality', () => {
expect(contents()).toMatchSnapshot()
})
+ it('should forward data attributes to multiple children when rendering a Fragment', () => {
+ testRender(
+
+
+ Contents A
+
+
+ Contents B
+
+
+ )
+
+ expect(getByTestId(document.body, 'child-a')).toHaveAttribute(
+ 'data-tsd-source',
+ 'src/routes/index.tsx:1:1'
+ )
+ expect(getByTestId(document.body, 'child-b')).toHaveAttribute('data-tsd-source', 'child-owned')
+ })
+
it(
'should error when we are applying props to a Fragment when we do not have a dedicated element',
suppressConsoleLogs(() => {
diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts
index dfb47687d..817b30df6 100644
--- a/packages/@headlessui-react/src/utils/render.ts
+++ b/packages/@headlessui-react/src/utils/render.ts
@@ -182,20 +182,47 @@ function _render(
}
if (isFragment(Component)) {
- if (Object.keys(compact(rest)).length > 0 || Object.keys(compact(dataAttributes)).length > 0) {
+ let passthroughProps = compact(rest)
+ let passthroughDataAttributes = mergeDataAttributeProps(
+ compact(dataAttributes),
+ pickDataAttributes(passthroughProps)
+ )
+ let passthroughNonDataProps = omit(
+ passthroughProps,
+ Object.keys(pickDataAttributes(passthroughProps))
+ )
+
+ if (Object.keys(passthroughDataAttributes).length > 0) {
+ let childrenWithDataAttributes = cloneChildrenWithDataAttributes(
+ resolvedChildren,
+ passthroughDataAttributes
+ )
+
+ if (
+ childrenWithDataAttributes !== null &&
+ Object.keys(passthroughNonDataProps).length === 0
+ ) {
+ return childrenWithDataAttributes
+ }
+ }
+
+ if (
+ Object.keys(passthroughProps).length > 0 ||
+ Object.keys(compact(dataAttributes)).length > 0
+ ) {
if (
!isValidElement(resolvedChildren) ||
(Array.isArray(resolvedChildren) && resolvedChildren.length > 1) ||
isFragmentInstance(resolvedChildren)
) {
- if (Object.keys(compact(rest)).length > 0) {
+ if (Object.keys(passthroughProps).length > 0) {
throw new Error(
[
'Passing props on "Fragment"!',
'',
`The current component <${name} /> is rendering a "Fragment".`,
`However we need to passthrough the following props:`,
- Object.keys(compact(rest))
+ Object.keys(passthroughProps)
.concat(Object.keys(compact(dataAttributes)))
.map((line) => ` - ${line}`)
.join('\n'),
@@ -462,6 +489,62 @@ function omit>(object: T, keysToOmit: string[] = [])
return clone
}
+function pickDataAttributes>(object: T) {
+ return Object.fromEntries(Object.entries(object).filter(([key]) => key.startsWith('data-')))
+}
+
+function mergeDataAttributeProps, U extends Record>(
+ primaryDataAttributes: T,
+ secondaryDataAttributes: U
+) {
+ return Object.assign({}, primaryDataAttributes, secondaryDataAttributes)
+}
+
+function cloneChildrenWithDataAttributes(
+ children: ReactElement | ReactElement[],
+ dataAttributes: Record
+): ReactElement | ReactElement[] | null {
+ if (Array.isArray(children)) {
+ let clonedChildren = children.map((child) =>
+ cloneChildWithDataAttributes(child, dataAttributes)
+ )
+
+ return clonedChildren.every((child) => child !== null)
+ ? (clonedChildren as ReactElement[])
+ : null
+ }
+
+ return cloneChildWithDataAttributes(children, dataAttributes)
+}
+
+function cloneChildWithDataAttributes(
+ child: ReactElement,
+ dataAttributes: Record
+): ReactElement | null {
+ if (!isValidElement(child)) return null
+
+ if (isFragmentInstance(child)) {
+ let clonedChildren = React.Children.map(child.props.children, (nestedChild) => {
+ if (!isValidElement(nestedChild)) return null
+
+ return cloneChildWithDataAttributes(nestedChild, dataAttributes)
+ })
+
+ if (clonedChildren?.some((nestedChild) => nestedChild === null)) return null
+
+ return cloneElement(child, undefined, clonedChildren)
+ }
+
+ let childDataAttributes = {}
+
+ for (let key in dataAttributes) {
+ if (key in child.props) continue
+ childDataAttributes[key] = dataAttributes[key]
+ }
+
+ return cloneElement(child, childDataAttributes)
+}
+
function getElementRef(element: React.ReactElement) {
// @ts-expect-error
return React.version.split('.')[0] >= '19' ? element.props.ref : element.ref