refactor(ui): replace Collapsible with Accordion component

Replaces the Base UI Collapsible component with a new Accordion component built on React Aria's Disclosure primitives.

Key changes:
- Removed Collapsible component and all related files
- Added new Accordion component with AccordionTrigger, AccordionPanel, and AccordionGroup
- Introduced opinionated styling with built-in trigger component featuring animated chevron icon
- Added support for title/subtitle props and custom trigger content via children
- Implemented AccordionGroup with single/multiple expansion modes
- Updated all documentation from collapsible.mdx to accordion.mdx
- Added comprehensive migration guide in changeset
- Updated API reports and exports

This is a breaking change. Users must migrate from Collapsible to either:
1. Accordion (opinionated styled component) - recommended for most use cases
2. React Aria Disclosure directly - for full customization

The changeset provides detailed migration examples for both paths.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2025-10-22 11:24:18 +02:00
parent e748a1e87f
commit 83c100e6ac
19 changed files with 869 additions and 400 deletions
+128
View File
@@ -0,0 +1,128 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { AccordionSnippet } from '@/snippets/stories-snippets';
import {
accordionPropDefs,
accordionTriggerPropDefs,
accordionPanelPropDefs,
accordionGroupPropDefs,
accordionUsageSnippet,
accordionWithSubtitleSnippet,
accordionCustomTriggerSnippet,
accordionDefaultExpandedSnippet,
accordionGroupSingleOpenSnippet,
accordionGroupMultipleOpenSnippet,
} from './accordion.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Accordion"
description="A component for showing and hiding content with animation."
/>
<Snippet
align="center"
py={4}
height={240}
preview={<AccordionSnippet story="Default" />}
code={accordionUsageSnippet}
/>
## Usage
<CodeBlock code={accordionUsageSnippet} />
## API reference
### Accordion
Root container for the accordion. Renders a `<div>` element.
<PropsTable data={accordionPropDefs} />
### AccordionTrigger
Trigger component with built-in animated chevron icon. Renders a heading element (defaults to `<h3>`, configurable via `level` prop) wrapping a `<button>`.
<PropsTable data={accordionTriggerPropDefs} />
### AccordionPanel
Panel with the accordion content. Renders a `<div>` element.
<PropsTable data={accordionPanelPropDefs} />
### AccordionGroup
Container for managing multiple accordions. Renders a `<div>` element.
<PropsTable data={accordionGroupPropDefs} />
## Examples
### With Subtitle
Here's a view when using both title and subtitle props.
<Snippet
align="center"
py={4}
height={240}
preview={<AccordionSnippet story="WithSubtitle" />}
code={accordionWithSubtitleSnippet}
/>
### Custom Trigger
Here's a view when providing custom multi-line content as children.
<Snippet
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="CustomTrigger" />}
code={accordionCustomTriggerSnippet}
/>
### Default Expanded
Here's a view when the panel is expanded by default.
<Snippet
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="DefaultExpanded" />}
code={accordionDefaultExpandedSnippet}
/>
### Group Single Open
Here's a view when only one accordion can be open at a time.
<Snippet
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="GroupSingleOpen" />}
code={accordionGroupSingleOpenSnippet}
/>
### Group Multiple Open
Here's a view when multiple accordions can be open simultaneously.
<Snippet
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="GroupMultipleOpen" />}
code={accordionGroupMultipleOpenSnippet}
/>
<Theming component="Accordion" />
<ChangelogComponent component="accordion" />
+119
View File
@@ -0,0 +1,119 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const accordionPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
},
defaultExpanded: {
type: 'boolean',
default: 'false',
},
isExpanded: {
type: 'boolean',
},
onExpandedChange: {
type: 'enum',
values: ['(isExpanded: boolean) => void'],
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionTriggerPropDefs: Record<string, PropDef> = {
level: {
type: 'enum',
values: ['1', '2', '3', '4', '5', '6'],
default: '3',
},
title: {
type: 'string',
},
subtitle: {
type: 'string',
},
children: {
type: 'enum',
values: ['ReactNode'],
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionPanelPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const accordionGroupPropDefs: Record<string, PropDef> = {
allowsMultiple: {
type: 'boolean',
default: 'false',
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>Your content</AccordionPanel>
</Accordion>`;
export const accordionWithSubtitleSnippet = `<Accordion>
<AccordionTrigger
title="Advanced Settings"
subtitle="Configure additional options"
/>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionCustomTriggerSnippet = `<Accordion>
<AccordionTrigger>
<Box>
<Text as="div" weight="bold">Custom Multi-line Trigger</Text>
<Text as="div" size="small" color="secondary">
Click to expand additional details
</Text>
</Box>
</AccordionTrigger>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionDefaultExpandedSnippet = `<Accordion defaultExpanded>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionGroupSingleOpenSnippet = `<AccordionGroup>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>Content 1</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>Content 2</AccordionPanel>
</Accordion>
</AccordionGroup>`;
export const accordionGroupMultipleOpenSnippet = `<AccordionGroup allowsMultiple>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>Content 1</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>Content 2</AccordionPanel>
</Accordion>
</AccordionGroup>`;
-72
View File
@@ -1,72 +0,0 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { CollapsibleSnippet } from '@/snippets/stories-snippets';
import {
collapsibleRootPropDefs,
collapsibleTriggerPropDefs,
collapsiblePanelPropDefs,
collapsibleUsageSnippet,
collapsibleDefaultSnippet,
collapsibleTriggerSnippet,
collapsibleOpenSnippet,
} from './collapsible.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Collapsible"
description="A collapsible component that can be used to display content in a box."
/>
<Snippet
align="center"
py={4}
height={240}
preview={<CollapsibleSnippet story="Default" />}
code={collapsibleDefaultSnippet}
/>
## Usage
<CodeBlock code={collapsibleUsageSnippet} />
## API reference
### Collapsible.Root
Groups all parts of the collapsible. Renders a `<div>` element.
<PropsTable data={collapsibleRootPropDefs} />
### Collapsible.Trigger
The trigger by default render a simple unstyled button. Because menus can be rendered in different ways, we recommend
using the `render` prop to render a custom trigger.
<CodeBlock code={collapsibleTriggerSnippet} />
<PropsTable data={collapsibleTriggerPropDefs} />
### Collapsible.Panel
A panel with the collapsible contents. Renders a `<div>` element.
<PropsTable data={collapsiblePanelPropDefs} />
## Examples
Open the panel by default by setting the `defaultOpen` prop to `true`.
<Snippet
align="center"
py={4}
open
preview={<CollapsibleSnippet story="Open" />}
code={collapsibleOpenSnippet}
/>
<Theming component="Collapsible" />
<ChangelogComponent component="collapsible" />
-86
View File
@@ -1,86 +0,0 @@
import {
classNamePropDefs,
stylePropDefs,
renderPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const collapsibleRootPropDefs: Record<string, PropDef> = {
defaultOpen: {
type: 'boolean',
default: 'false',
},
open: {
type: 'boolean',
},
onOpenChange: {
type: 'enum',
values: ['(open) => void'],
},
...renderPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const collapsibleTriggerPropDefs: Record<string, PropDef> = {
...renderPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const collapsiblePanelPropDefs: Record<string, PropDef> = {
hiddenUntilFound: {
type: 'boolean',
default: 'false',
},
keepMounted: {
type: 'boolean',
default: 'false',
},
...renderPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const collapsibleUsageSnippet = `import { Collapsible } from '@backstage/ui';
<Collapsible.Root>
<Collapsible.Trigger render={(props, state) => (
<Button {...props}>
{state.open ? 'Close Panel' : 'Open Panel'}
</Button>
)} />
<Collapsible.Panel>Your content</Collapsible.Panel>
</Collapsible.Root>`;
export const collapsibleDefaultSnippet = `<Collapsible.Root>
<Collapsible.Trigger render={(props, state) => (
<Button {...props}>
{state.open ? 'Close Panel' : 'Open Panel'}
</Button>
)} />
<Collapsible.Panel>
<Box>
<Text>It's the edge of the world and all of Western civilization</Text>
<Text>The sun may rise in the East, at least it settled in a final location</Text>
<Text>It's understood that Hollywood sells Californication</Text>
</Box>
</Collapsible.Panel>
</Collapsible.Root>`;
export const collapsibleTriggerSnippet = `<Collapsible.Trigger render={props => <Button {...props} />} />`;
export const collapsibleOpenSnippet = `<Collapsible.Root defaultOpen>
<Collapsible.Trigger render={(props, state) => (
<Button {...props}>
{state.open ? 'Close Panel' : 'Open Panel'}
</Button>
)} />
<Collapsible.Panel>
<Box>
<Text>It's the edge of the world and all of Western civilization</Text>
<Text>The sun may rise in the East, at least it settled in a final location</Text>
<Text>It's understood that Hollywood sells Californication</Text>
</Box>
</Collapsible.Panel>
</Collapsible.Root>`;
+2 -2
View File
@@ -15,7 +15,7 @@ import * as SelectStories from '../../../packages/ui/src/components/Select/Selec
import * as MenuStories from '../../../packages/ui/src/components/Menu/Menu.stories';
import * as LinkStories from '../../../packages/ui/src/components/Link/Link.stories';
import * as AvatarStories from '../../../packages/ui/src/components/Avatar/Avatar.stories';
import * as CollapsibleStories from '../../../packages/ui/src/components/Collapsible/Collapsible.stories';
import * as AccordionStories from '../../../packages/ui/src/components/Accordion/Accordion.stories';
import * as DialogStories from '../../../packages/ui/src/components/Dialog/Dialog.stories';
import * as RadioGroupStories from '../../../packages/ui/src/components/RadioGroup/RadioGroup.stories';
import * as TabsStories from '../../../packages/ui/src/components/Tabs/Tabs.stories';
@@ -62,7 +62,7 @@ export const SelectSnippet = createSnippetComponent(SelectStories);
export const MenuSnippet = createSnippetComponent(MenuStories);
export const LinkSnippet = createSnippetComponent(LinkStories);
export const AvatarSnippet = createSnippetComponent(AvatarStories);
export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
export const AccordionSnippet = createSnippetComponent(AccordionStories);
export const DialogSnippet = createSnippetComponent(DialogStories);
export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
export const TabsSnippet = createSnippetComponent(TabsStories);
+1
View File
@@ -13,6 +13,7 @@ export type Component =
| 'datatable'
| 'select'
| 'collapsible'
| 'accordion'
| 'checkbox'
| 'container'
| 'link'
+4 -4
View File
@@ -24,6 +24,10 @@ export const layoutComponents: Page[] = [
];
export const components: Page[] = [
{
title: 'Accordion',
slug: 'accordion',
},
{
title: 'Avatar',
slug: 'avatar',
@@ -48,10 +52,6 @@ export const components: Page[] = [
title: 'Checkbox',
slug: 'checkbox',
},
{
title: 'Collapsible',
slug: 'collapsible',
},
{
title: 'Dialog',
slug: 'dialog',