Add Collapsible + docs

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-04-15 15:29:11 +02:00
parent da1da033f3
commit 5a7cf6e027
14 changed files with 405 additions and 2 deletions
@@ -0,0 +1,113 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import { CollapsibleSnippet } from '@/snippets/stories-snippets';
import {
collapsibleRootPropDefs,
collapsibleTriggerPropDefs,
collapsiblePanelPropDefs,
} from './props';
# Collapsible
An avatar component with a fallback for initials.
<Snippet
align="center"
py={4}
height={240}
preview={<CollapsibleSnippet story="Default" />}
code={`<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>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Collapsible } from '@backstage/canon';
<Collapsible.Root>
<Collapsible.Trigger render={(props, state) => (
<Button {...props}>
{state.open ? 'Close Panel' : 'Open Panel'}
</Button>
)} />
<Collapsible.Panel>Your content</Collapsible.Panel>
</Collapsible.Root>`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
- `canon-CollapsibleRoot`
- `canon-CollapsibleTrigger`
- `canon-CollapsiblePanel`
</Tabs.Panel>
</Tabs.Root>
## 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={`<Collapsible.Trigger render={props => <Button {...props} />} />`}
/>
<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={`<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>`}
/>
@@ -0,0 +1,43 @@
import {
classNamePropDefs,
stylePropDefs,
renderPropDefs,
} from '@/utils/propDefs';
import 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,
};
+4 -2
View File
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { ReactNode, CSSProperties } from 'react';
import { CodeBlock } from '../CodeBlock';
import { Collapsible } from '@base-ui-components/react/collapsible';
import styles from './styles.module.css';
@@ -10,6 +10,7 @@ interface SnippetProps {
px?: number;
py?: number;
open?: boolean;
height?: CSSProperties['height'];
}
export const Snippet = ({
@@ -19,10 +20,11 @@ export const Snippet = ({
px = 2,
py = 2,
open = false,
height = 'auto',
}: SnippetProps) => {
return (
<Collapsible.Root className={styles.container} defaultOpen={open}>
<div className={styles.preview}>
<div className={styles.preview} style={{ height }}>
<div
className={`${styles.previewContent} ${styles[align]}`}
style={{ padding: `${py}rem ${px}rem` }}
@@ -16,6 +16,7 @@ import * as SelectStories from '../../../packages/canon/src/components/Select/Se
import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.stories';
import * as LinkStories from '../../../packages/canon/src/components/Link/Link.stories';
import * as AvatarStories from '../../../packages/canon/src/components/Avatar/Avatar.stories';
import * as CollapsibleStories from '../../../packages/canon/src/components/Collapsible/Collapsible.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
@@ -153,3 +154,14 @@ export const AvatarSnippet = ({
return StoryComponent ? <StoryComponent /> : null;
};
export const CollapsibleSnippet = ({
story,
}: {
story: keyof typeof CollapsibleStories;
}) => {
const stories = composeStories(CollapsibleStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
+5
View File
@@ -81,6 +81,11 @@ export const components: Page[] = [
slug: 'checkbox',
status: 'alpha',
},
{
title: 'Collapsible',
slug: 'collapsible',
status: 'alpha',
},
{
title: 'Heading',
slug: 'heading',
+10
View File
@@ -0,0 +1,10 @@
.canon-CollapsiblePanel {
height: var(--collapsible-panel-height);
transition: all .15s ease-out;
display: flex;
overflow: hidden;
&[data-starting-style], &[data-ending-style] {
height: 0;
}
}
+11
View File
@@ -144,6 +144,17 @@
height: 1.5rem;
}
.canon-CollapsiblePanel {
height: var(--collapsible-panel-height);
transition: all .15s ease-out;
display: flex;
overflow: hidden;
&[data-starting-style], &[data-ending-style] {
height: 0;
}
}
.canon-DataTableRoot {
gap: var(--canon-space-3);
flex-direction: column;
+11
View File
@@ -9368,6 +9368,17 @@
height: 1.5rem;
}
.canon-CollapsiblePanel {
height: var(--collapsible-panel-height);
transition: all .15s ease-out;
display: flex;
overflow: hidden;
&[data-starting-style], &[data-ending-style] {
height: 0;
}
}
.canon-DataTableRoot {
gap: var(--canon-space-3);
flex-direction: column;
@@ -0,0 +1,83 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Meta, StoryObj } from '@storybook/react';
import { Collapsible } from './';
import { Button } from '../Button';
import { Box } from '../Box';
import { Text } from '../Text';
const meta = {
title: 'Components/Collapsible',
component: Collapsible.Root,
} satisfies Meta<typeof Collapsible.Root>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
style: {
display: 'flex',
flexDirection: 'column',
gap: 'var(--canon-space-2)',
alignItems: 'center',
},
children: (
<>
<Collapsible.Trigger
render={(props, state) => (
<Button
variant="secondary"
iconEnd={state.open ? 'chevron-up' : 'chevron-down'}
{...props}
>
{state.open ? 'Close Panel' : 'Open Panel'}
</Button>
)}
/>
<Collapsible.Panel>
<Box
p="4"
style={{
border: '1px solid var(--canon-border)',
backgroundColor: 'var(--canon-bg-surface-1)',
color: 'var(--canon-fg-primary)',
borderRadius: 'var(--canon-radius-2)',
width: '460px',
}}
>
<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>
</>
),
},
};
export const Open: Story = {
args: {
...Default.args,
defaultOpen: true,
},
};
@@ -0,0 +1,27 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.canon-CollapsiblePanel {
display: flex;
height: var(--collapsible-panel-height);
overflow: hidden;
transition: all 150ms ease-out;
&[data-starting-style],
&[data-ending-style] {
height: 0;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef } from 'react';
import { Collapsible as CollapsiblePrimitive } from '@base-ui-components/react/collapsible';
import clsx from 'clsx';
const CollapsibleRoot = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Root
ref={ref}
className={clsx('canon-CollapsibleRoot', className)}
{...props}
/>
));
CollapsibleRoot.displayName = CollapsiblePrimitive.Root.displayName;
const CollapsibleTrigger = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Trigger
ref={ref}
className={clsx('canon-CollapsibleTrigger', className)}
{...props}
/>
));
CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName;
const CollapsiblePanel = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Panel>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Panel>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Panel
ref={ref}
className={clsx('canon-CollapsiblePanel', className)}
{...props}
/>
));
CollapsiblePanel.displayName = CollapsiblePrimitive.Panel.displayName;
/**
* Collapsible is a component that allows you to collapse and expand content.
* It is a wrapper around the CollapsiblePrimitive component from base-ui-components.
*
* @public
*/
export const Collapsible = {
Root: CollapsibleRoot,
Trigger: CollapsibleTrigger,
Panel: CollapsiblePanel,
};
@@ -0,0 +1,17 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Collapsible } from './Collapsible';
+1
View File
@@ -17,6 +17,7 @@
@import '../components/Avatar/Avatar.styles.css';
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/Collapsible/Collapsible.styles.css';
@import '../components/DataTable/Root/DataTableRoot.styles.css';
@import '../components/DataTable/Pagination/DataTablePagination.styles.css';
@import '../components/Flex/styles.css';
+1
View File
@@ -34,6 +34,7 @@ export * from './components/Heading';
// UI components
export * from './components/Avatar';
export * from './components/Button';
export * from './components/Collapsible';
export * from './components/DataTable';
export * from './components/Icon';
export * from './components/IconButton';