Merge pull request #29576 from backstage/canon-docs-updates

Canon - Added docs for Menu + Link
This commit is contained in:
Charles de Dreuille
2025-04-14 14:44:46 +02:00
committed by GitHub
8 changed files with 456 additions and 4 deletions
@@ -0,0 +1,84 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
MenuSnippet,
ButtonSnippet,
LinkSnippet,
} from '@/snippets/stories-snippets';
import { linkPropDefs } from './props';
# Link
A link component that renders a `<a>` element.
<Snippet
align="center"
py={4}
preview={<LinkSnippet story="Default" />}
code={`<Link href="https://backstage.io">Sign up for Backstage</Link>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Link } from '@backstage/canon';
<Link href="https://backstage.io">Sign up for Backstage</Link>`}
/>
</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-Link`
- `canon-Link[data-variant='body']`
- `canon-Link[data-variant='subtitle']`
- `canon-Link[data-variant='caption']`
- `canon-Link[data-variant='label']`
- `canon-Link[data-weight='regular']`
- `canon-Link[data-weight='bold']`
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable data={linkPropDefs} />
## Examples
### Variants
Here's a view when links have different variants.
<Snippet
align="center"
py={4}
open
preview={<LinkSnippet story="AllVariants" />}
code={`<Flex gap="4" direction="column">
<Link href="https://canon.backstage.io" variant="subtitle" />
<Link href="https://canon.backstage.io" variant="body" />
<Link href="https://canon.backstage.io" variant="caption" />
<Link href="https://canon.backstage.io" variant="label" />
</Flex>`}
/>
### Weights
Here's a view when links have different weights.
<Snippet
align="center"
py={4}
open
preview={<LinkSnippet story="AllWeights" />}
code={`<Flex gap="4" direction="column">
<Link href="https://canon.backstage.io" weight="regular" />
<Link href="https://canon.backstage.io" weight="bold" />
</Flex>`}
/>
@@ -0,0 +1,22 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const linkPropDefs: Record<string, PropDef> = {
to: {
type: 'string',
},
variant: {
type: 'enum',
values: ['subtitle', 'body', 'caption', 'label'],
default: 'body',
responsive: true,
},
weight: {
type: 'enum',
values: ['regular', 'bold'],
default: 'regular',
responsive: true,
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,157 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import { MenuSnippet } from '@/snippets/stories-snippets';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
import {
menuRootPropDefs,
menuTriggerPropDefs,
menuPositionerPropDefs,
menuItemPropDefs,
} from './props';
# Menu
A list of actions in a dropdown, enhanced with keyboard navigation.
<Snippet
align="center"
py={4}
preview={<MenuSnippet story="Default" />}
code={`<Menu.Root>
<Menu.Trigger
render={props => (
<Button
{...props}
size="medium"
variant="secondary"
iconEnd="chevron-down"
>
Menu
</Button>
)}
/>
<Menu.Portal>
<Menu.Positioner sideOffset={8} align="start">
<Menu.Popup>
<Menu.Item>Settings</Menu.Item>
<Menu.Item>Invite new members</Menu.Item>
<Menu.Item>Download app</Menu.Item>
<Menu.Item>Log out</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Menu } from '@backstage/canon';
<Menu.Root>
<Menu.Trigger />
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item />
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.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-MenuTrigger`
- `canon-MenuBackdrop`
- `canon-MenuPositioner`
- `canon-MenuPopup`
- `canon-MenuArrow`
- `canon-MenuItem`
- `canon-MenuGroup`
- `canon-MenuGroupLabel`
- `canon-MenuRadioGroup`
- `canon-MenuRadioItem`
- `canon-MenuRadioItemIndicator`
- `canon-MenuCheckboxItem`
- `canon-MenuCheckboxItemIndicator`
- `canon-MenuSubmenuTrigger`
- `canon-MenuSeparator`
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/menu" />
### Menu.Root
Groups all parts of the menu. Doesn't render its own HTML element.
<PropsTable data={menuRootPropDefs} />
### Menu.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={`<Menu.Trigger render={props => <Button {...props} />} />`} />
<PropsTable data={menuTriggerPropDefs} />
### Menu.Positioner
Positions the menu popup against the trigger. Renders a `<div>` element.
<PropsTable data={menuPositionerPropDefs} />
### Menu.Item
An individual interactive item in the menu. Renders a `<div>` element.
<PropsTable data={menuItemPropDefs} />
## Examples
### Open on hover
To create a menu that opens on hover, add the `openOnHover` prop to the Root part.
You can additionally configure how quickly the menu opens on hover using the `delay` prop.
<Snippet
align="center"
py={4}
open
preview={<MenuSnippet story="OpenOnHover" />}
code={`<Menu.Root openOnHover delay={100}>
<Menu.Trigger
render={props => (
<Button
{...props}
size="medium"
variant="secondary"
iconEnd="chevron-down"
>
Menu
</Button>
)}
/>
<Menu.Portal>
<Menu.Positioner sideOffset={8} align="start">
<Menu.Popup>
<Menu.Item>Settings</Menu.Item>
<Menu.Item>Invite new members</Menu.Item>
<Menu.Item>Download app</Menu.Item>
<Menu.Item>Log out</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>`}
/>
@@ -0,0 +1,142 @@
import {
classNamePropDefs,
stylePropDefs,
renderPropDefs,
} from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const menuRootPropDefs: Record<string, PropDef> = {
defaultOpen: {
type: 'boolean',
default: 'false',
},
open: {
type: 'boolean',
},
onOpenChange: {
type: 'enum',
values: ['(open, event) => void'],
},
closeParentOnEsc: {
type: 'boolean',
default: 'true',
},
modal: {
type: 'boolean',
default: 'true',
},
onOpenChangeComplete: {
type: 'enum',
values: ['(open) => void'],
},
disabled: {
type: 'boolean',
default: 'false',
},
openOnHover: {
type: 'boolean',
},
delay: {
type: 'number',
default: '100',
},
loop: {
type: 'boolean',
default: 'true',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'vertical',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuTriggerPropDefs: Record<string, PropDef> = {
...renderPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const menuPositionerPropDefs: Record<string, PropDef> = {
align: {
type: 'enum',
values: ['start', 'center', 'end'],
default: 'center',
},
alignOffset: {
type: 'enum',
values: ['number', '(data) => number'],
default: '0',
},
side: {
type: 'enum',
values: ['bottom', 'inline-end', 'inline-start', 'left', 'right', 'top'],
default: 'bottom',
},
sideOffset: {
type: 'enum',
values: ['number', '(data) => number'],
default: '0',
},
arrowPadding: {
type: 'number',
default: '5',
},
anchor: {
type: 'enum',
values: [
'React.Ref',
'Element',
'VirtualElement',
'(() => Element | VirtualElement | null)',
'null',
],
},
collisionBoundary: {
type: 'enum',
values: ['clipping-ancestors', 'Element', 'Element[]', 'Rect'],
default: 'clipping-ancestors',
},
collisionPadding: {
type: 'enum',
values: ['number', 'Rect'],
default: '5',
},
sticky: {
type: 'boolean',
default: 'false',
},
positionMethod: {
type: 'enum',
values: ['absolute', 'fixed'],
default: 'absolute',
},
trackAnchor: {
type: 'boolean',
default: 'true',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuItemPropDefs: Record<string, PropDef> = {
label: {
type: 'string',
},
onClick: {
type: 'enum',
values: ['(event) => void'],
},
closeOnClick: {
type: 'boolean',
default: 'true',
},
disabled: {
type: 'boolean',
default: 'false',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -13,6 +13,8 @@ import * as TextFieldStories from '../../../packages/canon/src/components/TextFi
import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories';
import * as FlexStories from '../../../packages/canon/src/components/Flex/Flex.stories';
import * as SelectStories from '../../../packages/canon/src/components/Select/Select.stories';
import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.stories';
import * as LinkStories from '../../../packages/canon/src/components/Link/Link.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
@@ -125,3 +127,17 @@ export const SelectSnippet = ({
return StoryComponent ? <StoryComponent /> : null;
};
export const MenuSnippet = ({ story }: { story: keyof typeof MenuStories }) => {
const stories = composeStories(MenuStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => {
const stories = composeStories(LinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
+10
View File
@@ -91,6 +91,16 @@ export const components: Page[] = [
slug: 'icon-button',
status: 'alpha',
},
{
title: 'Link',
slug: 'link',
status: 'alpha',
},
{
title: 'Menu',
slug: 'menu',
status: 'alpha',
},
{
title: 'Select',
slug: 'select',
+8
View File
@@ -203,3 +203,11 @@ export const childrenPropDefs: Record<string, PropDef> = {
responsive: false,
},
};
export const renderPropDefs: Record<string, PropDef> = {
render: {
type: 'enum',
values: ['React.ReactElement', '(props, state) => React.ReactElement'],
responsive: false,
},
};
@@ -32,7 +32,12 @@ export const Default: Story = {
<>
<Menu.Trigger
render={props => (
<Button {...props} size="small">
<Button
{...props}
size="medium"
variant="secondary"
iconEnd="chevron-down"
>
Menu
</Button>
)}
@@ -40,9 +45,10 @@ export const Default: Story = {
<Menu.Portal>
<Menu.Positioner sideOffset={8} align="start">
<Menu.Popup>
<Menu.Item>Item 1</Menu.Item>
<Menu.Item>Item 2</Menu.Item>
<Menu.Item>Item 3</Menu.Item>
<Menu.Item>Settings</Menu.Item>
<Menu.Item>Invite new members</Menu.Item>
<Menu.Item>Download app</Menu.Item>
<Menu.Item>Log out</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
@@ -57,3 +63,10 @@ export const Open: Story = {
open: true,
},
};
export const OpenOnHover: Story = {
args: {
...Default.args,
openOnHover: true,
},
};