From f81b00fa7550495006335021e3c9fc1d712c902e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Sj=C3=B6blad?= Date: Thu, 9 Oct 2025 11:46:13 +0200 Subject: [PATCH] add dialog component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sofia Sjöblad --- .../src/components/Dialog/Dialog.stories.tsx | 96 ++++++++++++++ .../src/components/Dialog/Dialog.styles.css | 108 +++++++++++++++ packages/ui/src/components/Dialog/Dialog.tsx | 124 ++++++++++++++++++ packages/ui/src/components/Dialog/index.ts | 17 +++ packages/ui/src/components/Dialog/types.ts | 64 +++++++++ packages/ui/src/index.ts | 1 + 6 files changed, 410 insertions(+) create mode 100644 packages/ui/src/components/Dialog/Dialog.stories.tsx create mode 100644 packages/ui/src/components/Dialog/Dialog.styles.css create mode 100644 packages/ui/src/components/Dialog/Dialog.tsx create mode 100644 packages/ui/src/components/Dialog/index.ts create mode 100644 packages/ui/src/components/Dialog/types.ts diff --git a/packages/ui/src/components/Dialog/Dialog.stories.tsx b/packages/ui/src/components/Dialog/Dialog.stories.tsx new file mode 100644 index 0000000000..47b62330f7 --- /dev/null +++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( + <> + Hello + + Hello world + + + + Close + console.log('Save')}> + Save + + + + + ), + }, + render: args => ( + + + + + ), +}; + +export const CustomBodyMaxHeight: Story = { + args: { + children: ( + <> + Hello + + {Array.from({ length: 40 }).map((_, index) => ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, + quos. + + ))} + + + + Close + console.log('Save')}> + Save + + + + + ), + }, + render: args => ( + + + + + ), +}; diff --git a/packages/ui/src/components/Dialog/Dialog.styles.css b/packages/ui/src/components/Dialog/Dialog.styles.css new file mode 100644 index 0000000000..8e445a42a8 --- /dev/null +++ b/packages/ui/src/components/Dialog/Dialog.styles.css @@ -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); + } +} \ No newline at end of file diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx new file mode 100644 index 0000000000..2f8bd27c27 --- /dev/null +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -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 ; +}; + +/** @public */ +export const DialogHeader = forwardRef< + React.ElementRef<'div'>, + DialogHeaderProps +>(({ className, children, ...props }, ref) => ( +
+ + {children} + + +
+)); +DialogHeader.displayName = 'DialogHeader'; + +/** @public */ +export const DialogBody = forwardRef, DialogBodyProps>( + ({ className, children, height, ...props }, ref) => ( + + {children} + + + + + ), +); +DialogBody.displayName = 'DialogBody'; + +/** @public */ +export const DialogFooter = forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ className, children, ...props }, ref) => ( +
+ {children} +
+)); +DialogFooter.displayName = 'DialogFooter'; + +/** @public */ +export const DialogClose = forwardRef< + React.ElementRef, + DialogCloseProps +>(({ className, variant = 'secondary', children, ...props }, ref) => ( + +)); +DialogClose.displayName = 'DialogClose'; + +/** @public */ +export const Dialog = forwardRef, DialogProps>( + ({ className, children, ...props }, ref) => ( + + {children} + + ), +); +Dialog.displayName = 'Dialog'; diff --git a/packages/ui/src/components/Dialog/index.ts b/packages/ui/src/components/Dialog/index.ts new file mode 100644 index 0000000000..8db1b29ff8 --- /dev/null +++ b/packages/ui/src/components/Dialog/index.ts @@ -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'; diff --git a/packages/ui/src/components/Dialog/types.ts b/packages/ui/src/components/Dialog/types.ts new file mode 100644 index 0000000000..b13f496d58 --- /dev/null +++ b/packages/ui/src/components/Dialog/types.ts @@ -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 { + variant?: 'primary' | 'secondary' | 'tertiary'; +} + +/** + * Props for the Dialog component. + * @public + */ +export interface DialogProps extends RAModalProps { + className?: string; + children?: React.ReactNode; +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index f5d9230d4d..efad69605b 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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';