Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions apps/www/src/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,28 @@ const Page = () => {
Analytics
</Sidebar.Item>

<Sidebar.Group label='Resources'>
<Sidebar.Group
label='Resources'
accordion
trailingIcon={
<button
type='button'
onClick={() => 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'
}}
>
<DotsHorizontalIcon width={16} height={16} />
</button>
}
>
<Sidebar.Item href='#' leadingIcon={<FileTextIcon />}>
Reports
</Sidebar.Item>
Expand All @@ -132,7 +153,27 @@ const Page = () => {
</Sidebar.More>
</Sidebar.Group>

<Sidebar.Group label='Account'>
<Sidebar.Group
label='Account'
trailingIcon={
<button
type='button'
onClick={() => 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'
}}
>
<DotsHorizontalIcon width={16} height={16} />
</button>
}
>
<Sidebar.Item href='#' leadingIcon={<GearIcon />}>
Settings
</Sidebar.Item>
Expand Down
32 changes: 32 additions & 0 deletions apps/www/src/content/docs/components/sidebar/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,38 @@ export const hideTooltipDemo = {
</Sidebar>`)
};

export const accordionGroupDemo = {
type: 'code',
code: sidebarLayout(`<Sidebar defaultOpen>
<Sidebar.Header>
<Flex align="center" gap={3}>
<IconButton size={4} aria-label="Logo">
<BellIcon width={24} height={24} />
</IconButton>
<Text size={4} weight="medium" data-collapse-hidden>Apsara</Text>
</Flex>
</Sidebar.Header>
<Sidebar.Main>
<Sidebar.Item href="#" leadingIcon={<OrganizationIcon width={16} height={16} />}>
Overview
</Sidebar.Item>
<Sidebar.Group label="Resources" accordion>
<Sidebar.Item href="#" leadingIcon={<FilterIcon width={16} height={16} />}>
Reports
</Sidebar.Item>
<Sidebar.Item href="#" leadingIcon={<OrganizationIcon width={16} height={16} />}>
Activities
</Sidebar.Item>
</Sidebar.Group>
<Sidebar.Group label="Account" trailingIcon={<OrganizationIcon width={16} height={16} />}>
<Sidebar.Item href="#" leadingIcon={<OrganizationIcon width={16} height={16} />}>
Settings
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>`)
};

export const moreDemo = {
type: 'code',
code: sidebarLayout(`<Sidebar defaultOpen>
Expand Down
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/components/sidebar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
preview,
positionDemo,
variantDemo,
accordionGroupDemo,
stateDemo,
tooltipDemo,
collapsibleDemo,
Expand Down Expand Up @@ -122,6 +123,12 @@ Set `hideCollapsedItemTooltip` to disable tooltips on navigation items when the

<Demo data={hideTooltipDemo} />

### Accordion Group

Enable `accordion` on `Sidebar.Group` to make section items collapsible. You can also pass `trailingIcon` for section-level actions.

<Demo data={accordionGroupDemo} />

### More

Use `Sidebar.More` when you want to keep a section compact and move secondary items into a menu.
Expand Down
148 changes: 148 additions & 0 deletions packages/raystack/components/sidebar/__tests__/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Sidebar>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
leadingIcon={<TestIcon />}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

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(
<Sidebar>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
leadingIcon={<TestIcon />}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

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(
<Sidebar open>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
leadingIcon={<TestIcon />}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

const trigger = screen.getByRole('button', { name: /Main/ });
fireEvent.click(trigger);
expect(screen.queryByText(DASHBOARD_ITEM_TEXT)).not.toBeInTheDocument();

rerender(
<Sidebar open={false}>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
leadingIcon={<TestIcon />}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

expect(
screen.getByRole('listitem', { name: DASHBOARD_ITEM_TEXT })
).toBeInTheDocument();
});

it('renders right icon when provided in accordion header', () => {
render(
<Sidebar>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
trailingIcon={<span data-testid='group-trailing-icon'>+</span>}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

expect(screen.getByTestId('group-trailing-icon')).toBeInTheDocument();
});

it('does not toggle accordion when trailing icon is clicked', () => {
const onTrailingIconClick = vi.fn();

render(
<Sidebar>
<Sidebar.Main>
<Sidebar.Group
label={MAIN_GROUP_LABEL}
accordion
trailingIcon={
<button
type='button'
data-testid='group-trailing-action'
onClick={onTrailingIconClick}
>
+
</button>
}
>
<Sidebar.Item href='#' leadingIcon={<InfoIcon />}>
{DASHBOARD_ITEM_TEXT}
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Main>
</Sidebar>
);

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', () => {
Expand Down
Loading