add dialog component
Signed-off-by: Sofia Sjöblad <ssjoblad@spotify.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user