Skip to content

feat: S2 SideNav#10306

Open
snowystinger wants to merge 39 commits into
mainfrom
s2-sidenav-rob
Open

feat: S2 SideNav#10306
snowystinger wants to merge 39 commits into
mainfrom
s2-sidenav-rob

Conversation

@snowystinger

@snowystinger snowystinger commented Jul 7, 2026

Copy link
Copy Markdown
Member

Closes

Early draft of SideNav for S2.

api is this right now:

<SideNav selectedRoute="/home"> // no onSelectionChange, control through router
  <SideNavItem href="/home">
    <SideNavContent>
      <SideNavLink>

decisions to make still

  • what are "categories"?
    • They are just items that don't have a link in them, don't participate in selection and don't have the selection hover state. This will be problematic with our API as we don't know ahead of time if there will be a link or not.
    • They can be mixed in the same SideNav, if it has children, it can either be a category or an item with a link.
  • how to stop focus from ever going to the "row/cell"
    • Solved by forwarding events and re-firing them on the row from the link. This way you don't need to navigate to the row.
  • what happens to selection indicator if it's on an item inside a collapsed item/category?
    • Figma shows that the item gets bolded and a different text color
  • does the selection indicator animate like Tabs do? what happens if it needs to move between levels?
    • Design says no due to how far the indicator may need to move and how it indents

✅ 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:

@snowystinger
snowystinger marked this pull request as draft July 7, 2026 05:14
@rspbot

rspbot commented Jul 7, 2026

Copy link
Copy Markdown

@snowystinger snowystinger changed the title S2 sidenav rob feat: S2 SideNav Jul 7, 2026
@rspbot

rspbot commented Jul 9, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 10, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 10, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 13, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 13, 2026

Copy link
Copy Markdown

Comment on lines +203 to +206
// RAC swallows arrow keys at the collection level (stopPropagation during capture), so a handler
// on the link never sees them. Intercept here on an ancestor, before RAC's row handler runs, and
// expand a collapsed row when the expand arrow is pressed while focus is on its link.
let onKeyDownCapture = (e: ReactKeyboardEvent) => {

@LFDanLu LFDanLu Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I haven't pulled this down to try it myself, but perhaps you could make SideNav a keyboardNavigationBehavior = tab tree and have focusMode="child" and allowsArrowNavigation=true so that arrow up/down navigation still work but focus lands on the link by default. useGridList's handleTreeExpansionKeys will have to be updated to also check allowsArrowNavigation so that it can skip the
activeElement check below, but that might allow you to get rid of all this extra handling at this level

  if (!('expandedKeys' in state) || activeElement !== rowRef) {
    return false;
  }

note that the extra props mentioned above are in #10159, so will require you to rebase/pull in that PR

@snowystinger snowystinger Jul 14, 2026

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.

Good news, textfield_gridlists is already my base :)
I attempted an implementation of the above and it appears to mostly work, so that's pretty awesome. I was missing that allowsArrowNavigation update when I tried those all together previously. Good thinking.

Just for posterity, there's a couple cases here:

Left/Right arrow to expand/collapse/traverse up that should still be accessible from the link.

In addition, focusMode="child" should only be the case for items with links (not categories) because then it might accidentally focus something like an ActionMenu that may be in the row regardless of if there is a Link.

Shift+Tab from a link should leave the SideNav.

Comment thread packages/@react-spectrum/s2/src/SideNav.tsx
@snowystinger
snowystinger marked this pull request as ready for review July 14, 2026 07:26
@rspbot

rspbot commented Jul 14, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 15, 2026

Copy link
Copy Markdown

Base automatically changed from textfield_gridlists to main July 17, 2026 07:30
@rspbot

rspbot commented Jul 21, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 21, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 21, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 21, 2026

Copy link
Copy Markdown

@LFDanLu LFDanLu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me, verified the recent fixes, just some small comments

import {useLocale} from 'react-aria/I18nProvider';
import {useScale} from './utils';

