Remove DialogClose and add width and height props

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-10-12 16:05:04 +01:00
parent c3ea9e0b16
commit 85d50f3716
8 changed files with 231 additions and 200 deletions
+13 -14
View File
@@ -11,9 +11,10 @@ import {
dialogClosePropDefs,
dialogUsageSnippet,
dialogDefaultSnippet,
dialogScrollableSnippet,
dialogFixedWidthAndHeightSnippet,
dialogWithFormSnippet,
dialogWithNoTriggerSnippet,
dialogCloseSnippet,
} from './dialog.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -37,18 +38,18 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
### Dialog
The main dialog container that renders as a modal overlay.
<PropsTable data={dialogPropDefs} />
### 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.
@@ -67,23 +68,21 @@ Contains action buttons or other footer content.
<PropsTable data={dialogFooterPropDefs} />
### DialogClose
If you want to close the dialog while pressing a button, you can use the `slot="close"` prop on the button.
A button that closes the dialog when pressed.
<PropsTable data={dialogClosePropDefs} />
<CodeBlock code={dialogCloseSnippet} />
## Examples
### Scrollable Content
### Fixed Width and Height
Dialog with a fixed height body that scrolls when content overflows.
<Snippet
align="center"
py={4}
preview={<DialogSnippet story="PreviewScrollable" />}
code={dialogScrollableSnippet}
preview={<DialogSnippet story="PreviewFixedWidthAndHeight" />}
code={dialogFixedWidthAndHeightSnippet}
/>
### Dialog with Form
+58 -40
View File
@@ -4,14 +4,52 @@ import {
type PropDef,
} from '@/utils/propDefs';
export const dialogPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
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> = {
@@ -55,7 +93,6 @@ export const dialogUsageSnippet = `import {
DialogHeader,
DialogBody,
DialogFooter,
DialogClose
} from '@backstage/ui';
<DialogTrigger>
@@ -64,7 +101,7 @@ export const dialogUsageSnippet = `import {
<DialogHeader>Title</DialogHeader>
<DialogBody>Content</DialogBody>
<DialogFooter>
<DialogClose>Close</DialogClose>
<Button variant="secondary" slot="close">Close</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
@@ -77,39 +114,22 @@ export const dialogDefaultSnippet = `<DialogTrigger>
<Text>This is a basic dialog example.</Text>
</DialogBody>
<DialogFooter>
<DialogClose>Close</DialogClose>
<DialogClose variant="primary">Save</DialogClose>
<Button variant="secondary" slot="close">Close</Button>
<Button variant="primary" slot="close">Save</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
export const dialogScrollableSnippet = `<DialogTrigger>
export const dialogFixedWidthAndHeightSnippet = `<DialogTrigger>
<Button variant="secondary">Scrollable Dialog</Button>
<Dialog>
<DialogHeader>Long Content Dialog</DialogHeader>
<DialogBody height={200}>
<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>
<DialogBody width={600} height={400}>
...
</DialogBody>
<DialogFooter>
<Flex gap="2" justify="end">
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Accept</DialogClose>
</Flex>
<Button variant="secondary" slot="close">Cancel</Button>
<Button variant="primary" slot="close">Accept</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
@@ -130,10 +150,8 @@ export const dialogWithFormSnippet = `<DialogTrigger>
</Flex>
</DialogBody>
<DialogFooter>
<Flex gap="2" justify="end">
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Create User</DialogClose>
</Flex>
<Button variant="secondary" slot="close">Cancel</Button>
<Button variant="primary" slot="close">Create User</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
@@ -146,9 +164,9 @@ export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(
Your content
</DialogBody>
<DialogFooter>
<Flex gap="2" justify="end">
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Create User</DialogClose>
</Flex>
<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>`;
+6 -15
View File
@@ -443,8 +443,8 @@ export const componentDefinitions: {
};
readonly Dialog: {
readonly classNames: {
readonly root: 'bui-Dialog';
readonly content: 'bui-DialogContent';
readonly overlay: 'bui-DialogOverlay';
readonly dialog: 'bui-Dialog';
readonly header: 'bui-DialogHeader';
readonly headerTitle: 'bui-DialogHeaderTitle';
readonly body: 'bui-DialogBody';
@@ -729,19 +729,6 @@ export interface DialogBodyProps {
children?: React.ReactNode;
// (undocumented)
className?: string;
// (undocumented)
height?: number | string;
}
// @public (undocumented)
export const DialogClose: ForwardRefExoticComponent<
DialogCloseProps & RefAttributes<HTMLButtonElement>
>;
// @public
export interface DialogCloseProps extends Omit<ButtonProps, 'slot'> {
// (undocumented)
variant?: 'primary' | 'secondary' | 'tertiary';
}
// @public (undocumented)
@@ -772,6 +759,10 @@ export interface DialogProps extends ModalOverlayProps {
children?: React.ReactNode;
// (undocumented)
className?: string;
// (undocumented)
height?: number | string;
// (undocumented)
width?: number | string;
}
// @public (undocumented)
@@ -20,7 +20,6 @@ import {
DialogHeader,
DialogBody,
DialogFooter,
DialogClose,
} from './Dialog';
import { Button, Flex, Text, TextField, Select } from '@backstage/ui';
import { useArgs } from 'storybook/preview-api';
@@ -44,16 +43,20 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: args => {
return (
<DialogTrigger {...args}>
<DialogTrigger>
<Button variant="secondary">Open Dialog</Button>
<Dialog>
<Dialog {...args}>
<DialogHeader>Example Dialog</DialogHeader>
<DialogBody>
<Text>This is a basic dialog example.</Text>
</DialogBody>
<DialogFooter>
<DialogClose>Close</DialogClose>
<DialogClose variant="primary">Save</DialogClose>
<Button variant="secondary" slot="close">
Close
</Button>
<Button variant="primary" slot="close">
Save
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
@@ -87,52 +90,89 @@ export const NoTrigger: Story = {
<Text>This is a basic dialog example.</Text>
</DialogBody>
<DialogFooter>
<DialogClose>Close</DialogClose>
<DialogClose variant="primary">Save</DialogClose>
<Button variant="secondary" slot="close">
Close
</Button>
<Button variant="primary" slot="close">
Save
</Button>
</DialogFooter>
</Dialog>
);
},
};
export const Scrollable: Story = {
export const FixedWidth: Story = {
args: {
isOpen: true,
defaultOpen: true,
width: 600,
},
render: args => (
<DialogTrigger {...args}>
<Button variant="secondary">Scrollable Dialog</Button>
<Dialog>
<DialogTrigger>
<Button variant="secondary">Open Dialog</Button>
<Dialog {...args}>
<DialogHeader>Long Content Dialog</DialogHeader>
<DialogBody height={200}>
<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>
<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>
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Accept</DialogClose>
<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,
@@ -149,8 +189,12 @@ export const Confirmation: Story = {
</Text>
</DialogBody>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Delete</DialogClose>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary" slot="close">
Delete
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
@@ -181,26 +225,30 @@ export const WithForm: Story = {
</Flex>
</DialogBody>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogClose variant="primary">Create User</DialogClose>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary" slot="close">
Create User
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
),
};
export const PreviewScrollable: Story = {
export const PreviewFixedWidthAndHeight: Story = {
args: {
...Scrollable.args,
isOpen: false,
defaultOpen: undefined,
width: 600,
height: 400,
},
render: Scrollable.render,
render: FixedWidth.render,
};
export const PreviewWithForm: Story = {
args: {
...WithForm.args,
isOpen: false,
defaultOpen: undefined,
},
render: WithForm.render,
};
@@ -1,5 +1,5 @@
/* Backdrop */
.bui-Dialog {
.bui-DialogOverlay {
position: fixed;
top: 0;
left: 0;
@@ -16,34 +16,36 @@
background: rgba(0, 0, 0, 0.5);
}
.bui-Dialog[data-entering] {
.bui-DialogOverlay[data-entering] {
animation: fade-in 200ms ease-out forwards;
}
.bui-Dialog[data-exiting] {
.bui-DialogOverlay[data-exiting] {
animation: fade-out 150ms ease-out forwards;
}
.bui-DialogContent {
.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: 24rem;
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-Dialog[data-entering] .bui-DialogContent {
.bui-DialogOverlay[data-entering] .bui-Dialog {
animation: dialog-enter 150ms ease-out forwards;
}
/* Dialog exiting animation */
.bui-Dialog[data-exiting] .bui-DialogContent {
.bui-DialogOverlay[data-exiting] .bui-Dialog {
animation: dialog-exit 150ms ease-out forwards;
}
@@ -74,6 +76,8 @@
.bui-DialogBody {
padding: var(--bui-space-3);
flex: 1;
overflow-y: auto;
}
/* Keyframe animations */
+39 -60
View File
@@ -27,11 +27,9 @@ import type {
DialogHeaderProps,
DialogProps,
DialogBodyProps,
DialogCloseProps,
} from './types';
import './Dialog.styles.css';
import { RiCloseLine } from '@remixicon/react';
import { ScrollArea } from '../ScrollArea';
import { Button } from '../Button';
import { useStyles } from '../../hooks/useStyles';
import { Flex } from '../Flex';
@@ -41,6 +39,41 @@ 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'>,
@@ -63,27 +96,13 @@ DialogHeader.displayName = 'DialogHeader';
/** @public */
export const DialogBody = forwardRef<React.ElementRef<'div'>, DialogBodyProps>(
({ className, children, height, ...props }, ref) => {
({ className, children, ...props }, ref) => {
const { classNames } = useStyles('Dialog');
return (
<ScrollArea.Root
className={clsx(classNames.body, className)}
style={{
height: height
? typeof height === 'number'
? `${height}px`
: height
: undefined,
}}
ref={ref}
{...props}
>
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical">
<ScrollArea.Thumb />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
<div className={clsx(classNames.body, className)} ref={ref} {...props}>
{children}
</div>
);
},
);
@@ -104,43 +123,3 @@ export const DialogFooter = forwardRef<
);
});
DialogFooter.displayName = 'DialogFooter';
/** @public */
export const DialogClose = forwardRef<
React.ElementRef<typeof Button>,
DialogCloseProps
>(({ className, variant = 'secondary', children, ...props }, ref) => {
return (
<Button
ref={ref}
slot="close"
variant={variant}
className={className}
{...props}
>
{children}
</Button>
);
});
DialogClose.displayName = 'DialogClose';
/** @public */
export const Dialog = forwardRef<React.ElementRef<typeof Modal>, DialogProps>(
({ className, children, ...props }, ref) => {
const { classNames } = useStyles('Dialog');
return (
<Modal
ref={ref}
className={clsx(classNames.root, className)}
isDismissable
isKeyboardDismissDisabled={false}
{...props}
>
<RADialog className="bui-DialogContent">{children}</RADialog>
</Modal>
);
},
);
Dialog.displayName = 'Dialog';
+11 -19
View File
@@ -19,7 +19,6 @@ import type {
ModalOverlayProps as RAModalProps,
HeadingProps as RAHeadingProps,
} from 'react-aria-components';
import type { ButtonProps } from '../Button';
/**
* Props for the DialogTrigger component.
@@ -27,6 +26,17 @@ import type { ButtonProps } from '../Button';
*/
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
@@ -43,22 +53,4 @@ export interface DialogHeaderProps extends RAHeadingProps {
export interface DialogBodyProps {
children?: React.ReactNode;
className?: string;
height?: number | string;
}
/**
* Props for the DialogClose component.
* @public
*/
export interface DialogCloseProps extends Omit<ButtonProps, 'slot'> {
variant?: 'primary' | 'secondary' | 'tertiary';
}
/**
* Props for the Dialog component.
* @public
*/
export interface DialogProps extends RAModalProps {
className?: string;
children?: React.ReactNode;
}
@@ -87,8 +87,8 @@ export const componentDefinitions = {
},
Dialog: {
classNames: {
root: 'bui-Dialog',
content: 'bui-DialogContent',
overlay: 'bui-DialogOverlay',
dialog: 'bui-Dialog',
header: 'bui-DialogHeader',
headerTitle: 'bui-DialogHeaderTitle',
body: 'bui-DialogBody',