From 7515ade24860316e9c8809e911b25a0a2dc94d23 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 18 Sep 2025 16:39:41 +0530 Subject: [PATCH 1/6] wip: accordion component --- .../accordion/__tests__/accordion.test.tsx | 383 ++++++++++++++++++ .../components/accordion/accordion.module.css | 136 +++++++ .../components/accordion/accordion.tsx | 140 +++++++ .../raystack/components/accordion/index.tsx | 7 + packages/raystack/index.tsx | 1 + 5 files changed, 667 insertions(+) create mode 100644 packages/raystack/components/accordion/__tests__/accordion.test.tsx create mode 100644 packages/raystack/components/accordion/accordion.module.css create mode 100644 packages/raystack/components/accordion/accordion.tsx create mode 100644 packages/raystack/components/accordion/index.tsx diff --git a/packages/raystack/components/accordion/__tests__/accordion.test.tsx b/packages/raystack/components/accordion/__tests__/accordion.test.tsx new file mode 100644 index 00000000..a8f00ea6 --- /dev/null +++ b/packages/raystack/components/accordion/__tests__/accordion.test.tsx @@ -0,0 +1,383 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { Accordion } from '../accordion'; +import styles from '../accordion.module.css'; + +describe('Accordion', () => { + describe('Basic Rendering', () => { + it('renders accordion with children', () => { + render( + + + Item 1 + Content 1 + + + ); + + expect( + screen.getByRole('button', { name: 'Item 1' }) + ).toBeInTheDocument(); + expect(screen.getByText('Content 1')).toBeInTheDocument(); + }); + + it('renders multiple accordion items', () => { + render( + + + Item 1 + Content 1 + + + Item 2 + Content 2 + + + ); + + expect( + screen.getByRole('button', { name: 'Item 1' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Item 2' }) + ).toBeInTheDocument(); + expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.getByText('Content 2')).toBeInTheDocument(); + }); + + it('applies custom className to accordion', () => { + render( + + + Item 1 + Content 1 + + + ); + + const accordion = screen.getByRole('region'); + expect(accordion).toHaveClass('custom-accordion'); + expect(accordion).toHaveClass(styles.accordion); + }); + + it('forwards ref correctly', () => { + const ref = vi.fn(); + render( + + + Item 1 + Content 1 + + + ); + expect(ref).toHaveBeenCalled(); + }); + }); + + describe('AccordionItem', () => { + it('renders with correct data attributes', () => { + render( + + + Item 1 + Content 1 + + + ); + + const item = screen + .getByRole('button', { name: 'Item 1' }) + .closest('[data-slot="accordion-item"]'); + expect(item).toHaveClass('custom-item'); + expect(item).toHaveClass(styles['accordion-item']); + }); + + it('forwards ref correctly', () => { + const ref = vi.fn(); + render( + + + Item 1 + Content 1 + + + ); + expect(ref).toHaveBeenCalled(); + }); + }); + + describe('AccordionTrigger', () => { + it('renders with correct data attributes', () => { + render( + + + + Item 1 + + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + expect(trigger).toHaveClass('custom-trigger'); + expect(trigger).toHaveClass(styles['accordion-trigger']); + }); + + it('renders with chevron icon', () => { + render( + + + Item 1 + Content 1 + + + ); + + const icon = screen + .getByRole('button', { name: 'Item 1' }) + .querySelector('svg'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass(styles['accordion-icon']); + }); + + it('forwards ref correctly', () => { + const ref = vi.fn(); + render( + + + Item 1 + Content 1 + + + ); + expect(ref).toHaveBeenCalled(); + }); + + it('handles click events', () => { + const handleClick = vi.fn(); + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + fireEvent.click(trigger); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('handles disabled state', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + expect(trigger).toBeDisabled(); + }); + }); + + describe('AccordionContent', () => { + it('renders with correct data attributes', () => { + render( + + + Item 1 + + Content 1 + + + + ); + + const content = screen + .getByText('Content 1') + .closest('[data-slot="accordion-content"]'); + expect(content).toHaveClass('custom-content'); + expect(content).toHaveClass(styles['accordion-content']); + }); + + it('forwards ref correctly', () => { + const ref = vi.fn(); + render( + + + Item 1 + Content 1 + + + ); + expect(ref).toHaveBeenCalled(); + }); + }); + + describe('Size Variants', () => { + const sizes = ['small', 'medium', 'large'] as const; + + it.each(sizes)('renders %s size correctly', size => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + expect(trigger).toHaveClass(styles[`accordion-trigger-${size}`]); + }); + + it('defaults to medium size', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + expect(trigger).toHaveClass(styles['accordion-trigger-medium']); + }); + }); + + describe('Interaction', () => { + it('expands and collapses content on trigger click', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + + // Initially closed + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + + // Click to open + fireEvent.click(trigger); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + + // Click to close + fireEvent.click(trigger); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + }); + + it('rotates icon when expanded', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + + // Initially not rotated + expect(trigger).not.toHaveAttribute('data-state', 'open'); + + // Click to open + fireEvent.click(trigger); + expect(trigger).toHaveAttribute('data-state', 'open'); + }); + }); + + describe('Accessibility', () => { + it('has proper ARIA attributes', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + expect(trigger).toHaveAttribute('aria-controls'); + }); + + it('supports keyboard navigation', () => { + render( + + + Item 1 + Content 1 + + + ); + + const trigger = screen.getByRole('button', { name: 'Item 1' }); + + // Focus should work + trigger.focus(); + expect(trigger).toHaveFocus(); + + // Enter key should toggle + fireEvent.keyDown(trigger, { key: 'Enter', code: 'Enter' }); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + }); + + it('supports aria-label on trigger', () => { + render( + + + + Item 1 + + Content 1 + + + ); + + expect(screen.getByLabelText('Custom label')).toBeInTheDocument(); + }); + }); + + describe('Multiple Items', () => { + it('handles multiple accordion items independently', () => { + render( + + + Item 1 + Content 1 + + + Item 2 + Content 2 + + + ); + + const trigger1 = screen.getByRole('button', { name: 'Item 1' }); + const trigger2 = screen.getByRole('button', { name: 'Item 2' }); + + // Open first item + fireEvent.click(trigger1); + expect(trigger1).toHaveAttribute('aria-expanded', 'true'); + expect(trigger2).toHaveAttribute('aria-expanded', 'false'); + + // Open second item + fireEvent.click(trigger2); + expect(trigger1).toHaveAttribute('aria-expanded', 'true'); + expect(trigger2).toHaveAttribute('aria-expanded', 'true'); + }); + }); +}); diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css new file mode 100644 index 00000000..6fc15c13 --- /dev/null +++ b/packages/raystack/components/accordion/accordion.module.css @@ -0,0 +1,136 @@ +.accordion { + width: 100%; +} + +.accordion-item { + border-bottom: 1px solid var(--rs-color-border-base-tertiary); +} + +.accordion-item:last-child { + border-bottom: none; +} + +.accordion-header { + display: flex; +} + +.accordion-trigger { + font-family: var(--rs-font-body); + font-weight: var(--rs-font-weight-medium); + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-primary); + background-color: transparent; + border: none; + cursor: pointer; + display: flex; + flex: 1; + align-items: flex-start; + justify-content: space-between; + gap: var(--rs-space-4); + padding: var(--rs-space-4) 0; + text-align: left; + outline: none; + transition: all 0.2s ease-in-out; + width: 100%; +} + +.accordion-trigger:hover { + text-decoration: underline; +} + +.accordion-trigger:focus-visible { + outline: 1px solid var(--rs-color-border-accent-emphasis); + outline-offset: 2px; +} + +.accordion-trigger:disabled { + pointer-events: none; + opacity: 0.5; +} + +.accordion-trigger[data-state="open"] .accordion-icon { + transform: rotate(90deg); +} + +.accordion-icon { + color: var(--rs-color-foreground-base-tertiary); + pointer-events: none; + flex-shrink: 0; + transform: translateY(2px); + transition: transform 0.2s ease-in-out; + width: 16px; + height: 16px; +} + +.accordion-content { + overflow: hidden; + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-secondary); +} + +.accordion-content[data-state="closed"] { + animation: accordion-up 0.2s ease-out; +} + +.accordion-content[data-state="open"] { + animation: accordion-down 0.2s ease-out; +} + +.accordion-content-inner { + padding: 0 0 var(--rs-space-4) 0; +} + +/* Size variants */ +.accordion-trigger-small { + font-size: var(--rs-font-size-mini); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); + padding: var(--rs-space-3) 0; +} + +.accordion-trigger-small .accordion-icon { + width: 14px; + height: 14px; +} + +.accordion-trigger-medium { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + padding: var(--rs-space-4) 0; +} + +.accordion-trigger-large { + font-size: var(--rs-font-size-regular); + line-height: var(--rs-line-height-regular); + letter-spacing: var(--rs-letter-spacing-regular); + padding: var(--rs-space-5) 0; +} + +.accordion-trigger-large .accordion-icon { + width: 18px; + height: 18px; +} + +/* Animations */ +@keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } +} + +@keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} diff --git a/packages/raystack/components/accordion/accordion.tsx b/packages/raystack/components/accordion/accordion.tsx new file mode 100644 index 00000000..50addd01 --- /dev/null +++ b/packages/raystack/components/accordion/accordion.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { type VariantProps, cva } from 'class-variance-authority'; +import { Accordion as AccordionPrimitive } from 'radix-ui'; +import { ElementRef, ReactNode, forwardRef } from 'react'; + +import { TriangleRightIcon } from '~/icons'; +import styles from './accordion.module.css'; + +const root = cva(styles.accordion); +const item = cva(styles['accordion-item']); +const trigger = cva(styles['accordion-trigger'], { + variants: { + size: { + small: styles['accordion-trigger-small'], + medium: styles['accordion-trigger-medium'], + large: styles['accordion-trigger-large'] + } + }, + defaultVariants: { + size: 'medium' + } +}); +const content = cva(styles['accordion-content']); + +interface CommonAccordionProps { + children: ReactNode; + className?: string; +} + +interface SingleAccordionProps extends Omit { + type: 'single'; + collapsible?: boolean; + defaultValue?: string; + value?: string; + onValueChange?: (value: string) => void; +} + +interface MultipleAccordionProps extends Omit { + type: 'multiple'; + defaultValue?: string[]; + value?: string[]; + onValueChange?: (value: string[]) => void; +} + +export type AccordionRootProps = SingleAccordionProps | MultipleAccordionProps; + +export interface AccordionItemProps + extends AccordionPrimitive.AccordionItemProps { + children: ReactNode; + className?: string; +} + +export interface AccordionTriggerProps + extends AccordionPrimitive.AccordionTriggerProps, + VariantProps { + children: ReactNode; + className?: string; +} + +export interface AccordionContentProps + extends AccordionPrimitive.AccordionContentProps { + children: ReactNode; + className?: string; +} + +const AccordionRootWithRef = forwardRef< + ElementRef, + AccordionRootProps +>((props, ref) => { + const { className, children, type = 'single', ...restProps } = props; + + return ( + + {children} + + ); +}); + +const AccordionItem = forwardRef< + ElementRef, + AccordionItemProps +>(({ className, children, ...props }, ref) => ( + + {children} + +)); + +const AccordionTrigger = forwardRef< + ElementRef, + AccordionTriggerProps +>(({ className, children, size, ...props }, ref) => ( + + + {children} + + + +)); + +const AccordionContent = forwardRef< + ElementRef, + AccordionContentProps +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionRootWithRef.displayName = AccordionPrimitive.Root.displayName; +AccordionItem.displayName = AccordionPrimitive.Item.displayName; +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export const Accordion = Object.assign(AccordionRootWithRef, { + Item: AccordionItem, + Trigger: AccordionTrigger, + Content: AccordionContent +}); diff --git a/packages/raystack/components/accordion/index.tsx b/packages/raystack/components/accordion/index.tsx new file mode 100644 index 00000000..310dc1af --- /dev/null +++ b/packages/raystack/components/accordion/index.tsx @@ -0,0 +1,7 @@ +export { + Accordion, + type AccordionRootProps, + type AccordionItemProps, + type AccordionTriggerProps, + type AccordionContentProps +} from './accordion'; diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx index e3ad71ae..481bab61 100644 --- a/packages/raystack/index.tsx +++ b/packages/raystack/index.tsx @@ -1,6 +1,7 @@ import './styles/index.css'; import './normalize.css'; +export { Accordion } from './components/accordion'; export { Amount } from './components/amount'; export { AnnouncementBar } from './components/announcement-bar'; export { Avatar, AvatarGroup, getAvatarColor } from './components/avatar'; From a2f32524cfa14b33a4ffe724e5d46ac7070f367f Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 18 Sep 2025 23:54:33 +0530 Subject: [PATCH 2/6] feat: accordion component --- .../components/accordion/accordion.module.css | 106 ++++++------------ .../components/accordion/accordion.tsx | 102 +++++++---------- 2 files changed, 74 insertions(+), 134 deletions(-) diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css index 6fc15c13..0b652b8b 100644 --- a/packages/raystack/components/accordion/accordion.module.css +++ b/packages/raystack/components/accordion/accordion.module.css @@ -1,13 +1,6 @@ .accordion { width: 100%; -} - -.accordion-item { - border-bottom: 1px solid var(--rs-color-border-base-tertiary); -} - -.accordion-item:last-child { - border-bottom: none; + color: var(--rs-color-foreground-base-primary); } .accordion-header { @@ -15,34 +8,30 @@ } .accordion-trigger { - font-family: var(--rs-font-body); - font-weight: var(--rs-font-weight-medium); - font-size: var(--rs-font-size-small); - line-height: var(--rs-line-height-small); - letter-spacing: var(--rs-letter-spacing-small); - color: var(--rs-color-foreground-base-primary); - background-color: transparent; - border: none; - cursor: pointer; display: flex; - flex: 1; - align-items: flex-start; + min-height: 36px; + padding: 0 var(--rs-space-4); justify-content: space-between; - gap: var(--rs-space-4); - padding: var(--rs-space-4) 0; + align-items: center; + gap: var(--rs-space-5); + flex-shrink: 0; + flex: 1; + border: 0.5px solid var(--rs-color-border-base-primary); + background: var(--rs-color-background-base-primary); + cursor: pointer; text-align: left; outline: none; - transition: all 0.2s ease-in-out; - width: 100%; + font-size: var(--rs-font-size-regular); + line-height: var(--rs-line-height-regular); + letter-spacing: var(--rs-letter-spacing-regular); } - -.accordion-trigger:hover { - text-decoration: underline; +.accordion-item:not(:first-child) .accordion-trigger { + border-top: none; } +.accordion-trigger:hover, .accordion-trigger:focus-visible { - outline: 1px solid var(--rs-color-border-accent-emphasis); - outline-offset: 2px; + background-color: var(--rs-color-background-base-primary-hover); } .accordion-trigger:disabled { @@ -51,69 +40,42 @@ } .accordion-trigger[data-state="open"] .accordion-icon { - transform: rotate(90deg); + transform: rotate(180deg); } .accordion-icon { - color: var(--rs-color-foreground-base-tertiary); + color: var(--rs-color-foreground-base-secondary); pointer-events: none; flex-shrink: 0; - transform: translateY(2px); - transition: transform 0.2s ease-in-out; - width: 16px; - height: 16px; + transition: transform 150ms ease-in-out; + width: var(--rs-space-4); + height: var(--rs-space-4); } .accordion-content { overflow: hidden; - font-size: var(--rs-font-size-small); - line-height: var(--rs-line-height-small); - letter-spacing: var(--rs-letter-spacing-small); - color: var(--rs-color-foreground-base-secondary); -} - -.accordion-content[data-state="closed"] { - animation: accordion-up 0.2s ease-out; -} - -.accordion-content[data-state="open"] { - animation: accordion-down 0.2s ease-out; } .accordion-content-inner { - padding: 0 0 var(--rs-space-4) 0; -} - -/* Size variants */ -.accordion-trigger-small { - font-size: var(--rs-font-size-mini); - line-height: var(--rs-line-height-mini); - letter-spacing: var(--rs-letter-spacing-mini); - padding: var(--rs-space-3) 0; -} - -.accordion-trigger-small .accordion-icon { - width: 14px; - height: 14px; -} - -.accordion-trigger-medium { + border: 0.5px solid var(--rs-color-border-base-primary); + background: var(--rs-color-background-base-primary); + box-shadow: var(--rs-shadow-inset); font-size: var(--rs-font-size-small); line-height: var(--rs-line-height-small); letter-spacing: var(--rs-letter-spacing-small); - padding: var(--rs-space-4) 0; + display: flex; + align-items: center; + gap: var(--rs-space-5); + align-self: stretch; + padding: var(--rs-space-5) var(--rs-space-4); } -.accordion-trigger-large { - font-size: var(--rs-font-size-regular); - line-height: var(--rs-line-height-regular); - letter-spacing: var(--rs-letter-spacing-regular); - padding: var(--rs-space-5) 0; +.accordion-content[data-state="closed"] { + animation: accordion-up 200ms ease-out; } -.accordion-trigger-large .accordion-icon { - width: 18px; - height: 18px; +.accordion-content[data-state="open"] { + animation: accordion-down 200ms ease-out; } /* Animations */ diff --git a/packages/raystack/components/accordion/accordion.tsx b/packages/raystack/components/accordion/accordion.tsx index 50addd01..7f2976d4 100644 --- a/packages/raystack/components/accordion/accordion.tsx +++ b/packages/raystack/components/accordion/accordion.tsx @@ -1,49 +1,24 @@ 'use client'; -import { type VariantProps, cva } from 'class-variance-authority'; +import { ChevronDownIcon } from '@radix-ui/react-icons'; +import { cx } from 'class-variance-authority'; import { Accordion as AccordionPrimitive } from 'radix-ui'; import { ElementRef, ReactNode, forwardRef } from 'react'; - -import { TriangleRightIcon } from '~/icons'; import styles from './accordion.module.css'; -const root = cva(styles.accordion); -const item = cva(styles['accordion-item']); -const trigger = cva(styles['accordion-trigger'], { - variants: { - size: { - small: styles['accordion-trigger-small'], - medium: styles['accordion-trigger-medium'], - large: styles['accordion-trigger-large'] - } - }, - defaultVariants: { - size: 'medium' - } -}); -const content = cva(styles['accordion-content']); - -interface CommonAccordionProps { - children: ReactNode; - className?: string; -} - -interface SingleAccordionProps extends Omit { - type: 'single'; - collapsible?: boolean; - defaultValue?: string; - value?: string; - onValueChange?: (value: string) => void; -} - -interface MultipleAccordionProps extends Omit { +type AccordionSingleProps = Omit< + AccordionPrimitive.AccordionSingleProps, + 'type' +> & { + type?: 'single'; +}; +type AccordionMultipleProps = Omit< + AccordionPrimitive.AccordionMultipleProps, + 'type' +> & { type: 'multiple'; - defaultValue?: string[]; - value?: string[]; - onValueChange?: (value: string[]) => void; -} - -export type AccordionRootProps = SingleAccordionProps | MultipleAccordionProps; +}; +export type AccordionRootProps = AccordionSingleProps | AccordionMultipleProps; export interface AccordionItemProps extends AccordionPrimitive.AccordionItemProps { @@ -52,8 +27,7 @@ export interface AccordionItemProps } export interface AccordionTriggerProps - extends AccordionPrimitive.AccordionTriggerProps, - VariantProps { + extends AccordionPrimitive.AccordionTriggerProps { children: ReactNode; className?: string; } @@ -64,22 +38,27 @@ export interface AccordionContentProps className?: string; } -const AccordionRootWithRef = forwardRef< +const AccordionRoot = forwardRef< ElementRef, AccordionRootProps ->((props, ref) => { - const { className, children, type = 'single', ...restProps } = props; +>(({ className, type = 'single', ...rest }, ref) => { + // this is a workaround to properly typecast the union type + const singleProps = { + type: 'single', + collapsible: true, + ...rest + } as AccordionPrimitive.AccordionSingleProps; + const multipleProps = { + type: 'multiple', + ...rest + } as AccordionPrimitive.AccordionMultipleProps; return ( - {children} - + className={cx(styles.accordion, className)} + {...(type === 'multiple' ? multipleProps : singleProps)} + /> ); }); @@ -89,8 +68,7 @@ const AccordionItem = forwardRef< >(({ className, children, ...props }, ref) => ( {children} @@ -100,16 +78,15 @@ const AccordionItem = forwardRef< const AccordionTrigger = forwardRef< ElementRef, AccordionTriggerProps ->(({ className, children, size, ...props }, ref) => ( +>(({ className, children, ...props }, ref) => ( {children} - + )); @@ -120,20 +97,21 @@ const AccordionContent = forwardRef< >(({ className, children, ...props }, ref) => ( -
{children}
+
+ {children} +
)); -AccordionRootWithRef.displayName = AccordionPrimitive.Root.displayName; +AccordionRoot.displayName = AccordionPrimitive.Root.displayName; AccordionItem.displayName = AccordionPrimitive.Item.displayName; AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; AccordionContent.displayName = AccordionPrimitive.Content.displayName; -export const Accordion = Object.assign(AccordionRootWithRef, { +export const Accordion = Object.assign(AccordionRoot, { Item: AccordionItem, Trigger: AccordionTrigger, Content: AccordionContent From b9ac2c7cb1d9a9006a52f09d6f35ae30d377ae79 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 19 Sep 2025 00:58:11 +0530 Subject: [PATCH 3/6] feat: add accordion tests --- .../accordion/__tests__/accordion.test.tsx | 338 +++++------------- 1 file changed, 97 insertions(+), 241 deletions(-) diff --git a/packages/raystack/components/accordion/__tests__/accordion.test.tsx b/packages/raystack/components/accordion/__tests__/accordion.test.tsx index a8f00ea6..d1bbaa8a 100644 --- a/packages/raystack/components/accordion/__tests__/accordion.test.tsx +++ b/packages/raystack/components/accordion/__tests__/accordion.test.tsx @@ -1,63 +1,64 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { Accordion } from '../accordion'; +import { AccordionRootProps } from '../accordion-root'; import styles from '../accordion.module.css'; +const ITEM_1_TEXT = 'Item 1'; +const ITEM_2_TEXT = 'Item 2'; +const CONTENT_1_TEXT = 'Content 1'; +const CONTENT_2_TEXT = 'Content 2'; + +const BasicAccordion = ({ + hasDisabledItem = false, + children, + ...props +}: AccordionRootProps & { hasDisabledItem?: boolean }) => { + return ( + + + {ITEM_1_TEXT} + {CONTENT_1_TEXT} + + + {ITEM_2_TEXT} + {CONTENT_2_TEXT} + + {children} + + ); +}; + describe('Accordion', () => { describe('Basic Rendering', () => { it('renders accordion with children', () => { - render( - - - Item 1 - Content 1 - - - ); + render(); expect( - screen.getByRole('button', { name: 'Item 1' }) + screen.getByRole('button', { name: ITEM_1_TEXT }) ).toBeInTheDocument(); - expect(screen.getByText('Content 1')).toBeInTheDocument(); + expect(screen.getByText(CONTENT_1_TEXT)).toBeInTheDocument(); }); it('renders multiple accordion items', () => { - render( - - - Item 1 - Content 1 - - - Item 2 - Content 2 - - - ); + render(); expect( - screen.getByRole('button', { name: 'Item 1' }) + screen.getByRole('button', { name: ITEM_1_TEXT }) ).toBeInTheDocument(); expect( - screen.getByRole('button', { name: 'Item 2' }) + screen.getByRole('button', { name: ITEM_2_TEXT }) ).toBeInTheDocument(); - expect(screen.getByText('Content 1')).toBeInTheDocument(); - expect(screen.getByText('Content 2')).toBeInTheDocument(); }); it('applies custom className to accordion', () => { render( - - - Item 1 - Content 1 - - + ); - const accordion = screen.getByRole('region'); + const accordion = screen.getByTestId('custom'); expect(accordion).toHaveClass('custom-accordion'); - expect(accordion).toHaveClass(styles.accordion); }); it('forwards ref correctly', () => { @@ -65,40 +66,51 @@ describe('Accordion', () => { render( - Item 1 - Content 1 + {ITEM_1_TEXT} + {CONTENT_1_TEXT} ); expect(ref).toHaveBeenCalled(); }); - }); - describe('AccordionItem', () => { - it('renders with correct data attributes', () => { - render( - - - Item 1 - Content 1 - - - ); + it('expands and collapses content on trigger click', () => { + render(); + + const trigger = screen.getByRole('button', { name: ITEM_1_TEXT }); + + // Initially closed + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + + // Click to open + fireEvent.click(trigger); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + + // Click to close + fireEvent.click(trigger); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + }); + + it('supports keyboard navigation', async () => { + const user = userEvent.setup(); + render(); - const item = screen - .getByRole('button', { name: 'Item 1' }) - .closest('[data-slot="accordion-item"]'); - expect(item).toHaveClass('custom-item'); - expect(item).toHaveClass(styles['accordion-item']); + const trigger = screen.getByRole('button', { name: ITEM_2_TEXT }); + + await user.keyboard('{Tab}{ArrowDown}{Enter}'); + + expect(trigger).toHaveAttribute('aria-expanded', 'true'); }); + }); + describe('AccordionItem', () => { it('forwards ref correctly', () => { const ref = vi.fn(); render( - Item 1 - Content 1 + {ITEM_1_TEXT} + {CONTENT_1_TEXT} ); @@ -107,35 +119,30 @@ describe('Accordion', () => { }); describe('AccordionTrigger', () => { - it('renders with correct data attributes', () => { + it('renders with custom className', () => { render( - - - Item 1 + + + {ITEM_1_TEXT} - Content 1 + {CONTENT_1_TEXT} ); - const trigger = screen.getByRole('button', { name: 'Item 1' }); + const trigger = screen.getByTestId('custom-trigger'); expect(trigger).toHaveClass('custom-trigger'); - expect(trigger).toHaveClass(styles['accordion-trigger']); }); it('renders with chevron icon', () => { - render( - - - Item 1 - Content 1 - - - ); + render(); const icon = screen - .getByRole('button', { name: 'Item 1' }) + .getByRole('button', { name: ITEM_1_TEXT }) .querySelector('svg'); expect(icon).toBeInTheDocument(); expect(icon).toHaveClass(styles['accordion-icon']); @@ -146,8 +153,8 @@ describe('Accordion', () => { render( - Item 1 - Content 1 + {ITEM_1_TEXT} + {CONTENT_1_TEXT} ); @@ -159,50 +166,42 @@ describe('Accordion', () => { render( - Item 1 - Content 1 + + {ITEM_1_TEXT} + + {CONTENT_1_TEXT} ); - const trigger = screen.getByRole('button', { name: 'Item 1' }); + const trigger = screen.getByRole('button', { name: ITEM_1_TEXT }); fireEvent.click(trigger); expect(handleClick).toHaveBeenCalledTimes(1); }); it('handles disabled state', () => { - render( - - - Item 1 - Content 1 - - - ); + render(); - const trigger = screen.getByRole('button', { name: 'Item 1' }); + const trigger = screen.getByRole('button', { name: ITEM_2_TEXT }); expect(trigger).toBeDisabled(); }); }); describe('AccordionContent', () => { - it('renders with correct data attributes', () => { + it('renders with correct className', () => { render( - - - Item 1 + + + {ITEM_1_TEXT} - Content 1 + {CONTENT_1_TEXT} ); - const content = screen - .getByText('Content 1') - .closest('[data-slot="accordion-content"]'); + const content = screen.getByText(CONTENT_1_TEXT).closest('div'); expect(content).toHaveClass('custom-content'); - expect(content).toHaveClass(styles['accordion-content']); }); it('forwards ref correctly', () => { @@ -210,8 +209,8 @@ describe('Accordion', () => { render( - Item 1 - Content 1 + {ITEM_1_TEXT} + {CONTENT_1_TEXT} ); @@ -219,155 +218,12 @@ describe('Accordion', () => { }); }); - describe('Size Variants', () => { - const sizes = ['small', 'medium', 'large'] as const; - - it.each(sizes)('renders %s size correctly', size => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - expect(trigger).toHaveClass(styles[`accordion-trigger-${size}`]); - }); - - it('defaults to medium size', () => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - expect(trigger).toHaveClass(styles['accordion-trigger-medium']); - }); - }); - - describe('Interaction', () => { - it('expands and collapses content on trigger click', () => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - - // Initially closed - expect(trigger).toHaveAttribute('aria-expanded', 'false'); - - // Click to open - fireEvent.click(trigger); - expect(trigger).toHaveAttribute('aria-expanded', 'true'); - - // Click to close - fireEvent.click(trigger); - expect(trigger).toHaveAttribute('aria-expanded', 'false'); - }); - - it('rotates icon when expanded', () => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - - // Initially not rotated - expect(trigger).not.toHaveAttribute('data-state', 'open'); - - // Click to open - fireEvent.click(trigger); - expect(trigger).toHaveAttribute('data-state', 'open'); - }); - }); - - describe('Accessibility', () => { - it('has proper ARIA attributes', () => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - - expect(trigger).toHaveAttribute('aria-expanded', 'false'); - expect(trigger).toHaveAttribute('aria-controls'); - }); - - it('supports keyboard navigation', () => { - render( - - - Item 1 - Content 1 - - - ); - - const trigger = screen.getByRole('button', { name: 'Item 1' }); - - // Focus should work - trigger.focus(); - expect(trigger).toHaveFocus(); - - // Enter key should toggle - fireEvent.keyDown(trigger, { key: 'Enter', code: 'Enter' }); - expect(trigger).toHaveAttribute('aria-expanded', 'true'); - }); - - it('supports aria-label on trigger', () => { - render( - - - - Item 1 - - Content 1 - - - ); - - expect(screen.getByLabelText('Custom label')).toBeInTheDocument(); - }); - }); - describe('Multiple Items', () => { it('handles multiple accordion items independently', () => { - render( - - - Item 1 - Content 1 - - - Item 2 - Content 2 - - - ); + render(); - const trigger1 = screen.getByRole('button', { name: 'Item 1' }); - const trigger2 = screen.getByRole('button', { name: 'Item 2' }); + const trigger1 = screen.getByRole('button', { name: ITEM_1_TEXT }); + const trigger2 = screen.getByRole('button', { name: ITEM_2_TEXT }); // Open first item fireEvent.click(trigger1); From 91a39eb7236d4746e9bc3f39a7ec64763f9d5c17 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 19 Sep 2025 00:58:49 +0530 Subject: [PATCH 4/6] feat: strucuture accordion into separate files --- .../accordion/accordion-content.tsx | 29 ++++++++++++ .../components/accordion/accordion-item.tsx | 27 +++++++++++ .../components/accordion/accordion-root.tsx | 46 +++++++++++++++++++ .../accordion/accordion-trigger.tsx | 31 +++++++++++++ .../raystack/components/accordion/index.tsx | 8 +--- 5 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 packages/raystack/components/accordion/accordion-content.tsx create mode 100644 packages/raystack/components/accordion/accordion-item.tsx create mode 100644 packages/raystack/components/accordion/accordion-root.tsx create mode 100644 packages/raystack/components/accordion/accordion-trigger.tsx diff --git a/packages/raystack/components/accordion/accordion-content.tsx b/packages/raystack/components/accordion/accordion-content.tsx new file mode 100644 index 00000000..794697ff --- /dev/null +++ b/packages/raystack/components/accordion/accordion-content.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import { Accordion as AccordionPrimitive } from 'radix-ui'; +import { ElementRef, ReactNode, forwardRef } from 'react'; +import styles from './accordion.module.css'; + +export interface AccordionContentProps + extends AccordionPrimitive.AccordionContentProps { + children: ReactNode; + className?: string; +} + +export const AccordionContent = forwardRef< + ElementRef, + AccordionContentProps +>(({ className, children, ...props }, ref) => ( + +
+ {children} +
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; diff --git a/packages/raystack/components/accordion/accordion-item.tsx b/packages/raystack/components/accordion/accordion-item.tsx new file mode 100644 index 00000000..58af12e6 --- /dev/null +++ b/packages/raystack/components/accordion/accordion-item.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import { Accordion as AccordionPrimitive } from 'radix-ui'; +import { ElementRef, ReactNode, forwardRef } from 'react'; +import styles from './accordion.module.css'; + +export interface AccordionItemProps + extends AccordionPrimitive.AccordionItemProps { + children: ReactNode; + className?: string; +} + +export const AccordionItem = forwardRef< + ElementRef, + AccordionItemProps +>(({ className, children, ...props }, ref) => ( + + {children} + +)); + +AccordionItem.displayName = AccordionPrimitive.Item.displayName; diff --git a/packages/raystack/components/accordion/accordion-root.tsx b/packages/raystack/components/accordion/accordion-root.tsx new file mode 100644 index 00000000..75d5bfbc --- /dev/null +++ b/packages/raystack/components/accordion/accordion-root.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import { Accordion as AccordionPrimitive } from 'radix-ui'; +import { ElementRef, forwardRef } from 'react'; +import styles from './accordion.module.css'; + +type AccordionSingleProps = Omit< + AccordionPrimitive.AccordionSingleProps, + 'type' +> & { + type?: 'single'; +}; +type AccordionMultipleProps = Omit< + AccordionPrimitive.AccordionMultipleProps, + 'type' +> & { + type: 'multiple'; +}; +export type AccordionRootProps = AccordionSingleProps | AccordionMultipleProps; + +export const AccordionRoot = forwardRef< + ElementRef, + AccordionRootProps +>(({ className, type = 'single', ...rest }, ref) => { + // this is a workaround to properly typecast the union type + const singleProps = { + type: 'single', + collapsible: true, + ...rest + } as AccordionPrimitive.AccordionSingleProps; + const multipleProps = { + type: 'multiple', + ...rest + } as AccordionPrimitive.AccordionMultipleProps; + + return ( + + ); +}); + +AccordionRoot.displayName = AccordionPrimitive.Root.displayName; diff --git a/packages/raystack/components/accordion/accordion-trigger.tsx b/packages/raystack/components/accordion/accordion-trigger.tsx new file mode 100644 index 00000000..997fff59 --- /dev/null +++ b/packages/raystack/components/accordion/accordion-trigger.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { ChevronDownIcon } from '@radix-ui/react-icons'; +import { cx } from 'class-variance-authority'; +import { Accordion as AccordionPrimitive } from 'radix-ui'; +import { ElementRef, ReactNode, forwardRef } from 'react'; +import styles from './accordion.module.css'; + +export interface AccordionTriggerProps + extends AccordionPrimitive.AccordionTriggerProps { + children: ReactNode; + className?: string; +} + +export const AccordionTrigger = forwardRef< + ElementRef, + AccordionTriggerProps +>(({ className, children, ...props }, ref) => ( + + + {children} + + + +)); + +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; diff --git a/packages/raystack/components/accordion/index.tsx b/packages/raystack/components/accordion/index.tsx index 310dc1af..f7ea0e91 100644 --- a/packages/raystack/components/accordion/index.tsx +++ b/packages/raystack/components/accordion/index.tsx @@ -1,7 +1 @@ -export { - Accordion, - type AccordionRootProps, - type AccordionItemProps, - type AccordionTriggerProps, - type AccordionContentProps -} from './accordion'; +export { Accordion } from './accordion'; From 980e2fbb6b355518ac8f8cf756ea9c5d3ae18423 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 19 Sep 2025 01:00:21 +0530 Subject: [PATCH 5/6] feat: add accordion docs --- .../content/docs/components/accordion/demo.ts | 161 ++++++++++++++++++ .../docs/components/accordion/index.mdx | 83 +++++++++ .../docs/components/accordion/props.ts | 81 +++++++++ .../components/accordion/accordion.tsx | 116 +------------ 4 files changed, 329 insertions(+), 112 deletions(-) create mode 100644 apps/www/src/content/docs/components/accordion/demo.ts create mode 100644 apps/www/src/content/docs/components/accordion/index.mdx create mode 100644 apps/www/src/content/docs/components/accordion/props.ts diff --git a/apps/www/src/content/docs/components/accordion/demo.ts b/apps/www/src/content/docs/components/accordion/demo.ts new file mode 100644 index 00000000..f4725dec --- /dev/null +++ b/apps/www/src/content/docs/components/accordion/demo.ts @@ -0,0 +1,161 @@ +'use client'; + +import { getPropsString } from '@/lib/utils'; + +export const getCode = (props: Record) => { + return ` + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that matches the other components. + + + + Is it animated? + + Yes. It's animated by default, but you can disable it if you prefer. + + +
`; +}; + +export const playground = { + type: 'playground', + controls: { + type: { + type: 'select', + options: ['single', 'multiple'], + defaultValue: 'single' + } + }, + getCode +}; + +export const typeDemo = { + type: 'code', + tabs: [ + { + name: 'Single', + code: ` + + + What is Apsara? + + Apsara is a modern design system and component library built with React and TypeScript. + + + + How do I get started? + + You can install Apsara using your preferred package manager and start building your application. + + + + Is it customizable? + + Yes, Apsara provides extensive customization options through CSS variables and component props. + + +` + }, + { + name: 'Multiple', + code: ` + + + What is Apsara? + + Apsara is a modern design system and component library built with React and TypeScript. + + + + How do I get started? + + You can install Apsara using your preferred package manager and start building your application. + + + + Is it customizable? + + Yes, Apsara provides extensive customization options through CSS variables and component props. + + +` + } + ] +}; +export const controlledDemo = { + type: 'code', + code: ` + function ControlledAccordion() { + const [value, setValue] = React.useState('item-1'); + + return ( + + + Item 1 + Content for item 1 + + + Item 2 + Content for item 2 + + + ); +}` +}; + +export const disabledDemo = { + type: 'code', + code: ` + + + Enabled Item + This item is enabled and can be toggled. + + + Disabled Item + This item is disabled and cannot be toggled. + + + Another Enabled Item + This item is also enabled. + +` +}; + +export const customContentDemo = { + type: 'code', + code: ` + + + Product Features + +
+

Key Features

+
    +
  • Responsive design
  • +
  • Accessible components
  • +
  • TypeScript support
  • +
  • Customizable themes
  • +
+
+
+
+ + Documentation + +
+

Comprehensive documentation with examples and API references.

+ View Documentation +
+
+
+
` +}; diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx new file mode 100644 index 00000000..6faca9b2 --- /dev/null +++ b/apps/www/src/content/docs/components/accordion/index.mdx @@ -0,0 +1,83 @@ +--- +title: Accordion +description: A vertically stacked set of interactive headings that each reveal a section of content. +tag: new +--- + +import { + playground, + typeDemo, + controlledDemo, + disabledDemo, + customContentDemo, +} from "./demo.ts"; + + + +## Usage + +```tsx +import { Accordion } from '@raystack/apsara' +``` + +## Accordion Props + + + +### Accordion.Item Props + + + +### Accordion.Trigger Props + + + +### Accordion.Content Props + + + +## Examples + +### Single vs Multiple + +The Accordion component supports two types of behavior: + +- **Single**: Only one item can be open at a time +- **Multiple**: Multiple items can be open simultaneously + + + +### Controlled + +You can control the accordion's state by providing `value` and `onValueChange` props. + + + +### Disabled Items + +Individual accordion items can be disabled using the `disabled` prop. + + + +### Custom Content + +The accordion content can contain any React elements, allowing for rich layouts and complex content. + + + +## Accessibility + +The Accordion component is built on top of [Radix UI's Accordion primitive](https://www.radix-ui.com/primitives/docs/components/accordion) and follows the WAI-ARIA design pattern for accordions. It includes: + +- Proper ARIA attributes for screen readers +- Keyboard navigation support +- Focus management +- Semantic HTML structure + +## Keyboard Navigation + +- **Space** or **Enter**: Toggle the focused accordion item +- **Arrow Down**: Move focus to the next accordion item +- **Arrow Up**: Move focus to the previous accordion item +- **Home**: Move focus to the first accordion item +- **End**: Move focus to the last accordion item diff --git a/apps/www/src/content/docs/components/accordion/props.ts b/apps/www/src/content/docs/components/accordion/props.ts new file mode 100644 index 00000000..6f52bdce --- /dev/null +++ b/apps/www/src/content/docs/components/accordion/props.ts @@ -0,0 +1,81 @@ +export interface AccordionRootProps { + /** + * The type of accordion behavior + * @defaultValue "single" + */ + type?: 'single' | 'multiple'; + + /** + * The controlled value of the accordion + */ + value?: string | string[]; + + /** + * The default value of the accordion + */ + defaultValue?: string | string[]; + + /** + * Event handler called when the value changes + */ + onValueChange?: (value: string | string[]) => void; + + /** + * Whether the accordion is collapsible when type is single + * @defaultValue true + */ + collapsible?: boolean; + + /** + * Whether the accordion is disabled + * @defaultValue false + */ + disabled?: boolean; + + /** + * The orientation of the accordion + * @defaultValue "vertical" + */ + orientation?: 'horizontal' | 'vertical'; + + /** + * The direction of the accordion + * @defaultValue "ltr" + */ + dir?: 'ltr' | 'rtl'; + + /** Custom CSS class names */ + className?: string; +} + +export interface AccordionItemProps { + /** + * A unique value for the item + */ + value: string; + + /** + * Whether the item is disabled + * @defaultValue false + */ + disabled?: boolean; + + /** Custom CSS class names */ + className?: string; +} + +export interface AccordionTriggerProps { + /** Custom CSS class names */ + className?: string; +} + +export interface AccordionContentProps { + /** + * Whether the content is force mounted + * @defaultValue false + */ + forceMount?: boolean; + + /** Custom CSS class names */ + className?: string; +} diff --git a/packages/raystack/components/accordion/accordion.tsx b/packages/raystack/components/accordion/accordion.tsx index 7f2976d4..e4c0dea4 100644 --- a/packages/raystack/components/accordion/accordion.tsx +++ b/packages/raystack/components/accordion/accordion.tsx @@ -1,115 +1,7 @@ -'use client'; - -import { ChevronDownIcon } from '@radix-ui/react-icons'; -import { cx } from 'class-variance-authority'; -import { Accordion as AccordionPrimitive } from 'radix-ui'; -import { ElementRef, ReactNode, forwardRef } from 'react'; -import styles from './accordion.module.css'; - -type AccordionSingleProps = Omit< - AccordionPrimitive.AccordionSingleProps, - 'type' -> & { - type?: 'single'; -}; -type AccordionMultipleProps = Omit< - AccordionPrimitive.AccordionMultipleProps, - 'type' -> & { - type: 'multiple'; -}; -export type AccordionRootProps = AccordionSingleProps | AccordionMultipleProps; - -export interface AccordionItemProps - extends AccordionPrimitive.AccordionItemProps { - children: ReactNode; - className?: string; -} - -export interface AccordionTriggerProps - extends AccordionPrimitive.AccordionTriggerProps { - children: ReactNode; - className?: string; -} - -export interface AccordionContentProps - extends AccordionPrimitive.AccordionContentProps { - children: ReactNode; - className?: string; -} - -const AccordionRoot = forwardRef< - ElementRef, - AccordionRootProps ->(({ className, type = 'single', ...rest }, ref) => { - // this is a workaround to properly typecast the union type - const singleProps = { - type: 'single', - collapsible: true, - ...rest - } as AccordionPrimitive.AccordionSingleProps; - const multipleProps = { - type: 'multiple', - ...rest - } as AccordionPrimitive.AccordionMultipleProps; - - return ( - - ); -}); - -const AccordionItem = forwardRef< - ElementRef, - AccordionItemProps ->(({ className, children, ...props }, ref) => ( - - {children} - -)); - -const AccordionTrigger = forwardRef< - ElementRef, - AccordionTriggerProps ->(({ className, children, ...props }, ref) => ( - - - {children} - - - -)); - -const AccordionContent = forwardRef< - ElementRef, - AccordionContentProps ->(({ className, children, ...props }, ref) => ( - -
- {children} -
-
-)); - -AccordionRoot.displayName = AccordionPrimitive.Root.displayName; -AccordionItem.displayName = AccordionPrimitive.Item.displayName; -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; -AccordionContent.displayName = AccordionPrimitive.Content.displayName; +import { AccordionContent } from './accordion-content'; +import { AccordionItem } from './accordion-item'; +import { AccordionRoot } from './accordion-root'; +import { AccordionTrigger } from './accordion-trigger'; export const Accordion = Object.assign(AccordionRoot, { Item: AccordionItem, From 581995420299ac4e8178bd9f3e2bf1f5a9996663 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Mon, 22 Sep 2025 11:58:35 +0530 Subject: [PATCH 6/6] fix: accordion style resolve --- apps/www/src/content/docs/components/accordion/props.ts | 4 +++- packages/raystack/components/accordion/accordion.module.css | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/www/src/content/docs/components/accordion/props.ts b/apps/www/src/content/docs/components/accordion/props.ts index 6f52bdce..a520ea8d 100644 --- a/apps/www/src/content/docs/components/accordion/props.ts +++ b/apps/www/src/content/docs/components/accordion/props.ts @@ -1,6 +1,8 @@ export interface AccordionRootProps { /** - * The type of accordion behavior + * Controls how many accordion items can be open at once. + * - "single": Only one item can be open at a time + * - "multiple": Multiple items can be open simultaneously * @defaultValue "single" */ type?: 'single' | 'multiple'; diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css index 0b652b8b..28eb4b7c 100644 --- a/packages/raystack/components/accordion/accordion.module.css +++ b/packages/raystack/components/accordion/accordion.module.css @@ -68,6 +68,7 @@ gap: var(--rs-space-5); align-self: stretch; padding: var(--rs-space-5) var(--rs-space-4); + border-top: 0px; } .accordion-content[data-state="closed"] {