From 37ba4b19adfc89a00c1d97933b75b5402db6b528 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 24 Sep 2025 23:55:41 +0530 Subject: [PATCH 1/2] feat: split sidebar into multiple files --- .../components/sidebar/sidebar-item.tsx | 99 ++++++ .../components/sidebar/sidebar-main.tsx | 23 ++ .../components/sidebar/sidebar-misc.tsx | 69 +++++ .../components/sidebar/sidebar-root.tsx | 116 +++++++ .../raystack/components/sidebar/sidebar.tsx | 288 +----------------- 5 files changed, 314 insertions(+), 281 deletions(-) create mode 100644 packages/raystack/components/sidebar/sidebar-item.tsx create mode 100644 packages/raystack/components/sidebar/sidebar-main.tsx create mode 100644 packages/raystack/components/sidebar/sidebar-misc.tsx create mode 100644 packages/raystack/components/sidebar/sidebar-root.tsx diff --git a/packages/raystack/components/sidebar/sidebar-item.tsx b/packages/raystack/components/sidebar/sidebar-item.tsx new file mode 100644 index 00000000..2155482d --- /dev/null +++ b/packages/raystack/components/sidebar/sidebar-item.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import { + ComponentPropsWithoutRef, + ReactElement, + ReactNode, + cloneElement, + forwardRef, + useContext +} from 'react'; +import { Avatar } from '../avatar'; +import { Flex } from '../flex'; +import { Tooltip } from '../tooltip'; +import { SidebarContext } from './sidebar-root'; +import styles from './sidebar.module.css'; + +export interface SidebarItemProps extends ComponentPropsWithoutRef<'a'> { + leadingIcon?: ReactNode; + active?: boolean; + disabled?: boolean; + as?: ReactElement; + classNames?: { + root?: string; + leadingIcon?: string; + text?: string; + }; +} + +export const SidebarItem = forwardRef( + ( + { + classNames, + leadingIcon, + children, + active, + disabled, + as = , + ...props + }, + ref + ) => { + const { isCollapsed, hideCollapsedItemTooltip } = + useContext(SidebarContext); + + const shouldShowFallback = + leadingIcon == undefined && + isCollapsed && + typeof children === 'string' && + children.length > 0; + + const content = cloneElement( + as, + { + ref, + className: cx(styles['nav-item'], classNames?.root), + 'data-active': active, + 'data-disabled': disabled, + role: 'menuitem', + 'aria-current': active ? 'page' : undefined, + 'aria-disabled': disabled, + ...props + }, + <> + + {!isCollapsed && {children}} + + ); + + if (isCollapsed && !hideCollapsedItemTooltip) { + return ( + + {content} + + ); + } + + return content; + } +); + +SidebarItem.displayName = 'Sidebar.Item'; diff --git a/packages/raystack/components/sidebar/sidebar-main.tsx b/packages/raystack/components/sidebar/sidebar-main.tsx new file mode 100644 index 00000000..cc33fe1b --- /dev/null +++ b/packages/raystack/components/sidebar/sidebar-main.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { ComponentPropsWithoutRef, forwardRef } from 'react'; +import { Flex } from '../flex'; +import styles from './sidebar.module.css'; + +export const SidebarMain = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef<'div'> +>(({ className, children, ...props }, ref) => ( + + {children} + +)); + +SidebarMain.displayName = 'Sidebar.Main'; diff --git a/packages/raystack/components/sidebar/sidebar-misc.tsx b/packages/raystack/components/sidebar/sidebar-misc.tsx new file mode 100644 index 00000000..36984eae --- /dev/null +++ b/packages/raystack/components/sidebar/sidebar-misc.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import { ComponentPropsWithoutRef, ReactNode, forwardRef } from 'react'; +import { Flex } from '../flex'; +import styles from './sidebar.module.css'; + +export const SidebarHeader = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef<'div'> +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +SidebarHeader.displayName = 'Sidebar.Header'; + +export const SidebarFooter = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef<'div'> +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +SidebarFooter.displayName = 'Sidebar.Footer'; + +export interface SidebarNavigationGroupProps + extends ComponentPropsWithoutRef<'div'> { + label: string; + leadingIcon?: ReactNode; +} + +export const SidebarNavigationGroup = forwardRef< + HTMLElement, + SidebarNavigationGroupProps +>(({ className, label, leadingIcon, children, ...props }, ref) => ( +
+ + {leadingIcon && ( + {leadingIcon} + )} + {label} + + + {children} + +
+)); + +SidebarNavigationGroup.displayName = 'Sidebar.Group'; diff --git a/packages/raystack/components/sidebar/sidebar-root.tsx b/packages/raystack/components/sidebar/sidebar-root.tsx new file mode 100644 index 00000000..608f505e --- /dev/null +++ b/packages/raystack/components/sidebar/sidebar-root.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { cva } from 'class-variance-authority'; +import { Collapsible } from 'radix-ui'; +import { + ComponentPropsWithoutRef, + ComponentRef, + createContext, + forwardRef, + useCallback, + useState +} from 'react'; +import { Tooltip } from '../tooltip'; +import styles from './sidebar.module.css'; + +export interface SidebarContextValue { + isCollapsed: boolean; + hideCollapsedItemTooltip?: boolean; +} + +export const SidebarContext = createContext({ + isCollapsed: false +}); + +const root = cva(styles.root); + +export interface SidebarProps + extends ComponentPropsWithoutRef { + position?: 'left' | 'right'; + hideCollapsedItemTooltip?: boolean; + collapsible?: boolean; +} + +export const SidebarRoot = forwardRef< + ComponentRef, + SidebarProps +>( + ( + { + className, + position = 'left', + open: providedOpen, + onOpenChange, + hideCollapsedItemTooltip, + collapsible = true, + defaultOpen, + children, + ...props + }, + ref + ) => { + const [internalOpen, setInternalOpen] = useState(defaultOpen); + + const open = providedOpen ?? internalOpen; + + const handleOpenChange = useCallback( + (value: boolean) => { + setInternalOpen(value); + onOpenChange?.(value); + }, + [onOpenChange] + ); + + return ( + + + + + + + + ); + } +); + +SidebarRoot.displayName = 'Sidebar'; diff --git a/packages/raystack/components/sidebar/sidebar.tsx b/packages/raystack/components/sidebar/sidebar.tsx index 9dc8c9c1..5ab0edde 100644 --- a/packages/raystack/components/sidebar/sidebar.tsx +++ b/packages/raystack/components/sidebar/sidebar.tsx @@ -1,285 +1,11 @@ -'use client'; - -import { cva, cx } from 'class-variance-authority'; -import { Collapsible } from 'radix-ui'; +import { SidebarItem } from './sidebar-item'; +import { SidebarMain } from './sidebar-main'; import { - ComponentPropsWithoutRef, - ComponentRef, - ReactElement, - ReactNode, - cloneElement, - createContext, - forwardRef, - useCallback, - useContext, - useState -} from 'react'; -import { Avatar } from '../avatar'; -import { Flex } from '../flex'; -import { Tooltip } from '../tooltip'; -import styles from './sidebar.module.css'; - -interface SidebarContextValue { - isCollapsed: boolean; - hideCollapsedItemTooltip?: boolean; -} - -const SidebarContext = createContext({ - isCollapsed: false -}); - -const root = cva(styles.root); - -export interface SidebarProps - extends ComponentPropsWithoutRef { - position?: 'left' | 'right'; - hideCollapsedItemTooltip?: boolean; - collapsible?: boolean; -} - -interface SidebarItemProps extends ComponentPropsWithoutRef<'a'> { - leadingIcon?: ReactNode; - active?: boolean; - disabled?: boolean; - as?: ReactElement; - classNames?: { - root?: string; - leadingIcon?: string; - text?: string; - }; -} - -interface SidebarFooterProps extends ComponentPropsWithoutRef<'div'> {} - -interface SidebarNavigationGroupProps extends ComponentPropsWithoutRef<'div'> { - label: string; - leadingIcon?: ReactNode; -} - -const SidebarRoot = forwardRef< - ComponentRef, - SidebarProps ->( - ( - { - className, - position = 'left', - open: providedOpen, - onOpenChange, - hideCollapsedItemTooltip, - collapsible = true, - defaultOpen, - children, - ...props - }, - ref - ) => { - const [internalOpen, setInternalOpen] = useState(defaultOpen); - - const open = providedOpen ?? internalOpen; - - const handleOpenChange = useCallback( - (value: boolean) => { - setInternalOpen(value); - onOpenChange?.(value); - }, - [onOpenChange] - ); - - return ( - - - - - - - - ); - } -); - -const SidebarHeader = forwardRef< - HTMLDivElement, - ComponentPropsWithoutRef<'div'> ->(({ className, children, ...props }, ref) => ( - - {children} - -)); - -const SidebarMain = forwardRef>( - ({ className, children, ...props }, ref) => ( - - {children} - - ) -); - -const SidebarFooter = forwardRef( - ({ className, children, ...props }, ref) => ( - - {children} - - ) -); - -const SidebarItem = forwardRef( - ( - { - classNames, - leadingIcon, - children, - active, - disabled, - as =
, - ...props - }, - ref - ) => { - const { isCollapsed, hideCollapsedItemTooltip } = - useContext(SidebarContext); - - const shouldShowFallback = - leadingIcon == undefined && - isCollapsed && - typeof children === 'string' && - children.length > 0; - - const content = cloneElement( - as, - { - ref, - className: cx(styles['nav-item'], classNames?.root), - 'data-active': active, - 'data-disabled': disabled, - role: 'menuitem', - 'aria-current': active ? 'page' : undefined, - 'aria-disabled': disabled, - ...props - }, - <> - - {!isCollapsed && {children}} - - ); - - if (isCollapsed && !hideCollapsedItemTooltip) { - return ( - - {content} - - ); - } - - return content; - } -); - -const SidebarNavigationGroup = forwardRef< - HTMLElement, - SidebarNavigationGroupProps ->(({ className, label, leadingIcon, children, ...props }, ref) => ( -
- - {leadingIcon && ( - {leadingIcon} - )} - {label} - - - {children} - -
-)); - -SidebarRoot.displayName = 'Sidebar.Root'; -SidebarHeader.displayName = 'Sidebar.Header'; -SidebarMain.displayName = 'Sidebar.Main'; -SidebarFooter.displayName = 'Sidebar.Footer'; -SidebarItem.displayName = 'Sidebar.Item'; -SidebarNavigationGroup.displayName = 'Sidebar.Group'; + SidebarFooter, + SidebarHeader, + SidebarNavigationGroup +} from './sidebar-misc'; +import { SidebarRoot } from './sidebar-root'; export const Sidebar = Object.assign(SidebarRoot, { Header: SidebarHeader, From 8a353a3ac9bd6886b7e5480dbb1151def1e5e127 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 25 Sep 2025 00:18:53 +0530 Subject: [PATCH 2/2] feat: support tooltip message customisation --- .../content/docs/components/sidebar/demo.ts | 21 +++++++++++++++++++ .../content/docs/components/sidebar/index.mdx | 10 ++++++++- .../content/docs/components/sidebar/props.ts | 5 +++++ .../sidebar/__tests__/sidebar.test.tsx | 5 +++-- .../components/sidebar/sidebar-root.tsx | 12 ++++++++--- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts index 4f24794c..bf14b811 100644 --- a/apps/www/src/content/docs/components/sidebar/demo.ts +++ b/apps/www/src/content/docs/components/sidebar/demo.ts @@ -150,3 +150,24 @@ export const stateDemo = { } ] }; + +export const tooltipDemo = { + type: 'code', + code: ` + + + + + + Apsara + + + + } active>Dashboard + } disabled>Settings + + ` +}; diff --git a/apps/www/src/content/docs/components/sidebar/index.mdx b/apps/www/src/content/docs/components/sidebar/index.mdx index 6aa98017..f0aab99c 100644 --- a/apps/www/src/content/docs/components/sidebar/index.mdx +++ b/apps/www/src/content/docs/components/sidebar/index.mdx @@ -3,7 +3,7 @@ title: Sidebar description: A collapsible side navigation panel component. --- -import { preview, positionDemo, stateDemo } from "./demo.ts"; +import { preview, positionDemo, stateDemo, tooltipDemo } from "./demo.ts"; @@ -47,6 +47,14 @@ The `data-collapse-hidden` attribute can be used to conditionally hide elements +### Custom Tooltip Message + +You can customize the tooltip message that appears when hovering over the collapse/expand handle using the `tooltipMessage` prop. + +You can use Sidebar as a controlled component to conditionally render different tooltip messages. + + + ## Accessibility The Sidebar implements the following accessibility features: diff --git a/apps/www/src/content/docs/components/sidebar/props.ts b/apps/www/src/content/docs/components/sidebar/props.ts index 5d267859..bca5abc7 100644 --- a/apps/www/src/content/docs/components/sidebar/props.ts +++ b/apps/www/src/content/docs/components/sidebar/props.ts @@ -24,6 +24,11 @@ export interface SidebarRootProps { * @default false */ hideCollapsedItemTooltip?: boolean; + + /** Custom message for the collapsible tooltip. + * By default, it shows "Click to collapse" when expanded, "Click to expand" when collapsed + */ + tooltipMessage?: ReactNode; } export interface SidebarGroupProps { diff --git a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx index e57bd1c9..12f9c222 100644 --- a/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx +++ b/packages/raystack/components/sidebar/__tests__/sidebar.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { Sidebar, SidebarProps } from '../sidebar'; +import { Sidebar } from '../sidebar'; +import { SidebarRootProps } from '../sidebar-root'; import styles from '../sidebar.module.css'; const HEADER_TEXT = 'Apsara'; @@ -24,7 +25,7 @@ const BasicSidebar = ({ position = 'left', children, ...props -}: SidebarProps) => ( +}: SidebarRootProps) => ( ({ const root = cva(styles.root); -export interface SidebarProps +export interface SidebarRootProps extends ComponentPropsWithoutRef { position?: 'left' | 'right'; hideCollapsedItemTooltip?: boolean; collapsible?: boolean; + tooltipMessage?: ReactNode; } export const SidebarRoot = forwardRef< ComponentRef, - SidebarProps + SidebarRootProps >( ( { @@ -43,6 +45,7 @@ export const SidebarRoot = forwardRef< onOpenChange, hideCollapsedItemTooltip, collapsible = true, + tooltipMessage, defaultOpen, children, ...props @@ -83,7 +86,10 @@ export const SidebarRoot = forwardRef<