From d3e9138a7eb2fecbebf78995e6e20a7433b351a6 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 30 Jan 2026 15:19:49 +0530 Subject: [PATCH] feat: migrate sidebar --- .../content/docs/components/sidebar/props.ts | 6 +- .../sidebar/__tests__/sidebar.test.tsx | 5 +- .../components/sidebar/sidebar-root.tsx | 86 +++++++++---------- .../components/sidebar/sidebar.module.css | 18 ++-- 4 files changed, 55 insertions(+), 60 deletions(-) diff --git a/apps/www/src/content/docs/components/sidebar/props.ts b/apps/www/src/content/docs/components/sidebar/props.ts index bca5abc7e..ccf03e15a 100644 --- a/apps/www/src/content/docs/components/sidebar/props.ts +++ b/apps/www/src/content/docs/components/sidebar/props.ts @@ -7,11 +7,13 @@ export interface SidebarRootProps { /** Callback when expanded/collapsed state changes. */ onOpenChange?: (open: boolean) => void; - /** Default expanded/collapsed state.*/ + /** Default expanded/collapsed state. + * @default true + */ defaultOpen?: boolean; /** Disable the click to collapse/expand the Sidebar. - * @default undefined + * @default true */ collapsible?: boolean; diff --git a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx index 12f9c2227..bbbc6e3e7 100644 --- a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx +++ b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx @@ -1,8 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { Sidebar } from '../sidebar'; -import { SidebarRootProps } from '../sidebar-root'; import styles from '../sidebar.module.css'; +import { SidebarRootProps } from '../sidebar-root'; const HEADER_TEXT = 'Apsara'; const MAIN_GROUP_LABEL = 'Main'; @@ -101,7 +101,8 @@ describe('Sidebar', () => { render(); const nav = screen.getByRole('navigation'); - expect(nav).toHaveAttribute('data-state', 'collapsed'); + expect(nav).toHaveAttribute('data-closed'); + expect(nav).not.toHaveAttribute('data-open'); }); it('does not show handle when not collapsible', () => { diff --git a/packages/raystack/components/sidebar/sidebar-root.tsx b/packages/raystack/components/sidebar/sidebar-root.tsx index d9945cf07..463d44a2a 100644 --- a/packages/raystack/components/sidebar/sidebar-root.tsx +++ b/packages/raystack/components/sidebar/sidebar-root.tsx @@ -1,13 +1,11 @@ 'use client'; import { cx } from 'class-variance-authority'; -import { Collapsible } from 'radix-ui'; import { ComponentPropsWithoutRef, - ComponentRef, - ReactNode, createContext, forwardRef, + ReactNode, useCallback, useState } from 'react'; @@ -23,18 +21,17 @@ export const SidebarContext = createContext({ isCollapsed: false }); -export interface SidebarRootProps - extends ComponentPropsWithoutRef { +export interface SidebarRootProps extends ComponentPropsWithoutRef<'aside'> { position?: 'left' | 'right'; hideCollapsedItemTooltip?: boolean; collapsible?: boolean; tooltipMessage?: ReactNode; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; } -export const SidebarRoot = forwardRef< - ComponentRef, - SidebarRootProps ->( +export const SidebarRoot = forwardRef( ( { className, @@ -44,14 +41,13 @@ export const SidebarRoot = forwardRef< hideCollapsedItemTooltip, collapsible = true, tooltipMessage, - defaultOpen, + defaultOpen = true, children, ...props }, ref ) => { const [internalOpen, setInternalOpen] = useState(defaultOpen); - const open = providedOpen ?? internalOpen; const handleOpenChange = useCallback( @@ -67,50 +63,46 @@ export const SidebarRoot = forwardRef< value={{ isCollapsed: !open, hideCollapsedItemTooltip }} > - - - + {collapsible && ( + +
handleOpenChange(!open)} + role='button' + tabIndex={0} + aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleOpenChange(!open); + } + }} + /> + + )} + {children} + ); diff --git a/packages/raystack/components/sidebar/sidebar.module.css b/packages/raystack/components/sidebar/sidebar.module.css index 018f97c22..ed084adf8 100644 --- a/packages/raystack/components/sidebar/sidebar.module.css +++ b/packages/raystack/components/sidebar/sidebar.module.css @@ -20,11 +20,11 @@ border-left: 1px solid var(--rs-color-border-base-primary); } -.root[data-state="expanded"] { +.root[data-open] { width: 240px; } -.root[data-state="collapsed"] { +.root[data-closed] { width: 57px; } @@ -46,11 +46,11 @@ align-items: flex-start; } -.root[data-state="collapsed"] .main { +.root[data-closed] .main { align-items: center; } -.root[data-state="collapsed"] [data-collapse-hidden="true"] { +.root[data-closed] [data-collapse-hidden] { display: none; } @@ -73,7 +73,7 @@ box-sizing: border-box; } -.root[data-state="collapsed"] .nav-item { +.root[data-closed] .nav-item { justify-content: center; } @@ -119,7 +119,7 @@ transition: opacity 0.2s ease; } -.root[data-state="collapsed"] .nav-text { +.root[data-closed] .nav-text { width: 0; opacity: 0; display: none; @@ -136,7 +136,7 @@ transform: translateX(50%); } -.root[data-collapse-disabled="true"] .resizeHandle { +.root[data-collapse-disabled] .resizeHandle { display: none; } @@ -158,7 +158,7 @@ width: 100%; } -.root[data-state="collapsed"] .nav-group { +.root[data-closed] .nav-group { align-items: center; } @@ -186,7 +186,7 @@ width: 100%; } -.root[data-state="collapsed"] .nav-group-label { +.root[data-closed] .nav-group-label { width: 0; opacity: 0; display: none;