From c68a182cf6d8c8640e9ae5c603b29411d64c691f Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Fri, 27 Mar 2026 13:40:35 +0530 Subject: [PATCH 1/2] feat: add accordion and trailingIcon --- apps/www/src/app/examples/page.tsx | 47 +++++- .../sidebar/__tests__/sidebar.test.tsx | 148 ++++++++++++++++++ .../components/sidebar/sidebar-misc.tsx | 137 +++++++++++++--- .../components/sidebar/sidebar.module.css | 87 +++++++++- 4 files changed, 393 insertions(+), 26 deletions(-) diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index 347dc0db5..d9b28d438 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -86,7 +86,7 @@ const Page = () => { backgroundColor: 'var(--rs-color-background-base-primary)' }} > - + { Analytics - + alert('Resources trailing icon clicked')} + aria-label='Resources group actions' + style={{ + border: 0, + background: 'transparent', + color: 'inherit', + padding: 0, + display: 'inline-flex', + alignItems: 'center', + cursor: 'pointer' + }} + > + + + } + > }> Reports @@ -132,7 +153,27 @@ const Page = () => { - + alert('Account trailing icon clicked')} + aria-label='Account group actions' + style={{ + border: 0, + background: 'transparent', + color: 'inherit', + padding: 0, + display: 'inline-flex', + alignItems: 'center', + cursor: 'pointer' + }} + > + + + } + > }> Settings diff --git a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx index 88bacde54..b3e581706 100644 --- a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx +++ b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx @@ -285,6 +285,154 @@ describe('Sidebar', () => { const group = screen.getByLabelText(MAIN_GROUP_LABEL); expect(group).toBeInTheDocument(); }); + + it('renders accordion trigger when accordion is enabled', () => { + render( + + + } + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + const trigger = screen.getByRole('button', { name: /Main/ }); + expect(trigger).toBeInTheDocument(); + expect(trigger).toHaveAttribute('data-panel-open'); + }); + + it('toggles group items when accordion is enabled', () => { + render( + + + } + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + const trigger = screen.getByRole('button', { name: /Main/ }); + expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument(); + + fireEvent.click(trigger); + expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument(); + + fireEvent.click(trigger); + expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument(); + }); + + it('forces accordion panel open when sidebar is collapsed', () => { + const { rerender } = render( + + + } + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + const trigger = screen.getByRole('button', { name: /Main/ }); + fireEvent.click(trigger); + expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument(); + + rerender( + + + } + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + expect( + screen.getByRole('listitem', { name: DASHBOARD_ITEM_TEXT }) + ).toBeInTheDocument(); + }); + + it('renders right icon when provided in accordion header', () => { + render( + + + +} + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + expect(screen.getByTestId('group-trailing-icon')).toBeInTheDocument(); + }); + + it('does not toggle accordion when trailing icon is clicked', () => { + const onTrailingIconClick = vi.fn(); + + render( + + + + + + + } + > + }> + {DASHBOARD_ITEM_TEXT} + + + + + ); + + const trigger = screen.getByRole('button', { name: /Main/ }); + expect(trigger).toHaveAttribute('data-panel-open'); + + fireEvent.click(screen.getByTestId('group-trailing-action')); + + expect(onTrailingIconClick).toHaveBeenCalledTimes(1); + expect(trigger).toHaveAttribute('data-panel-open'); + expect(screen.getByText(DASHBOARD_ITEM_TEXT)).toBeInTheDocument(); + }); }); describe('Sidebar More', () => { diff --git a/packages/raystack/components/sidebar/sidebar-misc.tsx b/packages/raystack/components/sidebar/sidebar-misc.tsx index 9c4b182e5..f782f67d1 100644 --- a/packages/raystack/components/sidebar/sidebar-misc.tsx +++ b/packages/raystack/components/sidebar/sidebar-misc.tsx @@ -1,9 +1,12 @@ 'use client'; +import { Accordion as AccordionPrimitive } from '@base-ui/react'; +import { TriangleDownIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { ComponentProps, ReactNode } from 'react'; +import { ComponentProps, ReactNode, useContext } from 'react'; import { Flex } from '../flex'; import styles from './sidebar.module.css'; +import { SidebarContext } from './sidebar-root'; export function SidebarHeader({ className, @@ -38,50 +41,142 @@ SidebarFooter.displayName = 'Sidebar.Footer'; export interface SidebarNavigationGroupProps extends ComponentProps<'section'> { label: string; + value?: string; + accordion?: boolean; leadingIcon?: ReactNode; + trailingIcon?: ReactNode; classNames?: { header?: string; items?: string; label?: string; icon?: string; + trigger?: string; + chevron?: string; + trailingIcon?: string; }; } export function SidebarNavigationGroup({ className, label, + value, + accordion = false, leadingIcon, + trailingIcon, classNames, children, ...props }: SidebarNavigationGroupProps) { + const { isCollapsed } = useContext(SidebarContext); + const groupValue = value ?? label; + + if (!accordion) { + return ( +
+ + {leadingIcon && ( + + {leadingIcon} + + )} + + {label} + + {trailingIcon ? ( + + {trailingIcon} + + ) : null} + + + {children} + +
+ ); + } + return (
- - {leadingIcon && ( - - {leadingIcon} - - )} - - {label} - - - - {children} - + + + + {leadingIcon && ( + + {leadingIcon} + + )} + + {label} + + + {trailingIcon ? ( + + {trailingIcon} + + ) : null} + + + + {children} + + + +
); } diff --git a/packages/raystack/components/sidebar/sidebar.module.css b/packages/raystack/components/sidebar/sidebar.module.css index 840a54133..7a793783b 100644 --- a/packages/raystack/components/sidebar/sidebar.module.css +++ b/packages/raystack/components/sidebar/sidebar.module.css @@ -205,9 +205,28 @@ } .nav-group-header { - padding: var(--rs-space-3) var(--rs-space-3); + display: flex; + height: var(--rs-space-7); + padding: var(--rs-space-2) var(--rs-space-3); + margin-bottom: var(--rs-space-1); + justify-content: space-between; + align-items: center; + align-self: stretch; color: var(--rs-color-foreground-base-secondary); margin-top: var(--rs-space-4); + border-radius: var(--rs-radius-2); +} + +.nav-group-accordion-item .nav-group-header:hover { + background-color: var(--rs-color-background-base-primary-hover); +} + +.nav-group-header-with-trailing:hover { + background-color: var(--rs-color-background-base-primary-hover); +} + +.nav-group-accordion-item .nav-group-header { + padding: 0; } .nav-group-header:first-child { @@ -223,11 +242,70 @@ letter-spacing: var(--rs-letter-spacing-small); } +.nav-group-trigger { + display: flex; + width: auto; + flex: 1; + height: 100%; + border: 0; + padding: var(--rs-space-2) var(--rs-space-2) var(--rs-space-2) var(--rs-space-3); + background: transparent; + align-items: center; + gap: var(--rs-space-2); + cursor: pointer; + color: inherit; + text-align: left; +} + +.nav-group-chevron { + width: var(--rs-space-4); + height: var(--rs-space-4); + color: var(--rs-color-foreground-base-secondary); + transform: rotate(-90deg); + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.nav-group-trigger[data-panel-open] .nav-group-chevron { + transform: rotate(0deg); +} + +.nav-group-trailing-icon { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--rs-space-2) var(--rs-space-3); + color: var(--rs-color-foreground-base-secondary); + opacity: 0; + transition: opacity 0.2s ease; +} + +.nav-group-header:hover .nav-group-trailing-icon { + opacity: 1; +} + .nav-group-items { gap: var(--rs-space-2); width: 100%; } +.nav-group-accordion, +.nav-group-accordion-item, +.nav-group-panel { + width: 100%; +} + +.nav-group-panel { + height: var(--accordion-panel-height); + overflow: hidden; + transition: height 0.2s ease; +} + +.nav-group-panel[data-starting-style], +.nav-group-panel[data-ending-style] { + height: 0; +} + /* Hide group header text when collapsed but show a separator line in its place */ .root[data-closed] .nav-group-header { visibility: hidden; @@ -256,12 +334,17 @@ /* Keep in flow (no display: none) so header row height is preserved */ } +.root[data-closed] .nav-group-chevron { + display: none; +} + @media (prefers-reduced-motion: reduce) { .root, .nav-item, .nav-text, - .resizeHandle { + .resizeHandle, + .nav-group-panel { transition: none; } } \ No newline at end of file From 7e88d29daae47e2cead1a0944af7b9f18673d4bb Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Fri, 27 Mar 2026 16:27:02 +0530 Subject: [PATCH 2/2] feat: update Sidebar variant to 'floating' and add accordion group demo with collapsible sections --- .../content/docs/components/sidebar/demo.ts | 32 +++++++++++++++++++ .../content/docs/components/sidebar/index.mdx | 7 ++++ 2 files changed, 39 insertions(+) diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts index 0ad382909..035856134 100644 --- a/apps/www/src/content/docs/components/sidebar/demo.ts +++ b/apps/www/src/content/docs/components/sidebar/demo.ts @@ -358,6 +358,38 @@ export const hideTooltipDemo = {
`) }; +export const accordionGroupDemo = { + type: 'code', + code: sidebarLayout(` + + + + + + Apsara + + + + }> + Overview + + + }> + Reports + + }> + Activities + + + }> + }> + Settings + + + + `) +}; + export const moreDemo = { type: 'code', code: sidebarLayout(` diff --git a/apps/www/src/content/docs/components/sidebar/index.mdx b/apps/www/src/content/docs/components/sidebar/index.mdx index 3bc09f882..80b405ddb 100644 --- a/apps/www/src/content/docs/components/sidebar/index.mdx +++ b/apps/www/src/content/docs/components/sidebar/index.mdx @@ -8,6 +8,7 @@ import { preview, positionDemo, variantDemo, + accordionGroupDemo, stateDemo, tooltipDemo, collapsibleDemo, @@ -122,6 +123,12 @@ Set `hideCollapsedItemTooltip` to disable tooltips on navigation items when the +### Accordion Group + +Enable `accordion` on `Sidebar.Group` to make section items collapsible. You can also pass `trailingIcon` for section-level actions. + + + ### More Use `Sidebar.More` when you want to keep a section compact and move secondary items into a menu.