From 90dee0f43e85fa1ee7586119310a1da555adc1b6 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:00:17 +0200 Subject: [PATCH] feat: partition sidebar sections into top/bottom groups with spacer SidebarRoot now extracts SidebarFooter before partitioning remaining children into topSections / bottomSections based on the position prop. Top sections render in a .sectionGroup wrapper; when bottom sections exist a .sectionSpacer divider is added followed by a second .sectionGroup, pushing bottom nav to the foot of the sidebar. Co-Authored-By: Claude Sonnet 4.6 --- .../layout/Sidebar/Sidebar.test.tsx | 83 +++++++++++++++++++ src/design-system/layout/Sidebar/Sidebar.tsx | 40 ++++++++- 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/design-system/layout/Sidebar/Sidebar.test.tsx b/src/design-system/layout/Sidebar/Sidebar.test.tsx index b308ae3..0c5b570 100644 --- a/src/design-system/layout/Sidebar/Sidebar.test.tsx +++ b/src/design-system/layout/Sidebar/Sidebar.test.tsx @@ -393,4 +393,87 @@ describe('Sidebar compound component', () => { const contentWrapper = container.querySelector('.sectionContent') expect(contentWrapper).not.toBeInTheDocument() }) + + // 20. renders top sections in sectionGroup wrapper + it('renders sections in top group wrapper by default', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+
+
, + ) + + const topGroup = container.querySelector('.sectionGroup') + expect(topGroup).toBeInTheDocument() + expect(topGroup!.textContent).toContain('Apps') + }) + + // 21. renders bottom sections in separate group with spacer + it('renders bottom sections in a separate group with spacer between', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+ ic} label="Routes" open onToggle={vi.fn()} position="bottom"> +
routes content
+
+
+
, + ) + + const groups = container.querySelectorAll('.sectionGroup') + expect(groups).toHaveLength(2) + expect(groups[0].textContent).toContain('Apps') + expect(groups[1].textContent).toContain('Routes') + + const spacer = container.querySelector('.sectionSpacer') + expect(spacer).toBeInTheDocument() + }) + + // 22. does not render spacer when no bottom sections + it('does not render spacer when all sections are top', () => { + const { container } = render( + + + ic} label="Apps" open onToggle={vi.fn()}> +
apps content
+
+ ic} label="Agents" open onToggle={vi.fn()}> +
agents content
+
+
+
, + ) + + const spacer = container.querySelector('.sectionSpacer') + expect(spacer).not.toBeInTheDocument() + }) + + // 23. collapsed sidebar renders bottom sections in bottom group + it('renders bottom sections in bottom group when sidebar is collapsed', () => { + const { container } = render( + + + ic} label="Apps" open={false} onToggle={vi.fn()}> +
apps
+
+ ic} label="Routes" open={false} onToggle={vi.fn()} position="bottom"> +
routes
+
+
+
, + ) + + const groups = container.querySelectorAll('.sectionGroup') + expect(groups).toHaveLength(2) + + // Bottom group should contain the Routes rail item + const bottomGroup = groups[1] + expect(bottomGroup.querySelector('[title="Routes"]')).toBeInTheDocument() + }) }) diff --git a/src/design-system/layout/Sidebar/Sidebar.tsx b/src/design-system/layout/Sidebar/Sidebar.tsx index 8dddcc3..d88563d 100644 --- a/src/design-system/layout/Sidebar/Sidebar.tsx +++ b/src/design-system/layout/Sidebar/Sidebar.tsx @@ -205,9 +205,11 @@ function SidebarRoot({ )} - {/* Render Header first, then search, then remaining children */} + {/* Render Header first, then search, then partitioned sections, then footer */} {(() => { const childArray = Children.toArray(children) + + // Extract header const headerIdx = childArray.findIndex( (child) => isValidElement(child) && child.type === SidebarHeader, ) @@ -216,6 +218,31 @@ function SidebarRoot({ ? [...childArray.slice(0, headerIdx), ...childArray.slice(headerIdx + 1)] : childArray + // Extract footer + const footerIdx = rest.findIndex( + (child) => isValidElement(child) && child.type === SidebarFooter, + ) + const footer = footerIdx >= 0 ? rest[footerIdx] : null + const sections = footerIdx >= 0 + ? [...rest.slice(0, footerIdx), ...rest.slice(footerIdx + 1)] + : rest + + // Partition sections into top/bottom by position prop + const topSections: typeof sections = [] + const bottomSections: typeof sections = [] + for (const child of sections) { + if ( + isValidElement(child) && + child.props.position === 'bottom' + ) { + bottomSections.push(child) + } else { + topSections.push(child) + } + } + + const hasBottomSections = bottomSections.length > 0 + return ( <> {header} @@ -245,7 +272,16 @@ function SidebarRoot({ )} - {rest} +
+ {topSections} +
+ {hasBottomSections &&
} + {hasBottomSections && ( +
+ {bottomSections} +
+ )} + {footer} ) })()}