diff --git a/apps/playground/app/test-tabnav/(accounts)/page.tsx b/apps/playground/app/test-tabnav/(accounts)/page.tsx
new file mode 100644
index 00000000..7888ce51
--- /dev/null
+++ b/apps/playground/app/test-tabnav/(accounts)/page.tsx
@@ -0,0 +1,5 @@
+import { Heading } from '@radix-ui/themes';
+
+export default function Accounts() {
+ return Accounts;
+}
diff --git a/apps/playground/app/test-tabnav/documents/page.tsx b/apps/playground/app/test-tabnav/documents/page.tsx
new file mode 100644
index 00000000..c37d1d33
--- /dev/null
+++ b/apps/playground/app/test-tabnav/documents/page.tsx
@@ -0,0 +1,5 @@
+import { Heading } from '@radix-ui/themes';
+
+export default function Documents() {
+ return Documents;
+}
diff --git a/apps/playground/app/test-tabnav/layout.tsx b/apps/playground/app/test-tabnav/layout.tsx
new file mode 100644
index 00000000..89cf1f67
--- /dev/null
+++ b/apps/playground/app/test-tabnav/layout.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { Theme, Container, Section, Box } from '@radix-ui/themes';
+import { NextThemeProvider } from '../next-theme-provider';
+import { Nav } from './nav';
+
+export default function Test({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/playground/app/test-tabnav/nav.tsx b/apps/playground/app/test-tabnav/nav.tsx
new file mode 100644
index 00000000..1d195712
--- /dev/null
+++ b/apps/playground/app/test-tabnav/nav.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import NextLink from 'next/link';
+import { usePathname } from 'next/navigation';
+import { TabNavRoot, TabNavLink, Heading, Flex, Text } from '@radix-ui/themes';
+
+export function Nav() {
+ const pathname = usePathname();
+ return (
+
+
+ Straight up `TabNavLink`
+
+
+ Accounts
+
+
+ Documents
+
+
+ Settings
+
+
+
+
+
+ {``} with `NextLink`
+
+
+ Accounts
+
+
+ Documents
+
+
+ Settings
+
+
+
+
+
+ {``} with `TabNavLink`
+
+
+ Accounts
+
+
+ Documents
+
+
+ Settings
+
+
+
+
+ );
+}
diff --git a/apps/playground/app/test-tabnav/settings/page.tsx b/apps/playground/app/test-tabnav/settings/page.tsx
new file mode 100644
index 00000000..c86dd578
--- /dev/null
+++ b/apps/playground/app/test-tabnav/settings/page.tsx
@@ -0,0 +1,5 @@
+import { Heading } from '@radix-ui/themes';
+
+export default function Settings() {
+ return Settings;
+}
diff --git a/packages/radix-ui-themes/src/components/avatar.tsx b/packages/radix-ui-themes/src/components/avatar.tsx
index ce900bfd..2a96581e 100644
--- a/packages/radix-ui-themes/src/components/avatar.tsx
+++ b/packages/radix-ui-themes/src/components/avatar.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import classNames from 'classnames';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { avatarPropDefs } from './avatar.props.js';
-import { extractProps, getRoot } from '../helpers/index.js';
+import { extractProps, getSubtree } from '../helpers/index.js';
import { marginPropDefs } from '../props/index.js';
import type { ComponentPropsWithoutColor } from '../helpers/index.js';
@@ -12,32 +12,23 @@ import type { MarginProps, GetPropDefTypes } from '../props/index.js';
interface AvatarProps extends MarginProps, AvatarImplProps {}
const Avatar = React.forwardRef((props, forwardedRef) => {
- const {
- asChild,
- children: childrenProp,
- className,
- style,
- color,
- radius,
- ...imageProps
- } = extractProps(props, avatarPropDefs, marginPropDefs);
-
- const { Root: AvatarRoot } = getRoot({
- asChild,
- children: childrenProp,
- parent: AvatarPrimitive.Root,
- });
+ const { asChild, children, className, style, color, radius, ...imageProps } = extractProps(
+ props,
+ avatarPropDefs,
+ marginPropDefs
+ );
return (
// TODO as a rule, should we rather spread the props on root?
-
-
-
+ {getSubtree({ asChild, children }, )}
+
);
});
Avatar.displayName = 'Avatar';
diff --git a/packages/radix-ui-themes/src/components/card.tsx b/packages/radix-ui-themes/src/components/card.tsx
index 15b5e10e..fd5dcca3 100644
--- a/packages/radix-ui-themes/src/components/card.tsx
+++ b/packages/radix-ui-themes/src/components/card.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { Slot } from '@radix-ui/react-slot';
import { cardPropDefs } from './card.props.js';
-import { extractProps, getRoot } from '../helpers/index.js';
+import { extractProps } from '../helpers/index.js';
import { marginPropDefs } from '../props/index.js';
import type { ComponentPropsWithoutColor } from '../helpers/index.js';
diff --git a/packages/radix-ui-themes/src/components/container.tsx b/packages/radix-ui-themes/src/components/container.tsx
index 8d5a0794..78b2a534 100644
--- a/packages/radix-ui-themes/src/components/container.tsx
+++ b/packages/radix-ui-themes/src/components/container.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { Slot } from '@radix-ui/react-slot';
import { containerPropDefs } from './container.props.js';
-import { extractProps, getRoot } from '../helpers/index.js';
+import { extractProps, getSubtree } from '../helpers/index.js';
import {
deprecatedLayoutPropDefs,
heightPropDefs,
@@ -23,12 +23,7 @@ interface ContainerProps
ContainerOwnProps {}
const Container = React.forwardRef(
({ width, minWidth, maxWidth, ...props }, forwardedRef) => {
- const {
- asChild,
- children: childrenProp,
- className,
- ...containerProps
- } = extractProps(
+ const { asChild, children, className, ...containerProps } = extractProps(
props,
containerPropDefs,
layoutPropDefs,
@@ -42,22 +37,20 @@ const Container = React.forwardRef(
heightPropDefs
);
- const { Root: ContainerRoot, children } = getRoot({
- asChild,
- children: childrenProp,
- parent: asChild ? Slot : 'div',
- });
+ const Comp = asChild ? Slot : 'div';
return (
-
-
- {children}
-
-
+ {getSubtree({ asChild, children }, (children) => (
+
+ {children}
+
+ ))}
+
);
}
);
diff --git a/packages/radix-ui-themes/src/components/scroll-area.tsx b/packages/radix-ui-themes/src/components/scroll-area.tsx
index 0535a137..a695b8db 100644
--- a/packages/radix-ui-themes/src/components/scroll-area.tsx
+++ b/packages/radix-ui-themes/src/components/scroll-area.tsx
@@ -8,8 +8,8 @@ import {
extractMarginProps,
getMarginStyles,
getResponsiveClassNames,
- getRoot,
mergeStyles,
+ getSubtree,
} from '../helpers/index.js';
import type { ComponentPropsWithoutColor } from '../helpers/index.js';
@@ -28,7 +28,7 @@ const ScrollArea = React.forwardRef((props,
const {
asChild,
- children: childrenProp,
+ children,
className,
style,
type,
@@ -40,66 +40,66 @@ const ScrollArea = React.forwardRef((props,
...viewportProps
} = marginRest;
- const { Root: ScrollAreaRoot, children } = getRoot({
- asChild,
- children: childrenProp,
- parent: ScrollAreaPrimitive.Root,
- });
-
return (
-
-
- {children}
-
-
+ {getSubtree({ asChild, children }, (children) => (
+ <>
+
+ {children}
+
+
+
- {scrollbars !== 'vertical' ? (
-
-
-
- ) : null}
+ {scrollbars !== 'vertical' ? (
+
+
+
+ ) : null}
- {scrollbars !== 'horizontal' ? (
-
-
-
- ) : null}
+ {scrollbars !== 'horizontal' ? (
+
+
+
+ ) : null}
- {scrollbars === 'both' ? (
-
- ) : null}
-
+ {scrollbars === 'both' ? (
+
+ ) : null}
+ >
+ ))}
+
);
});
ScrollArea.displayName = 'ScrollArea';
diff --git a/packages/radix-ui-themes/src/components/tab-nav.tsx b/packages/radix-ui-themes/src/components/tab-nav.tsx
index 70002e6c..86c5dd37 100644
--- a/packages/radix-ui-themes/src/components/tab-nav.tsx
+++ b/packages/radix-ui-themes/src/components/tab-nav.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import classNames from 'classnames';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import { tabNavLinkPropDefs, tabNavPropDefs } from './tab-nav.props.js';
-import { extractProps, getRoot } from '../helpers/index.js';
+import { extractProps, getSubtree } from '../helpers/index.js';
import { marginPropDefs } from '../props/index.js';
import type { ComponentPropsWithoutColor } from '../helpers/index.js';
@@ -46,27 +46,26 @@ interface TabNavLinkProps
extends Omit, 'onSelect'>,
TabNavLinkOwnProps {}
const TabNavLink = React.forwardRef((props, forwardedRef) => {
- const { asChild, className, children: childrenProp, ...linkProps } = props;
-
- const { Root: TabNavLinkRoot, children } = getRoot({
- asChild,
- children: childrenProp,
- parent: NavigationMenu.Link,
- });
+ const { asChild, children, className, ...linkProps } = props;
return (
- {}}
+ asChild={asChild}
>
- {children}
-
- {children}
-
-
+ {getSubtree({ asChild, children }, (children) => (
+ <>
+ {children}
+
+ {children}
+
+ >
+ ))}
+
);
});
diff --git a/packages/radix-ui-themes/src/helpers/get-root.tsx b/packages/radix-ui-themes/src/helpers/get-root.tsx
deleted file mode 100644
index 15b43eef..00000000
--- a/packages/radix-ui-themes/src/helpers/get-root.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as React from 'react';
-import { Slot } from '@radix-ui/react-slot';
-
-interface GetChildrenArgs {
- asChild: boolean | undefined;
- children: React.ReactNode;
- parent: T;
-}
-
-export const getRoot = <
- T extends React.ComponentType | keyof JSX.IntrinsicElements,
- U extends React.ComponentProps extends { asChild?: boolean } ? T : Exclude
->({
- asChild,
- children,
- parent: Parent,
-}: GetChildrenArgs): {
- Root: U;
- children: React.ReactNode;
-} => {
- if (asChild) {
- let child = React.Children.only(children) as React.ReactElement;
- const grandChildren = child.props.children;
- return {
- Root: ((props) => {
- child = React.cloneElement(child, {
- children: props.children,
- });
-
- // Make sure we don't pass `asChild` to DOM elements
- if ((Parent as unknown) === Slot) {
- return {child};
- }
-
- return (
-
- {child}
-
- );
- }) as U,
- children: grandChildren,
- };
- }
-
- return {
- Root: Parent as unknown as U,
- children: children,
- };
-};
diff --git a/packages/radix-ui-themes/src/helpers/get-subtree.ts b/packages/radix-ui-themes/src/helpers/get-subtree.ts
new file mode 100644
index 00000000..51595eba
--- /dev/null
+++ b/packages/radix-ui-themes/src/helpers/get-subtree.ts
@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+/**
+ * This is a helper function that is used when a component supports `asChild`
+ * using the `Slot` component but its implementation contains nested DOM elements.
+ *
+ * Using it ensures if a consumer uses the `asChild` prop, the elements are in
+ * correct order in the DOM, adopting the intended consumer `children`.
+ */
+export function getSubtree(
+ options: { asChild: boolean | undefined; children: React.ReactNode },
+ content: React.ReactNode | ((children: React.ReactNode) => React.ReactNode)
+) {
+ const { asChild, children } = options;
+ if (!asChild) return typeof content === 'function' ? content(children) : content;
+
+ const firstChild = React.Children.only(children) as React.ReactElement;
+ return React.cloneElement(firstChild, {
+ children: typeof content === 'function' ? content(firstChild.props.children) : content,
+ });
+}
diff --git a/packages/radix-ui-themes/src/helpers/index.ts b/packages/radix-ui-themes/src/helpers/index.ts
index 8197e388..3ee36f7e 100644
--- a/packages/radix-ui-themes/src/helpers/index.ts
+++ b/packages/radix-ui-themes/src/helpers/index.ts
@@ -5,7 +5,7 @@ export * from './extract-props.js';
export * from './get-margin-styles.js';
export * from './get-matching-gray-color.js';
export * from './get-responsive-styles.js';
-export * from './get-root.js';
+export * from './get-subtree.js';
export * from './has-own-property.js';
export * from './input-attributes.js';
export * from './is-responsive-object.js';