From 3870709db787698246b2bb95f52267683055cad0 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 25 Aug 2026 10:32:45 +1000 Subject: [PATCH 1/5] fix: NavigationTree updates from testing --- packages/dev/s2-docs/src/ComponentCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev/s2-docs/src/ComponentCard.tsx b/packages/dev/s2-docs/src/ComponentCard.tsx index 9e4bc0d261f..975f89cffa5 100644 --- a/packages/dev/s2-docs/src/ComponentCard.tsx +++ b/packages/dev/s2-docs/src/ComponentCard.tsx @@ -268,6 +268,7 @@ const componentIllustrations: Record = { Meter: [MeterLight, MeterDark], 'Migrating to Spectrum 2': [MigratingLight, MigratingDark], Modal: [DialogLight, DialogDark], + NavigationTree: [SideNavLight, SideNavDark], NumberField: [NumberFieldLight, NumberFieldDark], Picker: [PickerLight, PickerDark], Popover: [PopoverLight, PopoverDark], From b30dc947164030f47f7b3a8083a2aff47358c65d Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 25 Aug 2026 12:52:44 +1000 Subject: [PATCH 2/5] add starter stories and test, fix auto scroll, expand non-href, indicator width, css, structure --- .../pages/react-aria/NavigationTree.mdx | 146 ++++++++++++++---- .../src/NavigationTree.tsx | 27 +++- .../test/NavigationTree.test.tsx | 32 ++++ starters/docs/src/NavigationTree.css | 35 ++--- starters/docs/src/NavigationTree.tsx | 41 ++--- .../docs/stories/NavigationTree.stories.tsx | 117 ++++++++++++++ starters/tailwind/src/NavigationTree.tsx | 108 ++++++++----- .../stories/NavigationTree.stories.tsx | 115 ++++++++++++++ 8 files changed, 497 insertions(+), 124 deletions(-) create mode 100644 starters/docs/stories/NavigationTree.stories.tsx create mode 100644 starters/tailwind/stories/NavigationTree.stories.tsx diff --git a/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx b/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx index b2ccc1a31de..6f09b88cf36 100644 --- a/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx +++ b/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx @@ -17,20 +17,49 @@ export const description = 'A navigation component that displays a nested, hiera ```tsx render docs={docs.exports.NavigationTree} links={docs.links} props={[]} type="vanilla" files={["starters/docs/src/NavigationTree.tsx", "starters/docs/src/NavigationTree.css", "packages/dev/s2-docs/pages/react-aria/RoutedNavigationTree.tsx"]} "use client"; - import {NavigationTree, NavigationTreeItem} from 'vanilla-starter/NavigationTree'; + import {NavigationTree, NavigationTreeItem, NavigationTreeItemContent, NavigationTreeItemLink} from 'vanilla-starter/NavigationTree'; + import {Button} from 'vanilla-starter/Button'; + import {MoreHorizontal} from 'lucide-react'; import {RoutedNavigationTree} from './RoutedNavigationTree'; {({selectedRoute}) => ( - - - - + + + Home + + - - - + + + Files + + + + Photos + + + + + Videos + + + + + + Shared + + + + Food + + + + + Drinks + + )} @@ -39,20 +68,49 @@ export const description = 'A navigation component that displays a nested, hiera ```tsx render docs={docs.exports.NavigationTree} links={docs.links} props={[]} type="tailwind" files={["starters/tailwind/src/NavigationTree.tsx", "packages/dev/s2-docs/pages/react-aria/RoutedNavigationTree.tsx"]} "use client"; - import {NavigationTree, NavigationTreeItem} from 'tailwind-starter/NavigationTree'; + import {NavigationTree, NavigationTreeItem, NavigationTreeItemContent, NavigationTreeItemLink} from 'tailwind-starter/NavigationTree'; + import {Button} from 'tailwind-starter/Button'; + import {MoreHorizontal} from 'lucide-react'; import {RoutedNavigationTree} from './RoutedNavigationTree'; {({selectedRoute}) => ( - - - - + + + Home + + + + + + Files + + + + Photos + + + + + Videos + + - - - + + + Shared + + + + Food + + + + + Drinks + + )} @@ -72,7 +130,7 @@ export const description = 'A navigation component that displays a nested, hiera ```tsx render files={["packages/dev/s2-docs/pages/react-aria/RoutedNavigationTree.tsx"]} "use client"; -import {NavigationTree, NavigationTreeItem} from 'vanilla-starter/NavigationTree'; +import {NavigationTree, NavigationTreeItem, NavigationTreeItemContent, NavigationTreeItemLink} from 'vanilla-starter/NavigationTree'; import {RoutedNavigationTree} from './RoutedNavigationTree'; function Example() { @@ -87,7 +145,13 @@ function Example() { {({selectedRoute}) => ( /*- begin highlight -*/ - {item => } + {item => ( + + + {item.label} + + + )} /*- end highlight -*/ )} @@ -102,7 +166,7 @@ Use `NavigationTreeSection` to group related items, with an optional `Navigation ```tsx render files={["packages/dev/s2-docs/pages/react-aria/RoutedNavigationTree.tsx"]} "use client"; -import {NavigationTree, NavigationTreeItem, NavigationTreeSection, NavigationTreeHeader} from 'vanilla-starter/NavigationTree'; +import {NavigationTree, NavigationTreeItem, NavigationTreeItemContent, NavigationTreeItemLink, NavigationTreeSection, NavigationTreeHeader} from 'vanilla-starter/NavigationTree'; import {RoutedNavigationTree} from './RoutedNavigationTree'; @@ -111,14 +175,30 @@ import {RoutedNavigationTree} from './RoutedNavigationTree'; {/*- begin highlight -*/} Personal - - + + + Home + + + + + Starred + + {/*- end highlight -*/} Projects - - + + + Apollo + + + + + Gemini + + )} @@ -133,7 +213,7 @@ Combine `NavigationTree` with a client side router by wrapping your app in a [Ro ```tsx render "use client"; -import {NavigationTree, NavigationTreeItem} from 'vanilla-starter/NavigationTree'; +import {NavigationTree, NavigationTreeItem, NavigationTreeItemContent, NavigationTreeItemLink} from 'vanilla-starter/NavigationTree'; import {RouterProvider} from 'react-aria-components'; import {useState} from 'react'; @@ -144,9 +224,21 @@ function Example() { {/*- end highlight -*/} - - - + + + Inbox + + + + + Drafts + + + + + Sent + + ); diff --git a/packages/react-aria-components/src/NavigationTree.tsx b/packages/react-aria-components/src/NavigationTree.tsx index df406d98431..3c7c3227b99 100644 --- a/packages/react-aria-components/src/NavigationTree.tsx +++ b/packages/react-aria-components/src/NavigationTree.tsx @@ -60,6 +60,8 @@ interface InternalNavigationTreeContextValue { selectedRoute?: string | null; /** The last route the focused key was synced to; dedupes the focus sync across items. */ syncedRouteRef?: RefObject; + /** The NavigationTree's root element, used to keep current-route scrolling inside the tree. */ + treeRef?: RefObject; } const InternalNavigationTreeContext = createContext({}); @@ -130,13 +132,17 @@ export const NavigationTree = /*#__PURE__*/ (forwardRef as forwardRefType)(funct [props, ref] = useContextProps(props, ref, NavigationTreeContext); let {className, style, children, selectedRoute, ...rest} = props; let syncedRouteRef = useRef(undefined); - let context = useMemo(() => ({selectedRoute, syncedRouteRef}), [selectedRoute]); + let treeRef = useObjectRef(ref); + let context = useMemo( + () => ({selectedRoute, syncedRouteRef, treeRef}), + [selectedRoute, treeRef] + ); return ( 0; let isCurrent = hasLink && href === selectedRoute; let [isLinkFocused, setLinkFocused] = useState(false); @@ -232,9 +238,15 @@ export const NavigationTreeItem = /*#__PURE__*/ (forwardRef as forwardRefType)( useEffect(() => { if (isCurrent && objRef.current) { let scrollParent = getScrollParent(objRef.current, true) as HTMLElement; - scrollIntoView(scrollParent, objRef.current, {block: 'center'}); + // Only scroll the tree's own scroll container into view — never an outer ancestor such as the + // page. When the tree isn't its own scroll container the nearest scroll parent is outside the + // tree, so we skip rather than hijack the surrounding scroll position. + let treeRoot = treeRef?.current; + if (treeRoot && treeRoot.contains(scrollParent)) { + scrollIntoView(scrollParent, objRef.current, {block: 'center'}); + } } - }, [isCurrent, objRef]); + }, [isCurrent, objRef, treeRef]); return ( state.toggleKey(id) : undefined }; return {renderChildren}; } diff --git a/packages/react-aria-components/test/NavigationTree.test.tsx b/packages/react-aria-components/test/NavigationTree.test.tsx index b8a02e71539..df0d8f2482b 100644 --- a/packages/react-aria-components/test/NavigationTree.test.tsx +++ b/packages/react-aria-components/test/NavigationTree.test.tsx @@ -112,6 +112,26 @@ function NoLinkActionMenuNavigationTreeExample(props: Partial>) { + return ( + + + + Section + + + + + Section 2 + + + + + ); +} + // Section 1 has an aria-label instead of a NavigationTreeHeader (as is allowed by the underlying // TreeSection); Section 2 uses a NavigationTreeHeader so both default classes get covered. function SectionNavigationTreeExample(props: Partial>) { @@ -348,6 +368,18 @@ describe('NavigationTree', () => { expect(within(sectionRow).getByRole('button', {name: 'More actions'})).not.toHaveFocus(); }); + it('toggles expansion when pressing the label (a no-href Link) of a parent row', async () => { + let {getByRole} = render(); + let sectionRow = getByRole('row', {name: 'Section'}); + expect(sectionRow).toHaveAttribute('aria-expanded', 'false'); + + await user.click(within(sectionRow).getByRole('link', {name: 'Section'})); + expect(sectionRow).toHaveAttribute('aria-expanded', 'true'); + + await user.click(within(sectionRow).getByRole('link', {name: 'Section'})); + expect(sectionRow).toHaveAttribute('aria-expanded', 'false'); + }); + it('arrow left from a deep leaf steps to parent, collapses it, then moves to the grandparent', async () => { let {getByRole, queryByRole} = render( {({hasChildItems}: NavigationTreeItemContentRenderProps) => ( <> - {/* The label is rendered as a Link so it becomes the row's focusable child. It picks up - * href + aria-current automatically from the NavigationTree. Rows without an href render as a - * span instead of an anchor. */} - {props.children} - {hasChildItems && ( - - )} + {props.children} + )} ); } -export interface NavigationTreeItemProps extends Partial { - title?: React.ReactNode; -} - -export function NavigationTreeItem(props: NavigationTreeItemProps) { - let textValue = typeof props.title === 'string' ? props.title : ''; - return ( - - {props.title != null ? ( - <> - {props.title} - {props.children} - - ) : ( - props.children - )} - - ); +export function NavigationTreeItem(props: AriaNavigationTreeItemProps) { + return ; } export function NavigationTreeSection(props: NavigationTreeSectionProps) { @@ -68,3 +51,7 @@ export function NavigationTreeSection(props: NavigationTreeSec export function NavigationTreeHeader(props: NavigationTreeHeaderProps) { return ; } + +export function NavigationTreeItemLink(props: LinkProps) { + return ; +} diff --git a/starters/docs/stories/NavigationTree.stories.tsx b/starters/docs/stories/NavigationTree.stories.tsx new file mode 100644 index 00000000000..fb4e05485ca --- /dev/null +++ b/starters/docs/stories/NavigationTree.stories.tsx @@ -0,0 +1,117 @@ +import {Button} from '../src/Button'; +import type {Meta, StoryFn} from '@storybook/react'; +import {MoreHorizontal} from 'lucide-react'; +import { + NavigationTree, + NavigationTreeItem, + NavigationTreeItemContent, + NavigationTreeItemLink, + NavigationTreeSection, + NavigationTreeHeader +} from '../src/NavigationTree'; +import React, {type ReactNode, useState} from 'react'; +import {RouterProvider} from 'react-aria-components'; + +const meta: Meta = { + component: NavigationTree, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +}; + +export default meta; + +type Story = StoryFn; + +function RoutedNavigationTree(props: { + children: ({selectedRoute}: {selectedRoute: string}) => ReactNode, + defaultSelectedRoute: string +}) { + let [selectedRoute, setSelectedRoute] = useState(props.defaultSelectedRoute); + return {props.children({selectedRoute})}; +} + +export const Example: Story = args => ( + + {({selectedRoute}) => ( + + + + Home + + + + + + Files + + + + Photos + + + + + Videos + + + + + + Shared + + + + Food + + + + + Drinks + + + + + )} + +); + +export const Sections: Story = args => ( + + {({selectedRoute}) => ( + + + Personal + + + Home + + + + + Starred + + + + + Projects + + + Apollo + + + + + Gemini + + + + + )} + +); diff --git a/starters/tailwind/src/NavigationTree.tsx b/starters/tailwind/src/NavigationTree.tsx index 76f390a9f44..5de8c19c43c 100644 --- a/starters/tailwind/src/NavigationTree.tsx +++ b/starters/tailwind/src/NavigationTree.tsx @@ -2,6 +2,7 @@ import { Button, Link, + type LinkProps, NavigationTree as AriaNavigationTree, NavigationTreeHeader as AriaNavigationTreeHeader, type NavigationTreeHeaderProps, @@ -31,8 +32,8 @@ export function NavigationTree({children, ...props}: NavigationTreeProps) } // The focus ring lives on the row (not the link) so it spans the whole item. Hover/current/ancestor -// state is surfaced as a leading-edge indicator on the Link (see linkStyles) rather than a full-row -// background. The row is a `group` so the Link can react to the row's data-* attributes. isFocusVisible +// state is surfaced as a leading-edge indicator (see indicatorStyles) rather than a full-row +// background. The row is a `group` so the indicator can react to the row's data-* attributes. isFocusVisible // comes from the render props; RAC's isFocusVisible already follows the link (it is not true when // another child, e.g. a button, is focused). const itemStyles = tv({ @@ -45,28 +46,11 @@ const itemStyles = tv({ } }); -// A single `before` pseudo-element on the Link is the leading-edge indicator; the row's data-* -// attributes (via `group-[...]`) decide how it looks so the three states never overlap: -// - Hover pill (neutral, full height): only rows that render as a link (`data-href`) light up on -// hover, matching RAC's data-hovered on actionable rows. -// - Current pill (blue, full height): the selected row, but only when it is not also hovered, so -// hovering the selected row shows the neutral hover pill instead (never both). -// - Ancestor dot (neutral, short): a collapsed ancestor of the current route shows a small dot in -// the same spot. It shortens the height (higher specificity than the base) and shares the neutral -// color with hover, so hovering a collapsed ancestor still reads as the dot, not a full pill. -// Works for ancestors that render as a link or a plain span since it keys off the row, not the element. const linkStyles = tv({ base: 'relative flex-1 min-w-0 flex items-center gap-2 py-1.5 px-2 text-sm no-underline text-current cursor-pointer outline-none ' + // A row without an href renders its label as a non-interactive span, so it should not look clickable. - 'group-[:not([data-href])]:cursor-default ' + - "before:content-[''] before:absolute before:start-0.5 before:top-1/2 before:h-[1lh] before:w-1 before:-translate-y-1/2 before:rounded-full before:forced-color-adjust-none " + - 'group-[[data-hovered][data-href]]:before:bg-neutral-400 dark:group-[[data-hovered][data-href]]:before:bg-neutral-500 ' + - 'group-[[data-current]:not([data-hovered])]:before:bg-blue-600 dark:group-[[data-current]:not([data-hovered])]:before:bg-blue-400 ' + - 'group-[[data-current-ancestor]:not([data-expanded])]:before:h-1 group-[[data-current-ancestor]:not([data-expanded])]:before:bg-neutral-400 dark:group-[[data-current-ancestor]:not([data-expanded])]:before:bg-neutral-500 ' + - // In forced-colors mode authored backgrounds are dropped, so whenever the indicator is showing - // (any of the three states above) render it as the system Highlight color instead. - 'forced-colors:group-[:is([data-hovered][data-href],[data-current]:not([data-hovered]),[data-current-ancestor]:not([data-expanded]))]:before:bg-[Highlight]', + 'group-[:not([data-href])]:cursor-default', variants: { isDisabled: { true: 'cursor-default' @@ -74,6 +58,24 @@ const linkStyles = tv({ } }); +// The leading-edge indicator is a real, presentational element (aria-hidden). Its variant is chosen in +// JS (see NavigationTreeItemContent) and surfaced as a single `data-indicator` attribute, so the styles +// stay flat and precedence lives in one place instead of relying on CSS specificity: +// - current (blue pill): the selected row. Because the variant is picked in JS, it keeps this color +// even while hovered. +// - ancestor (neutral dot): a collapsed ancestor of the current route. +// - hover (neutral pill): any actionable row on hover. +const indicatorStyles = tv({ + base: + 'absolute start-0.5 top-1/2 -translate-y-1/2 w-0.5 h-[1lh] rounded-full forced-color-adjust-none ' + + // Only real link rows (the group row has data-href) get the hover pill. + 'group-[[data-href]]:data-[indicator=hover]:bg-neutral-400 dark:group-[[data-href]]:data-[indicator=hover]:bg-neutral-500 ' + + 'data-[indicator=current]:bg-blue-600 dark:data-[indicator=current]:bg-blue-400 ' + + 'data-[indicator=ancestor]:h-1 data-[indicator=ancestor]:w-1 data-[indicator=ancestor]:bg-neutral-400 dark:data-[indicator=ancestor]:bg-neutral-500 ' + + // In forced-colors mode authored backgrounds are dropped, so render any visible state as Highlight. + 'forced-colors:data-[indicator]:bg-[Highlight]' +}); + const expandButton = tv({ extend: focusRing, base: 'shrink-0 w-6 h-6 flex items-center justify-center rounded-md border-0 p-0 bg-transparent cursor-default [-webkit-tap-highlight-color:transparent] -outline-offset-2' @@ -88,25 +90,45 @@ const chevron = tv({ } }); +const NavTreeLinkContext = React.createContext<{ + indicator: 'current' | 'ancestor' | 'hover' | undefined; +}>({indicator: undefined}); + export function NavigationTreeItemContent(props: {children?: React.ReactNode}) { return ( - {({level, hasChildItems, isDisabled, isExpanded}) => ( - <> - {level > 1 && ( -
- )} - {props.children} - {hasChildItems && ( - - )} - - )} + + ); + }} ); } @@ -116,13 +138,7 @@ export interface NavigationTreeItemProps extends Partial - {props.title} - {props.children} - - ); + return ; } export function NavigationTreeSection(props: NavigationTreeSectionProps) { @@ -137,3 +153,13 @@ export function NavigationTreeHeader(props: NavigationTreeHeaderProps) { /> ); } + +export function NavigationTreeItemLink(props: LinkProps) { + let {indicator} = React.useContext(NavTreeLinkContext); + return ( + linkStyles({isDisabled})}> + + {props.children} + + ); +} diff --git a/starters/tailwind/stories/NavigationTree.stories.tsx b/starters/tailwind/stories/NavigationTree.stories.tsx new file mode 100644 index 00000000000..a8e47b26c1c --- /dev/null +++ b/starters/tailwind/stories/NavigationTree.stories.tsx @@ -0,0 +1,115 @@ +import {Button} from '../src/Button'; +import {type Meta} from '@storybook/react'; +import {MoreHorizontal} from 'lucide-react'; +import { + NavigationTree, + NavigationTreeItem, + NavigationTreeItemContent, + NavigationTreeItemLink, + NavigationTreeSection, + NavigationTreeHeader +} from '../src/NavigationTree'; +import React, {type ReactNode, useState} from 'react'; +import {RouterProvider} from 'react-aria-components'; + +const meta: Meta = { + component: NavigationTree, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +}; + +export default meta; + +function RoutedNavigationTree(props: { + children: ({selectedRoute}: {selectedRoute: string}) => ReactNode, + defaultSelectedRoute: string +}) { + let [selectedRoute, setSelectedRoute] = useState(props.defaultSelectedRoute); + return {props.children({selectedRoute})}; +} + +export const Example = (args: any) => ( + + {({selectedRoute}) => ( + + + + Home + + + + + + Files + + + + Photos + + + + + Videos + + + + + + Shared + + + + Food + + + + + Drinks + + + + + )} + +); + +export const Sections = (args: any) => ( + + {({selectedRoute}) => ( + + + Personal + + + Home + + + + + Starred + + + + + Projects + + + Apollo + + + + + Gemini + + + + + )} + +); From ea88e173aafd95cd5f8697a42a2af4ccedc3988d Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 25 Aug 2026 12:59:19 +1000 Subject: [PATCH 3/5] fix lint and tests --- .../react-aria-components/src/NavigationTree.tsx | 8 +++----- starters/docs/stories/NavigationTree.stories.tsx | 12 ++++++++---- starters/tailwind/src/NavigationTree.tsx | 15 ++++++++------- .../tailwind/stories/NavigationTree.stories.tsx | 12 ++++++++---- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/react-aria-components/src/NavigationTree.tsx b/packages/react-aria-components/src/NavigationTree.tsx index 3c7c3227b99..ee9cbf66e14 100644 --- a/packages/react-aria-components/src/NavigationTree.tsx +++ b/packages/react-aria-components/src/NavigationTree.tsx @@ -22,6 +22,7 @@ import { import {Collection, forwardRefType, Key, Node, RouterOptions} from '@react-types/shared'; import {getScrollParent} from 'react-aria/private/utils/getScrollParent'; import {LinkContext} from './Link'; +import {nodeContains} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import React, { createContext, ForwardedRef, @@ -133,10 +134,7 @@ export const NavigationTree = /*#__PURE__*/ (forwardRef as forwardRefType)(funct let {className, style, children, selectedRoute, ...rest} = props; let syncedRouteRef = useRef(undefined); let treeRef = useObjectRef(ref); - let context = useMemo( - () => ({selectedRoute, syncedRouteRef, treeRef}), - [selectedRoute, treeRef] - ); + let context = useMemo(() => ({selectedRoute, syncedRouteRef, treeRef}), [selectedRoute, treeRef]); return ( @@ -242,7 +240,7 @@ export const NavigationTreeItem = /*#__PURE__*/ (forwardRef as forwardRefType)( // page. When the tree isn't its own scroll container the nearest scroll parent is outside the // tree, so we skip rather than hijack the surrounding scroll position. let treeRoot = treeRef?.current; - if (treeRoot && treeRoot.contains(scrollParent)) { + if (treeRoot && nodeContains(treeRoot, scrollParent)) { scrollIntoView(scrollParent, objRef.current, {block: 'center'}); } } diff --git a/starters/docs/stories/NavigationTree.stories.tsx b/starters/docs/stories/NavigationTree.stories.tsx index fb4e05485ca..a338580061f 100644 --- a/starters/docs/stories/NavigationTree.stories.tsx +++ b/starters/docs/stories/NavigationTree.stories.tsx @@ -25,11 +25,13 @@ export default meta; type Story = StoryFn; function RoutedNavigationTree(props: { - children: ({selectedRoute}: {selectedRoute: string}) => ReactNode, - defaultSelectedRoute: string + children: ({selectedRoute}: {selectedRoute: string}) => ReactNode; + defaultSelectedRoute: string; }) { let [selectedRoute, setSelectedRoute] = useState(props.defaultSelectedRoute); - return {props.children({selectedRoute})}; + return ( + {props.children({selectedRoute})} + ); } export const Example: Story = args => ( @@ -43,7 +45,9 @@ export const Example: Story = args => ( Home - + diff --git a/starters/tailwind/src/NavigationTree.tsx b/starters/tailwind/src/NavigationTree.tsx index 5de8c19c43c..1d1d939de38 100644 --- a/starters/tailwind/src/NavigationTree.tsx +++ b/starters/tailwind/src/NavigationTree.tsx @@ -123,7 +123,8 @@ export function NavigationTreeItemContent(props: {children?: React.ReactNode}) { slot="chevron" isDisabled={!hasChildItems} className={({isFocusVisible}) => - expandButton({isFocusVisible, className: hasChildItems ? undefined : 'invisible'})}> + expandButton({isFocusVisible, className: hasChildItems ? undefined : 'invisible'}) + }> @@ -133,11 +134,7 @@ export function NavigationTreeItemContent(props: {children?: React.ReactNode}) { ); } -export interface NavigationTreeItemProps extends Partial { - title?: React.ReactNode; -} - -export function NavigationTreeItem(props: NavigationTreeItemProps) { +export function NavigationTreeItem(props: AriaNavigationTreeItemProps) { return ; } @@ -154,7 +151,11 @@ export function NavigationTreeHeader(props: NavigationTreeHeaderProps) { ); } -export function NavigationTreeItemLink(props: LinkProps) { +export interface NavigationTreeItemLinkProps extends Omit { + children?: React.ReactNode; +} + +export function NavigationTreeItemLink(props: NavigationTreeItemLinkProps) { let {indicator} = React.useContext(NavTreeLinkContext); return ( linkStyles({isDisabled})}> diff --git a/starters/tailwind/stories/NavigationTree.stories.tsx b/starters/tailwind/stories/NavigationTree.stories.tsx index a8e47b26c1c..474da7d5799 100644 --- a/starters/tailwind/stories/NavigationTree.stories.tsx +++ b/starters/tailwind/stories/NavigationTree.stories.tsx @@ -23,11 +23,13 @@ const meta: Meta = { export default meta; function RoutedNavigationTree(props: { - children: ({selectedRoute}: {selectedRoute: string}) => ReactNode, - defaultSelectedRoute: string + children: ({selectedRoute}: {selectedRoute: string}) => ReactNode; + defaultSelectedRoute: string; }) { let [selectedRoute, setSelectedRoute] = useState(props.defaultSelectedRoute); - return {props.children({selectedRoute})}; + return ( + {props.children({selectedRoute})} + ); } export const Example = (args: any) => ( @@ -41,7 +43,9 @@ export const Example = (args: any) => ( Home - + From afb416ac5736560b59a5a0d2461ec6863f5a5045 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 25 Aug 2026 13:00:21 +1000 Subject: [PATCH 4/5] move inline alert --- .../dev/s2-docs/pages/react-aria/NavigationTree.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx b/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx index 6f09b88cf36..7a67df06f1c 100644 --- a/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx +++ b/packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx @@ -119,11 +119,6 @@ export const description = 'A navigation component that displays a nested, hiera - - Accessibility - `NavigationTree` renders as a tree so keyboard users can navigate and expand the hierarchy. When it acts as the main navigation for a page, place it inside a [navigation landmark](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/navigation.html): wrap the `NavigationTree` in a `