export interface SideNavProps<T>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should SideNav have onAction exposed on it? I thought routing would be triggered via the SideNav links

Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
TreeStateContext
} from '../src/Tree';
export type {
GridListSectionProps,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be redefined as TreeSectionProps so if they diverge in the future it will be easier to handle?

@github-actions github-actions Bot added the Tree label Jul 22, 2026
@rspbot

rspbot commented Jul 22, 2026

Copy link
Copy Markdown

LFDanLu
LFDanLu previously approved these changes Jul 22, 2026
Comment thread packages/@react-spectrum/s2/src/SideNav.tsx
forcedColors: 'ButtonText'
},
fontWeight: {
isSelected: 'bold',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not ideal that when you select an item the entire sidenav gets wider and causes a layout shift. You can see this in the docs example when you select "Background layers"

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.

hmmm that is a good point. I could render a visually hidden zero height block element to reserve the horizontal space... I'll push that up and you can see how you like it

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.

think I should hardcode the width as well though? when you collapse a level it also causes a shift

SideNavs do not support an uncontrolled selection state, you are responsible for managing it through the `selectedRoute` prop. You may wire this up
to a router or other state management solution.

If a SideNavItem has an `href`, then you must pass a `SideNavItemLink` as a child of the `SideNavItemContent`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need these caveats. Instead of saying how it's different, just describe how it works. We don't need to frame it as a collection component in the docs. An example highlighting the selectedRoute prop would also be helpful. Maybe rename the section "Routing" (also move it after Content since that is usually first) and use it to describe how to hook up RouterProvider?

>,
UnsafeStyles {
/** The route that is currently selected. */
selectedRoute: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you think about having this automatically set by the RouterProvider context?

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.

I wanted selectedRoute to be a required prop in case someone wanted to use something other than the RouterProvider. Do you think that's reasonable?

Comment thread packages/@react-spectrum/s2/src/SideNav.tsx Outdated
Comment thread packages/dev/s2-docs/pages/s2/SideNav.mdx
Comment thread packages/dev/s2-docs/src/ComponentCard.tsx
Comment thread packages/dev/s2-docs/pages/s2/SideNav.mdx Outdated
inset: 0,
top: 0,
bottom: 0,
borderRadius: 'default', // tokens say 12... but that seems a lot, should it match selection in other collections?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

feel like it makes sense to just match the other collections which reminds me that we need to update TreeView's to match ListView still...

# Conflicts:
#	packages/dev/s2-docs/src/ComponentCard.tsx
@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:GridListHeaderProps

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

@react-aria/calendar

/@react-aria/calendar:useCalendarMonthPicker

-useCalendarMonthPicker {
-  props: CalendarMonthPickerProps
-  state: CalendarState<CalendarSelectionMode> | RangeCalendarState
-  returnVal: undefined
-}

/@react-aria/calendar:useCalendarYearPicker

-useCalendarYearPicker {
-  props: CalendarYearPickerProps
-  state: CalendarState<CalendarSelectionMode> | RangeCalendarState
-  returnVal: undefined
-}

/@react-aria/calendar:CalendarMonthPickerAria

-CalendarMonthPickerAria {
-  aria-label: string
-  items: Array<CalendarMonthPickerItem>
-  onChange: (Key | null) => void
-  value: Key
-}

/@react-aria/calendar:CalendarMonthPickerItem

-CalendarMonthPickerItem {
-  date: CalendarDate
-  formatted: string
-  id: number
-}

/@react-aria/calendar:CalendarMonthPickerProps

-CalendarMonthPickerProps {
-  format?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
-}

/@react-aria/calendar:CalendarYearPickerAria

-CalendarYearPickerAria {
-  aria-label: string
-  items: Array<CalendarYearPickerItem>
-  onChange: (Key | null) => void
-  value: Key
-}

/@react-aria/calendar:CalendarYearPickerItem

-CalendarYearPickerItem {
-  date: CalendarDate
-  formatted: string
-  id: number
-}

/@react-aria/calendar:CalendarYearPickerProps

-CalendarYearPickerProps {
-  format?: CalendarYearPickerFormatOptions
-  visibleYears?: number = 20
-}

@react-spectrum/s2

/@react-spectrum/s2:SideNav

+SideNav <T> {
+  UNSAFE_className?: UnsafeClassName
+  UNSAFE_style?: CSSProperties
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  selectedRoute: string
+  slot?: string | null
+  styles?: StylesPropWithHeight
+}

/@react-spectrum/s2:SideNavItem

+SideNavItem {
+  aria-label?: string
+  children: ReactNode
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  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
+  routerOptions?: RouterOptions
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/@react-spectrum/s2:SideNavItemContent

+SideNavItemContent {
+  children: ReactNode
+}

/@react-spectrum/s2:SideNavItemLink

+SideNavItemLink {
+  children?: ReactNode
+}

/@react-spectrum/s2:SideNavSection

+SideNavSection <T extends {}> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+}

/@react-spectrum/s2:SideNavHeader

+SideNavHeader {
+  children?: ReactNode
+  id?: string
+}

/@react-spectrum/s2:SideNavProps

+SideNavProps <T> {
+  UNSAFE_className?: UnsafeClassName
+  UNSAFE_style?: CSSProperties
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  defaultExpandedKeys?: Iterable<Key>
+  dependencies?: ReadonlyArray<any>
+  disabledKeys?: Iterable<Key>
+  expandedKeys?: Iterable<Key>
+  id?: string
+  items?: Iterable<T>
+  onExpandedChange?: (Set<Key>) => any
+  selectedRoute: string
+  slot?: string | null
+  styles?: StylesPropWithHeight
+}

/@react-spectrum/s2:SideNavItemProps

+SideNavItemProps {
+  aria-label?: string
+  children: ReactNode
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  isDisabled?: boolean
+  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
+  routerOptions?: RouterOptions
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+}

/@react-spectrum/s2:SideNavItemContentProps

+SideNavItemContentProps {
+  children: ReactNode
+}

/@react-spectrum/s2:SideNavItemLinkProps

+SideNavItemLinkProps {
+  children?: ReactNode
+}

/@react-spectrum/s2:SideNavSectionProps

+SideNavSectionProps <T> {
+  aria-label?: string
+  children?: ReactNode | (T) => ReactElement
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<T>
+}

/@react-spectrum/s2:SideNavHeaderProps

+SideNavHeaderProps {
+  children?: ReactNode
+  id?: string
+}

@rspbot

rspbot commented Jul 23, 2026

Copy link
Copy Markdown

Agent Skills Changes

Added (1)
Modified (13)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/998239d9cb2f1bae40e7ace94278521ff5f4f699/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/998239d9cb2f1bae40e7ace94278521ff5f4f699/

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants