Skip to content

feat: RAC SideNav - #10404

Open
snowystinger wants to merge 6 commits into
mainfrom
rac-sidenav
Open

feat: RAC SideNav#10404
snowystinger wants to merge 6 commits into
mainfrom
rac-sidenav

Conversation

@snowystinger

@snowystinger snowystinger commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes

Adds the RAC implementation of SideNav. Rewrites S2 to use that. Adds starter examples and docs page for new RAC.

Switches Docs SideNav over to our S2 SideNav.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 31, 2026

Copy link
Copy Markdown

if (key != null) {
key = closestVisibleKey(collection, expandedKeys, key);
syncedRouteRef.current = selectedRoute;
selectionManager.setFocusedKey(key);

@nwidynski nwidynski Aug 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm kind of wondering whether we may want to force a scrollIntoView here, even without the collection having focus. I think in principle, it would be nice if a SideNav could be re-used for the navigation markers of a "Table of Contents" component, in which the active marker would follow along while scrolling.

In that regard, I was also questioning whether "SideNav" might be too restraining for a use-case which may later expand. Maybe something like "NavigationTargetGroup", "NavTargetList" or something of those sorts would leave some more room to play with later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, naming definitely up for discussion :)

Interesting idea with the scrolling, right now I'm handling it outside and it's a little hack-y.

Thanks for the feedback!


let defaultRef = useRef<HTMLDivElement>(null);
let mergedRef = mergeRefs(ref, defaultRef);
useEffect(() => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always try to scroll the current item into view whenever it changes? Should we have a prop to disable this behaviour entirely? should there be an option to only do it on mount and no other time?

@nwidynski nwidynski Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fyi, I believe scroll-on-mount should be handled more holistically with an integration of the upcoming scroll-initial-target CSS attribute. Ideally that would be a mechanic for all collections, and also integrate with virtualizer to push the item into persisted keys.

Related to this, Im also not quite sure whether the current key here is being persisted? How does Tree learn of the Items quasi selection state? Is it just through “current” always being kept in-sync with the focusedKey?

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:SideNav

+SideNav <T> {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  className?: ClassNameOrFunction<SideNavRenderProps> = 'react-aria-SideNav'
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SideNavRenderProps>
+  selectedRoute?: string | null
+  slot?: string | null
+  style?: StyleOrFunction<SideNavRenderProps>
+}

/react-aria-components:SideNavItem

+SideNavItem <T extends {}> {
+  aria-label?: string
+  children: ReactNode
+  className?: ClassNameOrFunction<SideNavItemRenderProps> = 'react-aria-SideNavItem'
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SideNavItemRenderProps>
+  routerOptions?: RouterOptions
+  style?: StyleOrFunction<SideNavItemRenderProps>
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/react-aria-components:SideNavItemContent

+SideNavItemContent {
+  children: ChildrenOrFunction<SideNavItemContentRenderProps>
+}

/react-aria-components:SideNavSection

+SideNavSection <T extends {}> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  className?: string
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:SideNavHeader

+SideNavHeader {
+  children?: ReactNode
+  className?: string
+  id?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:SideNavContext

+SideNavContext {
+  UNTYPED
+}

/react-aria-components:SideNavProps

+SideNavProps <T> {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  className?: ClassNameOrFunction<SideNavRenderProps> = 'react-aria-SideNav'
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SideNavRenderProps>
+  selectedRoute?: string | null
+  slot?: string | null
+  style?: StyleOrFunction<SideNavRenderProps>
+}

/react-aria-components:SideNavRenderProps

+SideNavRenderProps {
+  isEmpty: boolean
+  isFocusVisible: boolean
+  isFocused: boolean
+  state: TreeState<unknown>
+}

/react-aria-components:SideNavItemProps

+SideNavItemProps <T = {}> {
+  aria-label?: string
+  children: ReactNode
+  className?: ClassNameOrFunction<SideNavItemRenderProps> = 'react-aria-SideNavItem'
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SideNavItemRenderProps>
+  routerOptions?: RouterOptions
+  style?: StyleOrFunction<SideNavItemRenderProps>
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/react-aria-components:SideNavItemRenderProps

+SideNavItemRenderProps {
+  allowsDragging?: boolean
+  hasChildItems: boolean
+  id: Key
+  isCurrent: boolean
+  isCurrentAncestor: boolean
+  isDisabled: boolean
+  isDragging?: boolean
+  isDropTarget?: boolean
+  isExpanded: boolean
+  isFocusVisible: boolean
+  isFocusVisibleWithin: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isPressed: boolean
+  isSelected: boolean
+  level: number
+  selectionBehavior: SelectionBehavior
+  selectionMode: SelectionMode
+  state: TreeState<unknown>
+}

/react-aria-components:SideNavItemContentProps

+SideNavItemContentProps {
+  children: ChildrenOrFunction<SideNavItemContentRenderProps>
+}

/react-aria-components:SideNavItemContentRenderProps

+SideNavItemContentRenderProps {
+  allowsDragging?: boolean
+  hasChildItems: boolean
+  id: Key
+  isCurrent: boolean
+  isCurrentAncestor: boolean
+  isDisabled: boolean
+  isDragging?: boolean
+  isDropTarget?: boolean
+  isExpanded: boolean
+  isFocusVisible: boolean
+  isFocusVisibleWithin: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isPressed: boolean
+  isSelected: boolean
+  level: number
+  selectionBehavior: SelectionBehavior
+  selectionMode: SelectionMode
+  state: TreeState<unknown>
+}

/react-aria-components:SideNavSectionProps

+SideNavSectionProps <T> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  className?: string
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

/react-aria-components:SideNavHeaderProps

+SideNavHeaderProps {
+  children?: ReactNode
+  className?: string
+  id?: string
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  style?: CSSProperties
+}

@rspbot

rspbot commented Aug 3, 2026

Copy link
Copy Markdown

Agent Skills Changes

Added (2)
Modified (9)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/9d0b47c3c6f911da53900087171a6f20e74f17cf/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/9d0b47c3c6f911da53900087171a6f20e74f17cf/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants