Add docs for Popover

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-01-14 22:48:14 +01:00
parent 24e42a83ca
commit ff3b228a6e
6 changed files with 296 additions and 50 deletions
+8
View File
@@ -0,0 +1,8 @@
'use client';
import * as stories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
export const Default = () => {
const { CustomField } = stories;
return <CustomField.Component />;
};
+2 -44
View File
@@ -13,53 +13,11 @@ import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { PasswordFieldDefinition } from '../utils/definitions';
import { Default } from './PasswordField';
<PageTitle
title="PasswordField"
description="A password field component for your forms."
/>
<Snippet
align="center"
py={4}
preview={<PasswordFieldSnippet story="WithLabel" />}
code={passwordFieldDefaultSnippet}
/>
## Usage
<CodeBlock code={passwordFieldUsageSnippet} />
## API reference
<PropsTable data={inputPropDefs} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<PasswordFieldSnippet story="Sizes" />}
code={passwordFieldSizesSnippet}
/>
### With description
Here's a simple PasswordField with a description.
<Snippet
align="center"
py={4}
open
preview={<PasswordFieldSnippet story="WithDescription" />}
code={passwordFieldDescriptionSnippet}
/>
<Theming definition={PasswordFieldDefinition} />
<ChangelogComponent component="password-field" />
<Default />
+102
View File
@@ -0,0 +1,102 @@
import { PropsTable } from '@/components/PropsTable';
import { PopoverSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
popoverPropDefs,
popoverUsageSnippet,
popoverDefaultSnippet,
popoverPlacementsSnippet,
popoverNoArrowSnippet,
popoverRichContentSnippet,
popoverNonModalSnippet,
} from './popover.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { PopoverDefinition } from '../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Popover"
description="A popover displays floating content anchored to a trigger element with automatic positioning and collision detection."
/>
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="Default" />}
code={popoverDefaultSnippet}
/>
## Usage
<CodeBlock code={popoverUsageSnippet} />
## API reference
### Popover
The main popover component that displays floating content with automatic positioning.
<PropsTable data={popoverPropDefs} />
## Examples
### Placements
Popover supports multiple placement options to position the content relative to the trigger.
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="DocsAllPlacements" />}
code={popoverPlacementsSnippet}
/>
### Without Arrow
You can hide the arrow by setting the `hideArrow` prop.
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="Default" />}
code={popoverNoArrowSnippet}
/>
### Rich Content
Popovers can contain complex content including buttons, forms, and other interactive elements.
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="Default" />}
code={popoverRichContentSnippet}
/>
### Non-Modal
Non-modal popovers allow interaction with other elements on the page while the popover is open.
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="Default" />}
code={popoverNonModalSnippet}
/>
### Long Content
Popovers automatically handle scrolling when content exceeds the available viewport space.
<Snippet
align="center"
py={4}
preview={<PopoverSnippet story="Default" />}
code={popoverDefaultSnippet}
/>
<Theming definition={PopoverDefinition} />
<ChangelogComponent component="popover" />
+174
View File
@@ -0,0 +1,174 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const popoverPropDefs: Record<string, PropDef> = {
hideArrow: {
type: 'boolean',
default: 'false',
description:
'Whether to hide the arrow pointing to the trigger element. Arrow is also automatically hidden for MenuTrigger and SubmenuTrigger contexts.',
},
placement: {
type: 'enum',
values: [
'top',
'top start',
'top end',
'bottom',
'bottom start',
'bottom end',
'left',
'left start',
'left end',
'right',
'right start',
'right end',
],
default: 'bottom',
description: 'The placement of the popover relative to the trigger.',
},
containerPadding: {
type: 'number',
default: '12',
description:
'The padding between the popover and the edge of the viewport.',
},
offset: {
type: 'number',
default: '0',
description: 'The offset from the trigger element.',
},
crossOffset: {
type: 'number',
default: '0',
description: 'The cross-axis offset from the trigger element.',
},
shouldFlip: {
type: 'boolean',
default: 'true',
description:
'Whether the popover should flip to the opposite side when there is not enough space.',
},
isOpen: {
type: 'boolean',
description: 'Whether the popover is open (controlled).',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the popover is open by default (uncontrolled).',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description:
"Handler that is called when the popover's open state changes.",
},
isNonModal: {
type: 'boolean',
default: 'false',
description:
'Whether the popover is non-modal. Non-modal popovers do not block interaction with the rest of the page.',
},
isKeyboardDismissDisabled: {
type: 'boolean',
default: 'false',
description:
'Whether pressing the escape key to close the popover should be disabled.',
},
shouldCloseOnBlur: {
type: 'boolean',
default: 'true',
description:
'Whether to close the popover when the user interacts outside it.',
},
triggerRef: {
type: 'enum',
values: ['RefObject<Element | null>'],
description: 'A ref for the trigger element.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const popoverUsageSnippet = `import { Popover, DialogTrigger, Button } from '@backstage/ui';
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Text>This is a popover</Text>
</Popover>
</DialogTrigger>`;
export const popoverDefaultSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Text>This is a popover</Text>
</Popover>
</DialogTrigger>`;
export const popoverPlacementsSnippet = `<DialogTrigger>
<Button>Top</Button>
<Popover placement="top">
<Text>Top placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Right</Button>
<Popover placement="right">
<Text>Right placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Bottom</Button>
<Popover placement="bottom">
<Text>Bottom placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Left</Button>
<Popover placement="left">
<Text>Left placement</Text>
</Popover>
</DialogTrigger>`;
export const popoverNoArrowSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover hideArrow>
<Text>Popover without arrow</Text>
</Popover>
</DialogTrigger>`;
export const popoverRichContentSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Flex direction="column" gap="3">
<Text style={{ fontWeight: 'bold' }}>Popover Title</Text>
<Text>
This is a popover with rich content. It can contain multiple
elements and formatted text.
</Text>
<Flex gap="2" justify="end">
<Button variant="tertiary" size="small">Cancel</Button>
<Button variant="primary" size="small">Confirm</Button>
</Flex>
</Flex>
</Popover>
</DialogTrigger>`;
export const popoverNonModalSnippet = `<DialogTrigger>
<Button>Open Non-Modal Popover</Button>
<Popover isNonModal>
<Text>
This is a non-modal popover. You can interact with other
elements on the page while it's open.
</Text>
</Popover>
</DialogTrigger>`;
+5 -6
View File
@@ -1,6 +1,5 @@
'use client';
import { composeStories } from '@storybook/react';
import * as BoxStories from '../../../packages/ui/src/components/Box/Box.stories';
import * as ButtonStories from '../../../packages/ui/src/components/Button/Button.stories';
import * as ButtonIconStories from '../../../packages/ui/src/components/ButtonIcon/ButtonIcon.stories';
@@ -29,6 +28,7 @@ import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPa
import * as TableStories from '../../../packages/ui/src/components/Table/stories/Table.docs.stories';
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
import * as PopoverStories from '../../../packages/ui/src/components/Popover/Popover.stories';
import * as VisuallyHiddenStories from '../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories';
import * as ToggleButtonStories from '../../../packages/ui/src/components/ToggleButton/ToggleButton.stories';
import * as ToggleButtonGroupStories from '../../../packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories';
@@ -37,11 +37,9 @@ import * as ToggleButtonGroupStories from '../../../packages/ui/src/components/T
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createSnippetComponent = (stories: any) => {
return function SnippetComponent({ story }: { story: string }) {
const composedStories = composeStories(stories);
const StoryComponent = composedStories[
story as keyof typeof composedStories
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
// In CSF3, stories are already React components that can be rendered directly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const StoryComponent = stories[story as keyof typeof stories] as any;
return StoryComponent ? <StoryComponent /> : null;
};
@@ -58,6 +56,7 @@ export const GridSnippet = createSnippetComponent(GridStories);
export const TextFieldSnippet = createSnippetComponent(TextFieldStories);
export const PasswordFieldSnippet =
createSnippetComponent(PasswordFieldStories);
export const PopoverSnippet = createSnippetComponent(PopoverStories);
export const TextSnippet = createSnippetComponent(TextStories);
export const FlexSnippet = createSnippetComponent(FlexStories);
export const SelectSnippet = createSnippetComponent(SelectStories);
+5
View File
@@ -76,6 +76,11 @@ export const components: Page[] = [
title: 'PasswordField',
slug: 'password-field',
},
{
title: 'Popover',
slug: 'popover',
status: 'new',
},
{
title: 'RadioGroup',
slug: 'radio-group',