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:
@@ -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" />
|
||||
@@ -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>`;
|
||||
@@ -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" />
|
||||
@@ -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>`;
|
||||
@@ -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);
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Component =
|
||||
| 'datatable'
|
||||
| 'select'
|
||||
| 'collapsible'
|
||||
| 'accordion'
|
||||
| 'checkbox'
|
||||
| 'container'
|
||||
| 'link'
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user