Skip to content

Commit ee3eb74

Browse files
authored
Fix brand NavList flattening deeply-nested sidebar categories (#62349)
1 parent c59fcbc commit ee3eb74

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

src/landings/components/SidebarProduct.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const SidebarProduct = () => {
6767
const productSection = () => (
6868
<div data-testid="product-sidebar">
6969
<NavList aria-label="Product sidebar">
70+
{navListLevelSentinel()}
7071
{sidebarTree &&
7172
sidebarTree.childPages.map((childPage) => (
7273
<NavListItem key={childPage.href} childPage={childPage} />
@@ -85,6 +86,7 @@ export const SidebarProduct = () => {
8586
return (
8687
<div>
8788
<NavList aria-label="REST sidebar overview articles">
89+
{navListLevelSentinel()}
8890
{conceptualPages.map((childPage) => (
8991
<NavListItem key={childPage.href} childPage={childPage} />
9092
))}
@@ -93,6 +95,7 @@ export const SidebarProduct = () => {
9395
<hr data-testid="rest-sidebar-reference" className="m-2" />
9496

9597
<NavList aria-label="REST sidebar reference pages">
98+
{navListLevelSentinel()}
9699
{restPages.map((category) => (
97100
<RestNavListItem key={category.href} category={category} />
98101
))}
@@ -135,6 +138,37 @@ function ExpandableItem({
135138
)
136139
}
137140

141+
// Brand NavList picks its starting nesting level by *statically* introspecting its
142+
// direct children for a NavList.SubNav (see the `m = d ? 1 : 2` check in the brand
143+
// esm source). Our items are custom wrapper components, so brand can't see their
144+
// SubNavs, treats the list as flat, and starts numbering at level 2 — wasting one of
145+
// its 5 available levels. Docs content nests 5 levels deep, so that lost level pushes
146+
// the deepest articles over brand's cap and the depth guard flattens them (#6757).
147+
// Brand exposes no `startLevel` prop, so this hidden sentinel gives brand a real
148+
// top-level SubNav to detect, making it number from level 1 and freeing the level the
149+
// deep content needs.
150+
//
151+
// The detector accepts a NavList.SubNav nested in ANY direct child's props.children,
152+
// not only a NavList.Item — so we use a plain <li> we fully control rather than a
153+
// NavList.Item. Brand's NavList.Item forwards style/aria-hidden to its inner <button>,
154+
// NOT the outer <li>, so a NavList.Item sentinel would leave a visible, focusable 40px
155+
// container (and trip the sibling-separator styles) at the top of every sidebar. A
156+
// hidden native <li> keeps the whole sentinel — container included — out of layout and
157+
// the a11y tree. It MUST be spread inline (returned by this factory), not rendered as a
158+
// <Component/>: brand's detector never renders function components, so a wrapper would
159+
// stay invisible to it.
160+
function navListLevelSentinel() {
161+
return (
162+
<li aria-hidden="true" style={{ display: 'none' }}>
163+
<NavList.SubNav aria-label="">
164+
<NavList.Item as="a" href="#">
165+
{''}
166+
</NavList.Item>
167+
</NavList.SubNav>
168+
</li>
169+
)
170+
}
171+
138172
function LeafLink({ node }: { node: ProductTreeNode }) {
139173
const router = useRouter()
140174
const { asPath, locale } = router
@@ -151,7 +185,7 @@ function LeafLink({ node }: { node: ProductTreeNode }) {
151185
)
152186
}
153187

154-
function NavListItem({ childPage, level = 2 }: { childPage: ProductTreeNode; level?: number }) {
188+
function NavListItem({ childPage, level = 1 }: { childPage: ProductTreeNode; level?: number }) {
155189
const router = useRouter()
156190
const { asPath, locale } = router
157191
const routePath = `/${locale}${asPath.split('?')[0].split('#')[0]}`

src/landings/components/sidebar-navlist-depth.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
type TreeNodeLike = { childPages: TreeNodeLike[] }
55

66
// Brand NavList supports up to 5 nesting levels; a level-5 item cannot contain a
7-
// SubNav (it is dropped with a warning). SidebarProduct's group-less top-level
8-
// items start at brand level 2, so items can keep nesting while level < 5. Real
9-
// docs content currently bottoms out at exactly level 5, so the guard that uses
10-
// this is defensive: if a deeper tree ever appears, the overflow is flattened
11-
// into leaf links rather than silently dropped by brand.
7+
// SubNav (it is dropped with a warning). SidebarProduct injects a hidden sentinel so
8+
// brand numbers its top-level items from level 1 (without it brand starts at level 2
9+
// and wastes a level — see navListLevelSentinel in SidebarProduct.tsx), so items can
10+
// keep nesting while level < 5. Real docs content currently bottoms out at exactly
11+
// level 5, so the guard that uses this is defensive: if a deeper tree ever appears,
12+
// the overflow is flattened into leaf links rather than silently dropped by brand.
1213
//
1314
// Kept in its own dependency-free module (no React/SCSS/Next imports) so it can
1415
// be unit-tested without booting the Next.js app.

0 commit comments

Comments
 (0)