diff --git a/.changeset/fifty-days-like.md b/.changeset/fifty-days-like.md new file mode 100644 index 0000000000..60ed73272b --- /dev/null +++ b/.changeset/fifty-days-like.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Add new Collapsible component for Canon. diff --git a/canon-docs/src/app/(docs)/components/collapsible/page.mdx b/canon-docs/src/app/(docs)/components/collapsible/page.mdx new file mode 100644 index 0000000000..e013dff89c --- /dev/null +++ b/canon-docs/src/app/(docs)/components/collapsible/page.mdx @@ -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. + +} + code={` + ( + + )} /> + + + It's the edge of the world and all of Western civilization + The sun may rise in the East, at least it settled in a final location + It's understood that Hollywood sells Californication + + +`} +/> + + + + Usage + Theming + + + + ( + + )} /> + Your content +`} + /> + + + 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` + + + +## API reference + +### Collapsible.Root + +Groups all parts of the collapsible. Renders a `
` element. + + + +### 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. + + + )} /> + + + It's the edge of the world and all of Western civilization + The sun may rise in the East, at least it settled in a final location + It's understood that Hollywood sells Californication + + +`} +/> diff --git a/canon-docs/src/app/(docs)/components/collapsible/props.ts b/canon-docs/src/app/(docs)/components/collapsible/props.ts new file mode 100644 index 0000000000..02c62dc89e --- /dev/null +++ b/canon-docs/src/app/(docs)/components/collapsible/props.ts @@ -0,0 +1,43 @@ +import { + classNamePropDefs, + stylePropDefs, + renderPropDefs, +} from '@/utils/propDefs'; +import type { PropDef } from '@/utils/propDefs'; + +export const collapsibleRootPropDefs: Record = { + defaultOpen: { + type: 'boolean', + default: 'false', + }, + open: { + type: 'boolean', + }, + onOpenChange: { + type: 'enum', + values: ['(open) => void'], + }, + ...renderPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const collapsibleTriggerPropDefs: Record = { + ...renderPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const collapsiblePanelPropDefs: Record = { + hiddenUntilFound: { + type: 'boolean', + default: 'false', + }, + keepMounted: { + type: 'boolean', + default: 'false', + }, + ...renderPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/canon-docs/src/components/Snippet/index.tsx b/canon-docs/src/components/Snippet/index.tsx index e1af88cb12..d579bf5f7a 100644 --- a/canon-docs/src/components/Snippet/index.tsx +++ b/canon-docs/src/components/Snippet/index.tsx @@ -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 ( -
+
{ const stories = composeStories(BoxStories); @@ -153,3 +154,14 @@ export const AvatarSnippet = ({ return StoryComponent ? : null; }; + +export const CollapsibleSnippet = ({ + story, +}: { + story: keyof typeof CollapsibleStories; +}) => { + const stories = composeStories(CollapsibleStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index d1e9277e57..891b057ad6 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -81,6 +81,11 @@ export const components: Page[] = [ slug: 'checkbox', status: 'alpha', }, + { + title: 'Collapsible', + slug: 'collapsible', + status: 'alpha', + }, { title: 'Heading', slug: 'heading', diff --git a/packages/canon/css/collapsible.css b/packages/canon/css/collapsible.css new file mode 100644 index 0000000000..e5508f1ecb --- /dev/null +++ b/packages/canon/css/collapsible.css @@ -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; + } +} diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index c2ad293461..f67d0b332d 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -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; diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 3d4e5ac0e2..0fa08102fd 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -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; diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 088da028b8..0a315ae105 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -6,6 +6,7 @@ import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; import { ChangeEvent } from 'react'; +import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { Context } from 'react'; import type { CSSProperties } from 'react'; import { FC } from 'react'; @@ -207,6 +208,25 @@ export interface CheckboxProps { value?: string; } +// @public +export const Collapsible: { + Root: ForwardRefExoticComponent< + Omit, 'ref'> & + RefAttributes + >; + Trigger: ForwardRefExoticComponent< + Omit< + Collapsible_2.Trigger.Props & RefAttributes, + 'ref' + > & + RefAttributes + >; + Panel: ForwardRefExoticComponent< + Omit, 'ref'> & + RefAttributes + >; +}; + // @public (undocumented) export type Columns = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'auto'; diff --git a/packages/canon/src/components/Collapsible/Collapsible.stories.tsx b/packages/canon/src/components/Collapsible/Collapsible.stories.tsx new file mode 100644 index 0000000000..4130419a0c --- /dev/null +++ b/packages/canon/src/components/Collapsible/Collapsible.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + style: { + display: 'flex', + flexDirection: 'column', + gap: 'var(--canon-space-2)', + alignItems: 'center', + }, + children: ( + <> + ( + + )} + /> + + + + It's the edge of the world and all of Western civilization + + + The sun may rise in the East, at least it settled in a final + location + + It's understood that Hollywood sells Californication + + + + ), + }, +}; + +export const Open: Story = { + args: { + ...Default.args, + defaultOpen: true, + }, +}; diff --git a/packages/canon/src/components/Collapsible/Collapsible.styles.css b/packages/canon/src/components/Collapsible/Collapsible.styles.css new file mode 100644 index 0000000000..78823cbb84 --- /dev/null +++ b/packages/canon/src/components/Collapsible/Collapsible.styles.css @@ -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; + } +} diff --git a/packages/canon/src/components/Collapsible/Collapsible.tsx b/packages/canon/src/components/Collapsible/Collapsible.tsx new file mode 100644 index 0000000000..75ba4565e0 --- /dev/null +++ b/packages/canon/src/components/Collapsible/Collapsible.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CollapsibleRoot.displayName = CollapsiblePrimitive.Root.displayName; + +const CollapsibleTrigger = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName; + +const CollapsiblePanel = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +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, +}; diff --git a/packages/canon/src/components/Collapsible/index.ts b/packages/canon/src/components/Collapsible/index.ts new file mode 100644 index 0000000000..6a7f8e8816 --- /dev/null +++ b/packages/canon/src/components/Collapsible/index.ts @@ -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'; diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index a26ebffae9..53c2882302 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -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'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index e5e38e696c..7677a539c7 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -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';