Merge pull request #31371 from backstage/bui-dialog-component
BUI - New Dialog component
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Adding a new Dialog component to Backstage UI.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,107 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { DialogSnippet } from '@/snippets/stories-snippets';
|
||||
import {
|
||||
dialogPropDefs,
|
||||
dialogTriggerPropDefs,
|
||||
dialogHeaderPropDefs,
|
||||
dialogBodyPropDefs,
|
||||
dialogFooterPropDefs,
|
||||
dialogClosePropDefs,
|
||||
dialogUsageSnippet,
|
||||
dialogDefaultSnippet,
|
||||
dialogFixedWidthAndHeightSnippet,
|
||||
dialogWithFormSnippet,
|
||||
dialogWithNoTriggerSnippet,
|
||||
dialogCloseSnippet,
|
||||
} from './dialog.props';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
|
||||
<PageTitle
|
||||
title="Dialog"
|
||||
description="A modal dialog component that displays content in an overlay window."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<DialogSnippet story="Default" />}
|
||||
code={dialogDefaultSnippet}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={dialogUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
|
||||
### DialogTrigger
|
||||
|
||||
Wraps a trigger element and the dialog content to handle open/close state.
|
||||
|
||||
<PropsTable data={dialogTriggerPropDefs} />
|
||||
|
||||
### Dialog
|
||||
|
||||
The main dialog container that renders as a modal overlay.
|
||||
|
||||
<PropsTable data={dialogPropDefs} />
|
||||
|
||||
### DialogHeader
|
||||
|
||||
Displays the dialog title with a built-in close button.
|
||||
|
||||
<PropsTable data={dialogHeaderPropDefs} />
|
||||
|
||||
### DialogBody
|
||||
|
||||
The main content area of the dialog with optional scrolling.
|
||||
|
||||
<PropsTable data={dialogBodyPropDefs} />
|
||||
|
||||
### DialogFooter
|
||||
|
||||
Contains action buttons or other footer content.
|
||||
|
||||
<PropsTable data={dialogFooterPropDefs} />
|
||||
|
||||
If you want to close the dialog while pressing a button, you can use the `slot="close"` prop on the button.
|
||||
|
||||
<CodeBlock code={dialogCloseSnippet} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Fixed Width and Height
|
||||
|
||||
Dialog with a fixed height body that scrolls when content overflows.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<DialogSnippet story="PreviewFixedWidthAndHeight" />}
|
||||
code={dialogFixedWidthAndHeightSnippet}
|
||||
/>
|
||||
|
||||
### Dialog with Form
|
||||
|
||||
Dialog containing form elements for user input.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<DialogSnippet story="PreviewWithForm" />}
|
||||
code={dialogWithFormSnippet}
|
||||
/>
|
||||
|
||||
### Dialog with no trigger and controlled by props
|
||||
|
||||
You can also control the dialog using your own states.
|
||||
|
||||
<CodeBlock code={dialogWithNoTriggerSnippet} />
|
||||
|
||||
<Theming component="Dialog" />
|
||||
|
||||
<ChangelogComponent component="dialog" />
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const dialogTriggerPropDefs: Record<string, PropDef> = {
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
isOpen: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the overlay is open by default (controlled).',
|
||||
},
|
||||
defaultOpen: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the overlay is open by default (uncontrolled).',
|
||||
},
|
||||
onOpenChange: {
|
||||
type: 'enum',
|
||||
values: ['(isOpen: boolean) => void'],
|
||||
description:
|
||||
"Handler that is called when the overlay's open state changes.",
|
||||
},
|
||||
};
|
||||
|
||||
export const dialogPropDefs: Record<string, PropDef> = {
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
isOpen: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the overlay is open by default (controlled).',
|
||||
},
|
||||
defaultOpen: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the overlay is open by default (uncontrolled).',
|
||||
},
|
||||
onOpenChange: {
|
||||
type: 'enum',
|
||||
values: ['(isOpen: boolean) => void'],
|
||||
description:
|
||||
"Handler that is called when the overlay's open state changes.",
|
||||
},
|
||||
width: {
|
||||
type: 'enum',
|
||||
values: ['number', 'string'],
|
||||
responsive: false,
|
||||
},
|
||||
height: {
|
||||
type: 'enum',
|
||||
values: ['number', 'string'],
|
||||
responsive: false,
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const dialogHeaderPropDefs: Record<string, PropDef> = {
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const dialogBodyPropDefs: Record<string, PropDef> = {
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
height: {
|
||||
type: 'enum',
|
||||
values: ['number', 'string'],
|
||||
responsive: false,
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const dialogFooterPropDefs: Record<string, PropDef> = {
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const dialogClosePropDefs: Record<string, PropDef> = {
|
||||
variant: {
|
||||
type: 'enum',
|
||||
values: ['primary', 'secondary', 'tertiary'],
|
||||
default: 'secondary',
|
||||
responsive: false,
|
||||
},
|
||||
children: { type: 'enum', values: ['ReactNode'], responsive: false },
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const dialogUsageSnippet = `import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@backstage/ui';
|
||||
|
||||
<DialogTrigger>
|
||||
<Button>Open Dialog</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Title</DialogHeader>
|
||||
<DialogBody>Content</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">Close</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>`;
|
||||
|
||||
export const dialogDefaultSnippet = `<DialogTrigger>
|
||||
<Button variant="secondary">Open Dialog</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Example Dialog</DialogHeader>
|
||||
<DialogBody>
|
||||
<Text>This is a basic dialog example.</Text>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">Close</Button>
|
||||
<Button variant="primary" slot="close">Save</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>`;
|
||||
|
||||
export const dialogFixedWidthAndHeightSnippet = `<DialogTrigger>
|
||||
<Button variant="secondary">Scrollable Dialog</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Long Content Dialog</DialogHeader>
|
||||
<DialogBody width={600} height={400}>
|
||||
...
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">Cancel</Button>
|
||||
<Button variant="primary" slot="close">Accept</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>`;
|
||||
|
||||
export const dialogWithFormSnippet = `<DialogTrigger>
|
||||
<Button variant="secondary">Create User</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Create New User</DialogHeader>
|
||||
<DialogBody>
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField label="Name" placeholder="Enter full name" />
|
||||
<TextField label="Email" placeholder="Enter email address" />
|
||||
<Select label="Role">
|
||||
<SelectItem>Admin</SelectItem>
|
||||
<SelectItem>User</SelectItem>
|
||||
<SelectItem>Viewer</SelectItem>
|
||||
</Select>
|
||||
</Flex>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">Cancel</Button>
|
||||
<Button variant="primary" slot="close">Create User</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>`;
|
||||
|
||||
export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogHeader>Create New User</DialogHeader>
|
||||
<DialogBody>
|
||||
Your content
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">Cancel</Button>
|
||||
<Button variant="primary" slot="close">Create User</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>`;
|
||||
|
||||
export const dialogCloseSnippet = `<Button slot="close">Close</Button>`;
|
||||
@@ -17,6 +17,7 @@ import * as MenuStories from '../../../packages/ui/src/components/Menu/Menu.stor
|
||||
import * as LinkStories from '../../../packages/ui/src/components/Link/Link.stories';
|
||||
import * as AvatarStories from '../../../packages/ui/src/components/Avatar/Avatar.stories';
|
||||
import * as CollapsibleStories from '../../../packages/ui/src/components/Collapsible/Collapsible.stories';
|
||||
import * as DialogStories from '../../../packages/ui/src/components/Dialog/Dialog.stories';
|
||||
import * as RadioGroupStories from '../../../packages/ui/src/components/RadioGroup/RadioGroup.stories';
|
||||
import * as TabsStories from '../../../packages/ui/src/components/Tabs/Tabs.stories';
|
||||
import * as SwitchStories from '../../../packages/ui/src/components/Switch/Switch.stories';
|
||||
@@ -63,6 +64,7 @@ export const MenuSnippet = createSnippetComponent(MenuStories);
|
||||
export const LinkSnippet = createSnippetComponent(LinkStories);
|
||||
export const AvatarSnippet = createSnippetComponent(AvatarStories);
|
||||
export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
|
||||
export const DialogSnippet = createSnippetComponent(DialogStories);
|
||||
export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
|
||||
export const TabsSnippet = createSnippetComponent(TabsStories);
|
||||
export const SwitchSnippet = createSnippetComponent(SwitchStories);
|
||||
|
||||
@@ -101,6 +101,11 @@ export const components: Page[] = [
|
||||
slug: 'collapsible',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Dialog',
|
||||
slug: 'dialog',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Header',
|
||||
slug: 'header',
|
||||
|
||||
@@ -10838,6 +10838,129 @@
|
||||
|
||||
@layer legacy;
|
||||
|
||||
.bui-DialogOverlay {
|
||||
z-index: 1000;
|
||||
background: #e8e8e8cc;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .bui-Dialog {
|
||||
background: #00000080;
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-entering] {
|
||||
animation: .2s ease-out forwards fade-in;
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-exiting] {
|
||||
animation: .15s ease-out forwards fade-out;
|
||||
}
|
||||
|
||||
.bui-Dialog {
|
||||
background: var(--bui-bg-surface-1);
|
||||
border: 1px solid var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem));
|
||||
max-width: calc(100vw - 3rem);
|
||||
height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem));
|
||||
border-radius: .5rem;
|
||||
outline: none;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 3rem);
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-entering] .bui-Dialog {
|
||||
animation: .15s ease-out forwards dialog-enter;
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-exiting] .bui-Dialog {
|
||||
animation: .15s ease-out forwards dialog-exit;
|
||||
}
|
||||
|
||||
.bui-DialogHeader {
|
||||
padding-inline: var(--bui-space-3);
|
||||
padding-block: var(--bui-space-2);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bui-DialogHeaderTitle {
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bui-DialogFooter {
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
padding-inline: var(--bui-space-3);
|
||||
padding-block: var(--bui-space-3);
|
||||
border-top: 1px solid var(--bui-border);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bui-DialogBody {
|
||||
padding: var(--bui-space-3);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-enter {
|
||||
from {
|
||||
opacity: .5;
|
||||
transform: scale(.8);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-exit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(.95);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-MenuPopover {
|
||||
border: 1px solid var(--bui-border);
|
||||
border-radius: var(--bui-radius-2);
|
||||
|
||||
@@ -11,8 +11,12 @@ import { ColumnProps } from 'react-aria-components';
|
||||
import { ComponentProps } from 'react';
|
||||
import type { ComponentPropsWithRef } from 'react';
|
||||
import { Context } from 'react';
|
||||
import { DetailedHTMLProps } from 'react';
|
||||
import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components';
|
||||
import type { ElementType } from 'react';
|
||||
import { ForwardRefExoticComponent } from 'react';
|
||||
import type { HeadingProps } from 'react-aria-components';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { LinkProps as LinkProps_2 } from 'react-aria-components';
|
||||
import type { ListBoxItemProps } from 'react-aria-components';
|
||||
@@ -21,6 +25,7 @@ import type { MenuItemProps as MenuItemProps_2 } from 'react-aria-components';
|
||||
import type { MenuProps as MenuProps_2 } from 'react-aria-components';
|
||||
import type { MenuSectionProps as MenuSectionProps_2 } from 'react-aria-components';
|
||||
import type { MenuTriggerProps as MenuTriggerProps_2 } from 'react-aria-components';
|
||||
import type { ModalOverlayProps } from 'react-aria-components';
|
||||
import type { PopoverProps } from 'react-aria-components';
|
||||
import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components';
|
||||
import type { RadioProps as RadioProps_2 } from 'react-aria-components';
|
||||
@@ -436,6 +441,16 @@ export const componentDefinitions: {
|
||||
readonly root: 'bui-Container';
|
||||
};
|
||||
};
|
||||
readonly Dialog: {
|
||||
readonly classNames: {
|
||||
readonly overlay: 'bui-DialogOverlay';
|
||||
readonly dialog: 'bui-Dialog';
|
||||
readonly header: 'bui-DialogHeader';
|
||||
readonly headerTitle: 'bui-DialogHeaderTitle';
|
||||
readonly body: 'bui-DialogBody';
|
||||
readonly footer: 'bui-DialogFooter';
|
||||
};
|
||||
};
|
||||
readonly FieldLabel: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-FieldLabelWrapper';
|
||||
@@ -698,6 +713,64 @@ export type DataAttributesMap = Record<string, DataAttributeValues>;
|
||||
// @public
|
||||
export type DataAttributeValues = readonly (string | number | boolean)[];
|
||||
|
||||
// @public (undocumented)
|
||||
export const Dialog: ForwardRefExoticComponent<
|
||||
DialogProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DialogBody: ForwardRefExoticComponent<
|
||||
DialogBodyProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface DialogBodyProps {
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const DialogFooter: ForwardRefExoticComponent<
|
||||
Omit<
|
||||
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
||||
'ref'
|
||||
> &
|
||||
RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DialogHeader: ForwardRefExoticComponent<
|
||||
DialogHeaderProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface DialogHeaderProps extends HeadingProps {
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface DialogProps extends ModalOverlayProps {
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
// (undocumented)
|
||||
height?: number | string;
|
||||
// (undocumented)
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const DialogTrigger: (props: DialogTriggerProps) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export interface DialogTriggerProps extends DialogTriggerProps_2 {}
|
||||
|
||||
// @public (undocumented)
|
||||
export type Display = 'none' | 'flex' | 'block' | 'inline';
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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-vite';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from './Dialog';
|
||||
import { Button, Flex, Text, TextField, Select } from '@backstage/ui';
|
||||
import { useArgs } from 'storybook/preview-api';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Dialog',
|
||||
component: Dialog,
|
||||
args: {
|
||||
isOpen: undefined,
|
||||
defaultOpen: undefined,
|
||||
},
|
||||
argTypes: {
|
||||
isOpen: { control: 'boolean' },
|
||||
defaultOpen: { control: 'boolean' },
|
||||
},
|
||||
} satisfies Meta<typeof Dialog>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => {
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">Open Dialog</Button>
|
||||
<Dialog {...args}>
|
||||
<DialogHeader>Example Dialog</DialogHeader>
|
||||
<DialogBody>
|
||||
<Text>This is a basic dialog example.</Text>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" slot="close">
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Open: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
defaultOpen: true,
|
||||
},
|
||||
render: Default.render,
|
||||
};
|
||||
|
||||
export const NoTrigger: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
render: args => {
|
||||
const [{ isOpen }, updateArgs] = useArgs();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...args}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={value => updateArgs({ isOpen: value })}
|
||||
>
|
||||
<DialogHeader>Example Dialog</DialogHeader>
|
||||
<DialogBody>
|
||||
<Text>This is a basic dialog example.</Text>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" slot="close">
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const FixedWidth: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
width: 600,
|
||||
},
|
||||
render: args => (
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">Open Dialog</Button>
|
||||
<Dialog {...args}>
|
||||
<DialogHeader>Long Content Dialog</DialogHeader>
|
||||
<DialogBody>
|
||||
<Flex direction="column" gap="3">
|
||||
<Text>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
</Text>
|
||||
<Text>
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse
|
||||
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
|
||||
cupidatat non proident, sunt in culpa qui officia deserunt mollit
|
||||
anim id est laborum.
|
||||
</Text>
|
||||
<Text>
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
|
||||
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
|
||||
quae ab illo inventore veritatis et quasi architecto beatae vitae
|
||||
dicta sunt explicabo.
|
||||
</Text>
|
||||
</Flex>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" slot="close">
|
||||
Accept
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>
|
||||
),
|
||||
};
|
||||
|
||||
export const FixedHeight: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
height: 500,
|
||||
},
|
||||
render: FixedWidth.render,
|
||||
};
|
||||
|
||||
export const FixedWidthAndHeight: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
width: 600,
|
||||
height: 400,
|
||||
},
|
||||
render: FixedWidth.render,
|
||||
};
|
||||
|
||||
export const FullWidthAndHeight: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
render: FixedWidth.render,
|
||||
};
|
||||
|
||||
export const Confirmation: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
render: args => (
|
||||
<DialogTrigger {...args}>
|
||||
<Button variant="secondary">Delete Item</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Confirm Delete</DialogHeader>
|
||||
<DialogBody>
|
||||
<Text>
|
||||
Are you sure you want to delete this item? This action cannot be
|
||||
undone.
|
||||
</Text>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" slot="close">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithForm: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
render: args => (
|
||||
<DialogTrigger {...args}>
|
||||
<Button variant="secondary">Create User</Button>
|
||||
<Dialog>
|
||||
<DialogHeader>Create New User</DialogHeader>
|
||||
<DialogBody>
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField label="Name" placeholder="Enter full name" />
|
||||
<TextField label="Email" placeholder="Enter email address" />
|
||||
<Select
|
||||
label="Role"
|
||||
options={[
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" slot="close">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" slot="close">
|
||||
Create User
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</DialogTrigger>
|
||||
),
|
||||
};
|
||||
|
||||
export const PreviewFixedWidthAndHeight: Story = {
|
||||
args: {
|
||||
defaultOpen: undefined,
|
||||
width: 600,
|
||||
height: 400,
|
||||
},
|
||||
render: FixedWidth.render,
|
||||
};
|
||||
|
||||
export const PreviewWithForm: Story = {
|
||||
args: {
|
||||
defaultOpen: undefined,
|
||||
},
|
||||
render: WithForm.render,
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
/* Backdrop */
|
||||
.bui-DialogOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(232, 232, 232, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .bui-Dialog {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-entering] {
|
||||
animation: fade-in 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
.bui-DialogOverlay[data-exiting] {
|
||||
animation: fade-out 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
.bui-Dialog {
|
||||
background: var(--bui-bg-surface-1);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
position: relative;
|
||||
width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem));
|
||||
max-width: calc(100vw - 3rem);
|
||||
height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem));
|
||||
max-height: calc(100vh - 3rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Dialog entering animation */
|
||||
.bui-DialogOverlay[data-entering] .bui-Dialog {
|
||||
animation: dialog-enter 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* Dialog exiting animation */
|
||||
.bui-DialogOverlay[data-exiting] .bui-Dialog {
|
||||
animation: dialog-exit 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
.bui-DialogHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-inline: var(--bui-space-3);
|
||||
padding-block: var(--bui-space-2);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
}
|
||||
|
||||
.bui-DialogHeaderTitle {
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bui-DialogFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: var(--bui-space-2);
|
||||
padding-inline: var(--bui-space-3);
|
||||
padding-block: var(--bui-space-3);
|
||||
border-top: 1px solid var(--bui-border);
|
||||
}
|
||||
|
||||
.bui-DialogBody {
|
||||
padding: var(--bui-space-3);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Keyframe animations */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-enter {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-exit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 {
|
||||
Dialog as RADialog,
|
||||
DialogTrigger as RADialogTrigger,
|
||||
Modal,
|
||||
Heading,
|
||||
} from 'react-aria-components';
|
||||
import clsx from 'clsx';
|
||||
import type {
|
||||
DialogTriggerProps,
|
||||
DialogHeaderProps,
|
||||
DialogProps,
|
||||
DialogBodyProps,
|
||||
} from './types';
|
||||
import './Dialog.styles.css';
|
||||
import { RiCloseLine } from '@remixicon/react';
|
||||
import { Button } from '../Button';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { Flex } from '../Flex';
|
||||
|
||||
/** @public */
|
||||
export const DialogTrigger = (props: DialogTriggerProps) => {
|
||||
return <RADialogTrigger {...props} />;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const Dialog = forwardRef<React.ElementRef<typeof Modal>, DialogProps>(
|
||||
({ className, children, width, height, style, ...props }, ref) => {
|
||||
const { classNames } = useStyles('Dialog');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
ref={ref}
|
||||
className={clsx(classNames.overlay)}
|
||||
isDismissable
|
||||
isKeyboardDismissDisabled={false}
|
||||
{...props}
|
||||
>
|
||||
<RADialog
|
||||
className={clsx(classNames.dialog, className)}
|
||||
style={{
|
||||
['--bui-dialog-min-width' as keyof React.CSSProperties]:
|
||||
typeof width === 'number' ? `${width}px` : width || '400px',
|
||||
['--bui-dialog-min-height' as keyof React.CSSProperties]: height
|
||||
? typeof height === 'number'
|
||||
? `${height}px`
|
||||
: height
|
||||
: 'auto',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RADialog>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Dialog.displayName = 'Dialog';
|
||||
|
||||
/** @public */
|
||||
export const DialogHeader = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
DialogHeaderProps
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { classNames } = useStyles('Dialog');
|
||||
|
||||
return (
|
||||
<Flex ref={ref} className={clsx(classNames.header, className)} {...props}>
|
||||
<Heading slot="title" className={classNames.headerTitle}>
|
||||
{children}
|
||||
</Heading>
|
||||
<Button name="close" aria-label="Close" variant="tertiary" slot="close">
|
||||
<RiCloseLine />
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
/** @public */
|
||||
export const DialogBody = forwardRef<React.ElementRef<'div'>, DialogBodyProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { classNames } = useStyles('Dialog');
|
||||
|
||||
return (
|
||||
<div className={clsx(classNames.body, className)} ref={ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DialogBody.displayName = 'DialogBody';
|
||||
|
||||
/** @public */
|
||||
export const DialogFooter = forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { classNames } = useStyles('Dialog');
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx(classNames.footer, className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
@@ -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 * from './Dialog';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
DialogTriggerProps as RADialogTriggerProps,
|
||||
ModalOverlayProps as RAModalProps,
|
||||
HeadingProps as RAHeadingProps,
|
||||
} from 'react-aria-components';
|
||||
|
||||
/**
|
||||
* Props for the DialogTrigger component.
|
||||
* @public
|
||||
*/
|
||||
export interface DialogTriggerProps extends RADialogTriggerProps {}
|
||||
|
||||
/**
|
||||
* Props for the Dialog component.
|
||||
* @public
|
||||
*/
|
||||
export interface DialogProps extends RAModalProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the DialogHeader component.
|
||||
* @public
|
||||
*/
|
||||
export interface DialogHeaderProps extends RAHeadingProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the DialogBody component.
|
||||
* @public
|
||||
*/
|
||||
export interface DialogBodyProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
@import '../components/Checkbox/styles.css';
|
||||
@import '../components/Collapsible/Collapsible.styles.css';
|
||||
@import '../components/Container/styles.css';
|
||||
@import '../components/Dialog/Dialog.styles.css';
|
||||
@import '../components/FieldError/FieldError.styles.css';
|
||||
@import '../components/FieldLabel/FieldLabel.styles.css';
|
||||
@import '../components/Flex/styles.css';
|
||||
|
||||
@@ -34,6 +34,7 @@ export * from './components/Avatar';
|
||||
export * from './components/Button';
|
||||
export * from './components/Card';
|
||||
export * from './components/Collapsible';
|
||||
export * from './components/Dialog';
|
||||
export * from './components/FieldLabel';
|
||||
export * from './components/Header';
|
||||
export * from './components/HeaderPage';
|
||||
|
||||
@@ -85,6 +85,16 @@ export const componentDefinitions = {
|
||||
root: 'bui-Container',
|
||||
},
|
||||
},
|
||||
Dialog: {
|
||||
classNames: {
|
||||
overlay: 'bui-DialogOverlay',
|
||||
dialog: 'bui-Dialog',
|
||||
header: 'bui-DialogHeader',
|
||||
headerTitle: 'bui-DialogHeaderTitle',
|
||||
body: 'bui-DialogBody',
|
||||
footer: 'bui-DialogFooter',
|
||||
},
|
||||
},
|
||||
FieldLabel: {
|
||||
classNames: {
|
||||
root: 'bui-FieldLabelWrapper',
|
||||
|
||||
Reference in New Issue
Block a user