feat(SideNav): popover for collapsed items with children - #680
Conversation
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR Modified ComponentsSideNav · View in Storybook
Bundle Size Summary
Accessibility AuditStatus: 1 accessibility violation(s) found — 1 serious. XDSSideNav - 1 issue(s)
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
bad015f to
cf73d29
Compare
| }, | ||
| // Popover surface for collapsed items with children | ||
| popoverSurface: { | ||
| backgroundColor: colorVars['--color-surface'], |
There was a problem hiding this comment.
Should be color-popover
There was a problem hiding this comment.
Fixed — updated to colorVars['--color-popover']. ✓
| navItemStyles.item, | ||
| styles.itemCollapsed, | ||
| isSelected && navItemStyles.selected, | ||
| isDisabled && navItemStyles.disabled, |
There was a problem hiding this comment.
xdsClassName('side-nav-item') needs to be alongside these styles since these are the styles that determine the appearance of the nav item.
It might be easier to extract the stylex.props here as the nav item styles since we reapply it in multiple places.
There was a problem hiding this comment.
Great call — see commit b7b41b2 which extracts collapsedItemStyles and moves xdsClassName('side-nav-item') to the interactive elements. Much cleaner with the shared variable.
There was a problem hiding this comment.
These also need the xdsClassName('side-nav-item'), class
There was a problem hiding this comment.
Fixed in 2c729f3 — added xdsClassName('side-nav-item') to both the <LinkComponent> and <button> in the expanded (non-collapsed) path. They now use mergeProps(xdsClassName('side-nav-item'), stylex.props(...)) matching the collapsed path.
a0e10fa to
2c729f3
Compare
| borderStyle: 'solid', | ||
| borderColor: colorVars['--color-divider'], | ||
| borderRadius: radiusVars['--radius-2'], | ||
| boxShadow: elevationVars['--elevation-menu'], |
There was a problem hiding this comment.
--color-divider is being renamed to --color-border in #698. Will need updating after that PR merges.
There was a problem hiding this comment.
Done — updated to colorVars['--color-border'] now that #698 has merged. Also rebased onto main to pick up the rename across the codebase. ✓
bc9ed49 to
5e29dab
Compare
When the SideNav is collapsed to icon-only mode, items with sub-items now show a popover on click: - Appears to the end (right in LTR) of the icon-only trigger - Contains children rendered in expanded form (not collapsed) via XDSSideNavCollapseProvider wrapping with isCollapsed: false - Closes on click outside, escape, or clicking a child item - Shows the parent label as a header in the popover - Uses XDS design tokens for surface, border, elevation, and radius Items without children in collapsed mode render as icon-only link/button with tooltip (existing behavior, cleaned up). Changes: - XDSSideNavCollapseContext.ts → .tsx with XDSSideNavCollapseProvider - XDSSideNavItem: collapsed render path with useXDSPopover - Removed dead isCollapsed references in expanded render path - 8 new tests for collapsed mode behavior Co-authored-by: Navi <navi@navibot.dev>
Popover now opens on hover (150ms delay) in addition to click, matching TopNavMenu behavior. Hovering the popover content keeps it open; leaving both trigger and panel closes after 200ms. Co-authored-by: Navi <navi@navibot.dev>
React 19 supports <Context value={...}> without .Provider. Remove
the unnecessary Provider wrapper — use XDSSideNavCollapseContext
directly in SideNavItem popover and test helpers.
CollapseContext file stays as .ts (no JSX needed).
Co-authored-by: Navi <navi@navibot.dev>
All useRef/useCallback hooks must run before the early return for items without icons in collapsed mode. React requires the same number of hooks every render. Co-authored-by: Navi <navi@navibot.dev>
Co-authored-by: Navi <navi@navibot.dev>
…ents
- Extract shared collapsed item styles (navItemStyles.item + itemCollapsed +
selected + disabled) into a single collapsedItemStyles variable
- Move xdsClassName('side-nav-item') from wrapper div to the actual item
elements (button/link/trigger) where the styles are applied
- Wrapper divs now only get styles.root
Co-authored-by: Navi <navi@navibot.dev>
- Move xdsClassName('side-nav-item') to expanded link/button elements
(was only on wrapper div)
- Add marginInlineStart: spacing-1 (4px) to popover surface for gap
between trigger and popover
Co-authored-by: Navi <navi@navibot.dev>
PR #698 renamed --color-divider to --color-border across the codebase. Updated the popoverSurface borderColor to use the new token name. Co-authored-by: Navi <navi@navibot.dev>
5e29dab to
201a3f4
Compare
Badge: add label prop (new in #709) to English, Chinese, and dense docs. Section: add xstyle and padding props (xstyle new in #721) to all translations. SideNavItem: add collapsible prop (new in #680) with controlled/uncontrolled mode documentation. Updated nested items example to show collapsible usage. Found during Night Watch doc review. Co-authored-by: Navi <navi@multimango.com>
* feat(SideNav): popover for collapsed items with children
When the SideNav is collapsed to icon-only mode, items with sub-items
now show a popover on click:
- Appears to the end (right in LTR) of the icon-only trigger
- Contains children rendered in expanded form (not collapsed) via
XDSSideNavCollapseProvider wrapping with isCollapsed: false
- Closes on click outside, escape, or clicking a child item
- Shows the parent label as a header in the popover
- Uses XDS design tokens for surface, border, elevation, and radius
Items without children in collapsed mode render as icon-only
link/button with tooltip (existing behavior, cleaned up).
Changes:
- XDSSideNavCollapseContext.ts → .tsx with XDSSideNavCollapseProvider
- XDSSideNavItem: collapsed render path with useXDSPopover
- Removed dead isCollapsed references in expanded render path
- 8 new tests for collapsed mode behavior
Co-authored-by: Navi <navi@users.noreply.github.com>
* feat: hover to open collapsed subnav popover
Popover now opens on hover (150ms delay) in addition to click,
matching TopNavMenu behavior. Hovering the popover content keeps
it open; leaving both trigger and panel closes after 200ms.
Co-authored-by: Navi <navi@users.noreply.github.com>
* refactor: remove XDSSideNavCollapseProvider, use context directly
React 19 supports <Context value={...}> without .Provider. Remove
the unnecessary Provider wrapper — use XDSSideNavCollapseContext
directly in SideNavItem popover and test helpers.
CollapseContext file stays as .ts (no JSX needed).
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: move hooks before early return to prevent hooks violation
All useRef/useCallback hooks must run before the early return for
items without icons in collapsed mode. React requires the same
number of hooks every render.
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: use --color-popover for collapsed subnav popover surface
Co-authored-by: Navi <navi@users.noreply.github.com>
* refactor: extract collapsedItemStyles, move xdsClassName to item elements
- Extract shared collapsed item styles (navItemStyles.item + itemCollapsed +
selected + disabled) into a single collapsedItemStyles variable
- Move xdsClassName('side-nav-item') from wrapper div to the actual item
elements (button/link/trigger) where the styles are applied
- Wrapper divs now only get styles.root
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: add xdsClassName to expanded items, 4px popover offset
- Move xdsClassName('side-nav-item') to expanded link/button elements
(was only on wrapper div)
- Add marginInlineStart: spacing-1 (4px) to popover surface for gap
between trigger and popover
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: update --color-divider to --color-border after #698 merge
PR #698 renamed --color-divider to --color-border across the codebase.
Updated the popoverSurface borderColor to use the new token name.
Co-authored-by: Navi <navi@users.noreply.github.com>
---------
Co-authored-by: Navi <navi@users.noreply.github.com>
* feat(SideNav): popover for collapsed items with children
When the SideNav is collapsed to icon-only mode, items with sub-items
now show a popover on click:
- Appears to the end (right in LTR) of the icon-only trigger
- Contains children rendered in expanded form (not collapsed) via
XDSSideNavCollapseProvider wrapping with isCollapsed: false
- Closes on click outside, escape, or clicking a child item
- Shows the parent label as a header in the popover
- Uses XDS design tokens for surface, border, elevation, and radius
Items without children in collapsed mode render as icon-only
link/button with tooltip (existing behavior, cleaned up).
Changes:
- XDSSideNavCollapseContext.ts → .tsx with XDSSideNavCollapseProvider
- XDSSideNavItem: collapsed render path with useXDSPopover
- Removed dead isCollapsed references in expanded render path
- 8 new tests for collapsed mode behavior
Co-authored-by: Navi <navi@users.noreply.github.com>
* feat: hover to open collapsed subnav popover
Popover now opens on hover (150ms delay) in addition to click,
matching TopNavMenu behavior. Hovering the popover content keeps
it open; leaving both trigger and panel closes after 200ms.
Co-authored-by: Navi <navi@users.noreply.github.com>
* refactor: remove XDSSideNavCollapseProvider, use context directly
React 19 supports <Context value={...}> without .Provider. Remove
the unnecessary Provider wrapper — use XDSSideNavCollapseContext
directly in SideNavItem popover and test helpers.
CollapseContext file stays as .ts (no JSX needed).
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: move hooks before early return to prevent hooks violation
All useRef/useCallback hooks must run before the early return for
items without icons in collapsed mode. React requires the same
number of hooks every render.
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: use --color-popover for collapsed subnav popover surface
Co-authored-by: Navi <navi@users.noreply.github.com>
* refactor: extract collapsedItemStyles, move xdsClassName to item elements
- Extract shared collapsed item styles (navItemStyles.item + itemCollapsed +
selected + disabled) into a single collapsedItemStyles variable
- Move xdsClassName('side-nav-item') from wrapper div to the actual item
elements (button/link/trigger) where the styles are applied
- Wrapper divs now only get styles.root
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: add xdsClassName to expanded items, 4px popover offset
- Move xdsClassName('side-nav-item') to expanded link/button elements
(was only on wrapper div)
- Add marginInlineStart: spacing-1 (4px) to popover surface for gap
between trigger and popover
Co-authored-by: Navi <navi@users.noreply.github.com>
* fix: update --color-divider to --color-border after #698 merge
PR #698 renamed --color-divider to --color-border across the codebase.
Updated the popoverSurface borderColor to use the new token name.
Co-authored-by: Navi <navi@users.noreply.github.com>
---------
Co-authored-by: Navi <navi@users.noreply.github.com>
* feat(SideNav): popover for collapsed items with children
When the SideNav is collapsed to icon-only mode, items with sub-items
now show a popover on click:
- Appears to the end (right in LTR) of the icon-only trigger
- Contains children rendered in expanded form (not collapsed) via
XDSSideNavCollapseProvider wrapping with isCollapsed: false
- Closes on click outside, escape, or clicking a child item
- Shows the parent label as a header in the popover
- Uses XDS design tokens for surface, border, elevation, and radius
Items without children in collapsed mode render as icon-only
link/button with tooltip (existing behavior, cleaned up).
Changes:
- XDSSideNavCollapseContext.ts → .tsx with XDSSideNavCollapseProvider
- XDSSideNavItem: collapsed render path with useXDSPopover
- Removed dead isCollapsed references in expanded render path
- 8 new tests for collapsed mode behavior
* feat: hover to open collapsed subnav popover
Popover now opens on hover (150ms delay) in addition to click,
matching TopNavMenu behavior. Hovering the popover content keeps
it open; leaving both trigger and panel closes after 200ms.
* refactor: remove XDSSideNavCollapseProvider, use context directly
React 19 supports <Context value={...}> without .Provider. Remove
the unnecessary Provider wrapper — use XDSSideNavCollapseContext
directly in SideNavItem popover and test helpers.
CollapseContext file stays as .ts (no JSX needed).
* fix: move hooks before early return to prevent hooks violation
All useRef/useCallback hooks must run before the early return for
items without icons in collapsed mode. React requires the same
number of hooks every render.
* fix: use --color-popover for collapsed subnav popover surface
* refactor: extract collapsedItemStyles, move xdsClassName to item elements
- Extract shared collapsed item styles (navItemStyles.item + itemCollapsed +
selected + disabled) into a single collapsedItemStyles variable
- Move xdsClassName('side-nav-item') from wrapper div to the actual item
elements (button/link/trigger) where the styles are applied
- Wrapper divs now only get styles.root
* fix: add xdsClassName to expanded items, 4px popover offset
- Move xdsClassName('side-nav-item') to expanded link/button elements
(was only on wrapper div)
- Add marginInlineStart: spacing-1 (4px) to popover surface for gap
between trigger and popover
* fix: update --color-divider to --color-border after #698 merge
PR #698 renamed --color-divider to --color-border across the codebase.
Updated the popoverSurface borderColor to use the new token name.
---------
The collapsed submenu flyout painted two surfaces. `usePopover` already paints the panel it renders into — `--color-background-popover`, a `--radius-container` corner and `--shadow-low` — and the content div underneath it added a 1px `--color-border` rectangle at radius 0, so the flyout showed square grey corners inside rounded ones. Every other popover consumer in core (DropdownMenu, TopNavMenu, Selector, TabMenu) leaves the chrome to the surface; SideNavItem and TopNavMegaMenu were the only two adding a border of their own. The same div carried `marginInlineStart: --spacing-1`. Inside the panel that is not a gap from the rail — it insets the content, leaving an unbalanced 4px strip down the flyout's inside edge with the painted panel poking out past it. The gap belongs on the positioned layer, which is where DropdownMenu puts its `popoverInlineGap`, so it moves there and becomes what it was always meant to be: 4px between the trigger and the flyout. The content lands on exactly the same pixel column as before (x=470 at a 900px viewport); what moves is the painted panel's left edge, from 4px behind the content to flush with it. Reported on #4880 by @kentonquatman. Pre-existing since #680 — the border predates `usePopover` gaining a default surface. Verified: 224 tests green across SideNav and TopNav; flyout geometry measured in real Chromium before and after (border 1px→0, content inset 4/0 → 0/0, panel radius 12px with no square inner rectangle).
The collapsed submenu flyout painted two surfaces. `usePopover` already paints the panel it renders into — `--color-background-popover`, a `--radius-container` corner and `--shadow-low` — and the content div underneath it added a 1px `--color-border` rectangle at radius 0, so the flyout showed square grey corners inside rounded ones. Every other popover consumer in core (DropdownMenu, TopNavMenu, Selector, TabMenu) leaves the chrome to the surface; SideNavItem and TopNavMegaMenu were the only two adding a border of their own. The same div carried `marginInlineStart: --spacing-1`. Inside the panel that is not a gap from the rail — it insets the content, leaving an unbalanced 4px strip down the flyout's inside edge with the painted panel poking out past it. The gap belongs on the positioned layer, which is where DropdownMenu puts its `popoverInlineGap`, so it moves there and becomes what it was always meant to be: 4px between the trigger and the flyout. The content lands on exactly the same pixel column as before (x=470 at a 900px viewport); what moves is the painted panel's left edge, from 4px behind the content to flush with it. Reported on #4880 by @kentonquatman. Pre-existing since #680 — the border predates `usePopover` gaining a default surface. Verified: 224 tests green across SideNav and TopNav; flyout geometry measured in real Chromium before and after (border 1px→0, content inset 4/0 → 0/0, panel radius 12px with no square inner rectangle).
The collapsed submenu flyout painted two surfaces. `usePopover` already paints the panel it renders into — `--color-background-popover`, a `--radius-container` corner and `--shadow-low` — and the content div underneath it added a 1px `--color-border` rectangle at radius 0, so the flyout showed square grey corners inside rounded ones. Every other popover consumer in core (DropdownMenu, TopNavMenu, Selector, TabMenu) leaves the chrome to the surface; SideNavItem and TopNavMegaMenu were the only two adding a border of their own. The same div carried `marginInlineStart: --spacing-1`. Inside the panel that is not a gap from the rail — it insets the content, leaving an unbalanced 4px strip down the flyout's inside edge with the painted panel poking out past it. The gap belongs on the positioned layer, which is where DropdownMenu puts its `popoverInlineGap`, so it moves there and becomes what it was always meant to be: 4px between the trigger and the flyout. The content lands on exactly the same pixel column as before (x=470 at a 900px viewport); what moves is the painted panel's left edge, from 4px behind the content to flush with it. Reported on #4880 by @kentonquatman. Pre-existing since #680 — the border predates `usePopover` gaining a default surface. Verified: 224 tests green across SideNav and TopNav; flyout geometry measured in real Chromium before and after (border 1px→0, content inset 4/0 → 0/0, panel radius 12px with no square inner rectangle).
Popover for Collapsed SideNav Items with Children
Re-implementation of #655 on current main (post shell hardening batch).
Behavior
When the SideNav is collapsed to icon-only mode, items with sub-items show a popover on hover or click:
XDSSideNavCollapseProvider--color-surface,--color-divider,--elevation-menu,--radius-elementItems without children in collapsed mode: icon-only link/button with tooltip.
Changes
XDSSideNavCollapseContext.ts→.tsxwith newXDSSideNavCollapseProviderXDSSideNavItem: dedicated collapsed render path usinguseXDSPopoverisCollapsedreferences in expanded render pathnavItemStylesfor all collapsed item styling