add dialog component

Signed-off-by: Sofia Sjöblad <ssjoblad@spotify.com>
This commit is contained in:
Sofia Sjöblad
2025-10-09 11:46:13 +02:00
parent 2fbc5c4cb3
commit f81b00fa75
6 changed files with 410 additions and 0 deletions
@@ -0,0 +1,96 @@
/*
* 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,
DialogClose,
} from './Dialog';
import { Button, Flex, Text } from '@backstage/ui';
const meta = {
title: 'Components/Dialog',
component: Dialog,
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: (
<>
<DialogHeader>Hello</DialogHeader>
<DialogBody>
<Text>Hello world</Text>
</DialogBody>
<DialogFooter>
<Flex gap="2" align="center" justify="end">
<DialogClose>Close</DialogClose>
<DialogClose variant="primary" onPress={() => console.log('Save')}>
Save
</DialogClose>
</Flex>
</DialogFooter>
</>
),
},
render: args => (
<DialogTrigger>
<Button size="small" variant="secondary">
Open Dialog
</Button>
<Dialog {...args} />
</DialogTrigger>
),
};
export const CustomBodyMaxHeight: Story = {
args: {
children: (
<>
<DialogHeader> Hello</DialogHeader>
<DialogBody height={400}>
{Array.from({ length: 40 }).map((_, index) => (
<Text key={index}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
quos.
</Text>
))}
</DialogBody>
<DialogFooter>
<Flex gap="2" align="center" justify="end">
<DialogClose>Close</DialogClose>
<DialogClose variant="primary" onPress={() => console.log('Save')}>
Save
</DialogClose>
</Flex>
</DialogFooter>
</>
),
},
render: args => (
<DialogTrigger>
<Button size="small" variant="secondary">
Open Dialog
</Button>
<Dialog {...args} />
</DialogTrigger>
),
};
@@ -0,0 +1,108 @@
/* Backdrop */
.bui-Dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.bui-Dialog[data-entering] {
animation: fade-in 200ms ease-out forwards;
}
.bui-Dialog[data-exiting] {
animation: fade-out 150ms ease-out forwards;
}
.bui-DialogContent {
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;
max-width: calc(100vw - 3rem);
display: flex;
flex-direction: column;
}
/* Dialog entering animation */
.bui-Dialog[data-entering] .bui-DialogContent {
animation: dialog-enter 150ms ease-out forwards;
}
/* Dialog exiting animation */
.bui-Dialog[data-exiting] .bui-DialogContent {
animation: dialog-exit 150ms ease-out forwards;
}
.bui-DialogHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--bui-space-3);
border-bottom: 1px solid var(--bui-border);
}
.bui-Dialog [slot="title"] {
font-size: var(--bui-font-size-3);
}
.bui-DialogFooter {
display: flex;
align-items: center;
justify-content: end;
gap: var(--bui-space-2);
padding: var(--bui-space-3);
border-top: 1px solid var(--bui-border);
}
.bui-DialogBody {
padding: var(--bui-space-3);
}
/* 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,124 @@
/*
* 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,
DialogCloseProps,
} from './types';
import './Dialog.styles.css';
import { RiCloseLine } from '@remixicon/react';
import { ScrollArea } from '../ScrollArea';
import { Button } from '../Button';
/** @public */
export const DialogTrigger = (props: DialogTriggerProps) => {
return <RADialogTrigger {...props} />;
};
/** @public */
export const DialogHeader = forwardRef<
React.ElementRef<'div'>,
DialogHeaderProps
>(({ className, children, ...props }, ref) => (
<div ref={ref} className={clsx('bui-DialogHeader', className)} {...props}>
<Heading slot="title" className="bui-DialogHeaderTitle">
{children}
</Heading>
<Button variant="tertiary" className="bui-DialogHeaderClose" slot="close">
<RiCloseLine />
</Button>
</div>
));
DialogHeader.displayName = 'DialogHeader';
/** @public */
export const DialogBody = forwardRef<React.ElementRef<'div'>, DialogBodyProps>(
({ className, children, height, ...props }, ref) => (
<ScrollArea.Root
className={clsx('bui-DialogBody', 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>
),
);
DialogBody.displayName = 'DialogBody';
/** @public */
export const DialogFooter = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ className, children, ...props }, ref) => (
<div ref={ref} className={clsx('bui-DialogFooter', className)} {...props}>
{children}
</div>
));
DialogFooter.displayName = 'DialogFooter';
/** @public */
export const DialogClose = forwardRef<
React.ElementRef<typeof Button>,
DialogCloseProps
>(({ className, variant = 'secondary', children, ...props }, ref) => (
<Button
ref={ref}
slot="close"
variant={variant}
className={clsx('bui-DialogClose', className)}
{...props}
>
{children}
</Button>
));
DialogClose.displayName = 'DialogClose';
/** @public */
export const Dialog = forwardRef<React.ElementRef<typeof Modal>, DialogProps>(
({ className, children, ...props }, ref) => (
<Modal
ref={ref}
className={clsx('bui-Dialog', className)}
isDismissable
isKeyboardDismissDisabled={false}
{...props}
>
<RADialog className="bui-DialogContent">{children}</RADialog>
</Modal>
),
);
Dialog.displayName = 'Dialog';
@@ -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,64 @@
/*
* 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';
import type { ButtonProps } from '../Button';
/**
* Props for the DialogTrigger component.
* @public
*/
export interface DialogTriggerProps extends RADialogTriggerProps {}
/**
* 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;
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;
}
+1
View File
@@ -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';