diff --git a/apps/www/src/components/typetable/typetable.tsx b/apps/www/src/components/typetable/typetable.tsx index 2f15a29ff..e4201045d 100644 --- a/apps/www/src/components/typetable/typetable.tsx +++ b/apps/www/src/components/typetable/typetable.tsx @@ -57,7 +57,7 @@ export function TypeTable({

Prop

Type

- + {entries.map(([key, value]) => ( ))} diff --git a/apps/www/src/content/docs/components/accordion/demo.ts b/apps/www/src/content/docs/components/accordion/demo.ts index f4725decc..6bfb7104e 100644 --- a/apps/www/src/content/docs/components/accordion/demo.ts +++ b/apps/www/src/content/docs/components/accordion/demo.ts @@ -28,10 +28,9 @@ export const getCode = (props: Record) => { export const playground = { type: 'playground', controls: { - type: { - type: 'select', - options: ['single', 'multiple'], - defaultValue: 'single' + multiple: { + type: 'checkbox', + defaultValue: false } }, getCode @@ -43,7 +42,7 @@ export const typeDemo = { { name: 'Single', code: ` - + What is Apsara? @@ -67,7 +66,7 @@ export const typeDemo = { { name: 'Multiple', code: ` - + What is Apsara? diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx index 4945a75b7..5f90d1302 100644 --- a/apps/www/src/content/docs/components/accordion/index.mdx +++ b/apps/www/src/content/docs/components/accordion/index.mdx @@ -40,10 +40,10 @@ import { Accordion } from '@raystack/apsara' ### Single vs Multiple -The Accordion component supports two types of behavior: +The Accordion component supports two modes: -- **Single**: Only one item can be open at a time -- **Multiple**: Multiple items can be open simultaneously +- **Single** (default): Only one item can be open at a time. Set `multiple={false}` or omit the prop. +- **Multiple**: Multiple items can be open simultaneously. Set `multiple={true}`. @@ -64,20 +64,3 @@ Individual accordion items can be disabled using the `disabled` prop. 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 index a520ea8d6..cd71eeedb 100644 --- a/apps/www/src/content/docs/components/accordion/props.ts +++ b/apps/www/src/content/docs/components/accordion/props.ts @@ -1,32 +1,30 @@ export interface AccordionRootProps { /** - * 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" + * Whether multiple accordion items can be open at the same time. + * @defaultValue false */ - type?: 'single' | 'multiple'; + multiple?: boolean; /** - * The controlled value of the accordion + * The controlled value of the accordion. + * For single mode: string | undefined + * For multiple mode: string[] */ value?: string | string[]; /** - * The default value of the accordion + * The default value of the accordion. + * For single mode: string | undefined + * For multiple mode: string[] */ 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 + * Event handler called when the value changes. + * For single mode: (value?: string) => void + * For multiple mode: (value?: string[]) => void */ - collapsible?: boolean; + onValueChange?: (value?: string | string[]) => void; /** * Whether the accordion is disabled @@ -41,10 +39,22 @@ export interface AccordionRootProps { orientation?: 'horizontal' | 'vertical'; /** - * The direction of the accordion - * @defaultValue "ltr" + * Whether to loop keyboard focus back to the first item when the end is reached + * @defaultValue true + */ + loopFocus?: boolean; + + /** + * Whether to keep the element in the DOM while the panel is closed + * @defaultValue false + */ + keepMounted?: boolean; + + /** + * Allows the browser's built-in page search to find and expand the panel contents + * @defaultValue false */ - dir?: 'ltr' | 'rtl'; + hiddenUntilFound?: boolean; /** Custom CSS class names */ className?: string; @@ -73,10 +83,16 @@ export interface AccordionTriggerProps { export interface AccordionContentProps { /** - * Whether the content is force mounted + * Whether to keep the element in the DOM while the panel is closed + * @defaultValue false + */ + keepMounted?: boolean; + + /** + * Allows the browser's built-in page search to find and expand the panel contents * @defaultValue false */ - forceMount?: boolean; + hiddenUntilFound?: boolean; /** Custom CSS class names */ className?: string; diff --git a/apps/www/src/content/docs/components/slider/index.mdx b/apps/www/src/content/docs/components/slider/index.mdx index 938bbe460..7da30ed42 100644 --- a/apps/www/src/content/docs/components/slider/index.mdx +++ b/apps/www/src/content/docs/components/slider/index.mdx @@ -11,7 +11,7 @@ import { playground, variantDemo, controlDemo, thumbSizeDemo } from "./demo.ts"; ## Usage ```tsx -import { Slider } from '@raystack/apsara' +import { Slider, SliderValue } from '@raystack/apsara' ``` ## Slider Props diff --git a/packages/raystack/components/accordion/__tests__/accordion.test.tsx b/packages/raystack/components/accordion/__tests__/accordion.test.tsx index d1bbaa8ac..061927350 100644 --- a/packages/raystack/components/accordion/__tests__/accordion.test.tsx +++ b/packages/raystack/components/accordion/__tests__/accordion.test.tsx @@ -2,8 +2,8 @@ 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'; +import { AccordionRootProps } from '../accordion-root'; const ITEM_1_TEXT = 'Item 1'; const ITEM_2_TEXT = 'Item 2'; @@ -183,7 +183,8 @@ describe('Accordion', () => { render(); const trigger = screen.getByRole('button', { name: ITEM_2_TEXT }); - expect(trigger).toBeDisabled(); + // Base UI uses aria-disabled instead of disabled attribute + expect(trigger).toHaveAttribute('aria-disabled', 'true'); }); }); @@ -207,20 +208,21 @@ describe('Accordion', () => { it('forwards ref correctly', () => { const ref = vi.fn(); render( - + {ITEM_1_TEXT} {CONTENT_1_TEXT} ); + // Base UI Panel ref should be called when panel is open expect(ref).toHaveBeenCalled(); }); }); describe('Multiple Items', () => { it('handles multiple accordion items independently', () => { - render(); + render(); const trigger1 = screen.getByRole('button', { name: ITEM_1_TEXT }); const trigger2 = screen.getByRole('button', { name: ITEM_2_TEXT }); diff --git a/packages/raystack/components/accordion/accordion-content.tsx b/packages/raystack/components/accordion/accordion-content.tsx index 794697ffa..5dc7183bc 100644 --- a/packages/raystack/components/accordion/accordion-content.tsx +++ b/packages/raystack/components/accordion/accordion-content.tsx @@ -1,21 +1,15 @@ 'use client'; +import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { Accordion as AccordionPrimitive } from 'radix-ui'; -import { ElementRef, ReactNode, forwardRef } from 'react'; +import { ElementRef, 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 + ElementRef, + AccordionPrimitive.Panel.Props >(({ className, children, ...props }, ref) => ( - {children} - + )); -AccordionContent.displayName = AccordionPrimitive.Content.displayName; +AccordionContent.displayName = 'Accordion.Content'; diff --git a/packages/raystack/components/accordion/accordion-item.tsx b/packages/raystack/components/accordion/accordion-item.tsx index 58af12e68..3b487fa5c 100644 --- a/packages/raystack/components/accordion/accordion-item.tsx +++ b/packages/raystack/components/accordion/accordion-item.tsx @@ -1,19 +1,13 @@ 'use client'; +import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { Accordion as AccordionPrimitive } from 'radix-ui'; -import { ElementRef, ReactNode, forwardRef } from 'react'; +import { ElementRef, 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 + AccordionPrimitive.Item.Props >(({ className, children, ...props }, ref) => ( )); -AccordionItem.displayName = AccordionPrimitive.Item.displayName; +AccordionItem.displayName = 'Accordion.Item'; diff --git a/packages/raystack/components/accordion/accordion-root.tsx b/packages/raystack/components/accordion/accordion-root.tsx index 75d5bfbcc..be7030010 100644 --- a/packages/raystack/components/accordion/accordion-root.tsx +++ b/packages/raystack/components/accordion/accordion-root.tsx @@ -1,46 +1,87 @@ 'use client'; +import { Accordion as AccordionPrimitive } from '@base-ui/react'; 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' + AccordionPrimitive.Root.Props, + 'multiple' | 'value' | 'defaultValue' | 'onValueChange' > & { - type?: 'single'; + multiple?: false; + value?: string; + defaultValue?: string; + onValueChange?: (value?: string) => void; }; + type AccordionMultipleProps = Omit< - AccordionPrimitive.AccordionMultipleProps, - 'type' + AccordionPrimitive.Root.Props, + 'multiple' | 'value' | 'defaultValue' | 'onValueChange' > & { - type: 'multiple'; + multiple: true; + value?: string[]; + defaultValue?: string[]; + onValueChange?: (value?: string[]) => void; }; + 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; +>( + ( + { + className, + multiple = false, + value, + defaultValue, + onValueChange, + ...rest + }, + ref + ) => { + // Convert value to array format for Base UI + const baseValue = value + ? Array.isArray(value) + ? value + : [value] + : undefined; + + const baseDefaultValue = defaultValue + ? Array.isArray(defaultValue) + ? defaultValue + : [defaultValue] + : undefined; + + const handleValueChange = ( + newValue: string[], + eventDetails: AccordionPrimitive.Root.ChangeEventDetails + ) => { + if (onValueChange) { + if (multiple) { + (onValueChange as (value: string[]) => void)(newValue); + } else { + (onValueChange as (value: string | undefined) => void)( + newValue[0] || undefined + ); + } + } + }; + + return ( + + ); + } +); + +AccordionRoot.displayName = 'Accordion.Root'; diff --git a/packages/raystack/components/accordion/accordion-trigger.tsx b/packages/raystack/components/accordion/accordion-trigger.tsx index 997fff59c..ef50a0ad3 100644 --- a/packages/raystack/components/accordion/accordion-trigger.tsx +++ b/packages/raystack/components/accordion/accordion-trigger.tsx @@ -1,20 +1,14 @@ 'use client'; +import { Accordion as AccordionPrimitive } from '@base-ui/react'; 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 { ElementRef, 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 + AccordionPrimitive.Trigger.Props >(({ className, children, ...props }, ref) => ( )); -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; +AccordionTrigger.displayName = 'Accordion.Trigger'; diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css index 28eb4b7c5..a8db803e7 100644 --- a/packages/raystack/components/accordion/accordion.module.css +++ b/packages/raystack/components/accordion/accordion.module.css @@ -39,7 +39,7 @@ opacity: 0.5; } -.accordion-trigger[data-state="open"] .accordion-icon { +.accordion-trigger[data-panel-open] .accordion-icon { transform: rotate(180deg); } @@ -53,7 +53,14 @@ } .accordion-content { + height: var(--accordion-panel-height); overflow: hidden; + transition: height 150ms ease-out; +} + +.accordion-content[data-starting-style], +.accordion-content[data-ending-style] { + height: 0; } .accordion-content-inner { @@ -70,30 +77,3 @@ padding: var(--rs-space-5) var(--rs-space-4); border-top: 0px; } - -.accordion-content[data-state="closed"] { - animation: accordion-up 200ms ease-out; -} - -.accordion-content[data-state="open"] { - animation: accordion-down 200ms ease-out; -} - -/* 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; - } -}