Add Collapsible + docs
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -81,6 +81,11 @@ export const components: Page[] = [
|
||||
slug: 'checkbox',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Collapsible',
|
||||
slug: 'collapsible',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Heading',
|
||||
slug: 'heading',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user