Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/www/src/components/typetable/typetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function TypeTable({
<p className={styles.propLabel}>Prop</p>
<p className={styles.typeLabel}>Type</p>
</div>
<Accordion type='multiple'>
<Accordion multiple>
{entries.map(([key, value]) => (
<Item key={key} name={key} item={value} />
))}
Expand Down
11 changes: 5 additions & 6 deletions apps/www/src/content/docs/components/accordion/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ export const getCode = (props: Record<string, unknown>) => {
export const playground = {
type: 'playground',
controls: {
type: {
type: 'select',
options: ['single', 'multiple'],
defaultValue: 'single'
multiple: {
type: 'checkbox',
defaultValue: false
}
},
getCode
Expand All @@ -43,7 +42,7 @@ export const typeDemo = {
{
name: 'Single',
code: `
<Accordion type="single" collapsible>
<Accordion multiple={false}>
<Accordion.Item value="item-1">
<Accordion.Trigger>What is Apsara?</Accordion.Trigger>
<Accordion.Content>
Expand All @@ -67,7 +66,7 @@ export const typeDemo = {
{
name: 'Multiple',
code: `
<Accordion type="multiple">
<Accordion multiple>
<Accordion.Item value="item-1">
<Accordion.Trigger>What is Apsara?</Accordion.Trigger>
<Accordion.Content>
Expand Down
23 changes: 3 additions & 20 deletions apps/www/src/content/docs/components/accordion/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`.

<Demo data={typeDemo} />

Expand All @@ -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.

<Demo data={customContentDemo} />

## 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
56 changes: 36 additions & 20 deletions apps/www/src/content/docs/components/accordion/props.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/content/docs/components/slider/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -183,7 +183,8 @@ describe('Accordion', () => {
render(<BasicAccordion hasDisabledItem />);

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');
});
});

Expand All @@ -207,20 +208,21 @@ describe('Accordion', () => {
it('forwards ref correctly', () => {
const ref = vi.fn();
render(
<Accordion>
<Accordion defaultValue='item-1'>
<Accordion.Item value='item-1'>
<Accordion.Trigger>{ITEM_1_TEXT}</Accordion.Trigger>
<Accordion.Content ref={ref}>{CONTENT_1_TEXT}</Accordion.Content>
</Accordion.Item>
</Accordion>
);
// Base UI Panel ref should be called when panel is open
expect(ref).toHaveBeenCalled();
});
});

describe('Multiple Items', () => {
it('handles multiple accordion items independently', () => {
render(<BasicAccordion type='multiple' />);
render(<BasicAccordion multiple={true} />);

const trigger1 = screen.getByRole('button', { name: ITEM_1_TEXT });
const trigger2 = screen.getByRole('button', { name: ITEM_2_TEXT });
Expand Down
20 changes: 7 additions & 13 deletions packages/raystack/components/accordion/accordion-content.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
'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<typeof AccordionPrimitive.Content>,
AccordionContentProps
ElementRef<typeof AccordionPrimitive.Panel>,
AccordionPrimitive.Panel.Props
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
<AccordionPrimitive.Panel
ref={ref}
className={styles['accordion-content']}
{...props}
>
<div className={cx(styles['accordion-content-inner'], className)}>
{children}
</div>
</AccordionPrimitive.Content>
</AccordionPrimitive.Panel>
));

AccordionContent.displayName = AccordionPrimitive.Content.displayName;
AccordionContent.displayName = 'Accordion.Content';
14 changes: 4 additions & 10 deletions packages/raystack/components/accordion/accordion-item.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof AccordionPrimitive.Item>,
AccordionItemProps
AccordionPrimitive.Item.Props
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
Expand All @@ -24,4 +18,4 @@ export const AccordionItem = forwardRef<
</AccordionPrimitive.Item>
));

AccordionItem.displayName = AccordionPrimitive.Item.displayName;
AccordionItem.displayName = 'Accordion.Item';
Loading