diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.css b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.css new file mode 100644 index 00000000000..fade052ace1 --- /dev/null +++ b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.css @@ -0,0 +1,213 @@ +@import './_base.css'; + +@layer base { + .dbv-kit-dialog__backdrop { + background-color: transparent; + opacity: 0; + transition-property: opacity, background-color; + transition-timing-function: ease-in-out; + transition-duration: 100ms; + + &[data-enter] { + opacity: 1; + background-color: var(--dbv-kit-dialog-backdrop-background); + } + + &:not([data-enter]) { + opacity: 0; + } + + &[data-animated='false'] { + transition: none !important; + opacity: 1; + background-color: var(--dbv-kit-dialog-backdrop-background); + } + + @media (prefers-reduced-motion: reduce) { + transition: none !important; + opacity: 1; + background-color: var(--dbv-kit-dialog-backdrop-background); + } + } + + .dbv-kit-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + display: flex; + flex-direction: column; + padding: 0; + background-color: var(--dbv-kit-dialog-content-background); + color: var(--dbv-kit-dialog-content-foreground); + border: none; + border-radius: var(--dbv-kit-dialog-content-border-radius); + box-shadow: var(--dbv-kit-dialog-content-shadow); + outline: 0; + overflow: hidden; + margin: 0; + max-width: var(--dbv-kit-dialog-large-width); + + @media (prefers-reduced-motion: no-preference) { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + transition-property: opacity, transform; + transition-timing-function: ease-in-out; + transition-duration: 150ms; + + &:not([data-enter]) { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + &[data-enter] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + + &[data-animated='false'] { + transition: none !important; + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + } + + @media (prefers-reduced-motion: reduce) { + transition: none !important; + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + + &[data-size='small'] { + min-width: var(--dbv-kit-dialog-small-width); + min-height: var(--dbv-kit-dialog-small-height); + max-height: max(var(--app-height, 100vh) - 48px, var(--dbv-kit-dialog-small-height)); + } + + &[data-size='medium'] { + min-width: var(--dbv-kit-dialog-medium-width); + min-height: var(--dbv-kit-dialog-medium-height); + max-height: max(var(--app-height, 100vh) - 48px, var(--dbv-kit-dialog-medium-height)); + } + + &[data-size='large'] { + min-width: var(--dbv-kit-dialog-large-width); + min-height: var(--dbv-kit-dialog-large-height); + max-height: max(var(--app-height, 100vh) - 48px, var(--dbv-kit-dialog-large-height)); + } + + &[data-size='free'] { + min-width: auto; + min-height: auto; + max-width: calc(100vw - 48px); + max-height: calc(100vh - 48px); + } + + /* Slide variant (side panel from right) */ + &[data-variant='slide'] { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: translateX(100%); + width: 100%; + max-width: none; + height: 100%; + max-height: 100%; + min-width: 0; + min-height: 0; + border-radius: 0; + + @media (prefers-reduced-motion: no-preference) { + opacity: 1; + transition-property: transform; + transition-timing-function: var(--dbv-kit-dialog-slide-transition-timing); + transition-duration: var(--dbv-kit-dialog-slide-transition-duration); + + &[data-enter] { + transform: translateX(0); + } + + &:not([data-enter]) { + transform: translateX(100%); + } + + &[data-animated='false'] { + transition: none !important; + transform: translateX(0); + } + } + + @media (prefers-reduced-motion: reduce) { + transition: none !important; + opacity: 1; + transform: translateX(0); + } + } + } + + .dbv-kit-dialog__header { + flex-shrink: 0; + padding: var(--dbv-kit-dialog-header-padding); + border-bottom: var(--dbv-kit-dialog-header-border-bottom); + } + + .dbv-kit-dialog__body { + flex: 1 1 auto; + overflow-y: auto; + padding: var(--dbv-kit-dialog-body-padding); + } + + .dbv-kit-dialog__footer { + flex-shrink: 0; + padding: var(--dbv-kit-dialog-footer-padding); + border-top: var(--dbv-kit-dialog-footer-border-top); + display: flex; + gap: var(--dbv-kit-dialog-footer-gap); + justify-content: flex-end; + } + + .dbv-kit-dialog__disclosure { + background-color: var(--dbv-kit-dialog-disclosure-background); + color: var(--dbv-kit-dialog-disclosure-foreground); + } + + .dbv-kit-dialog__heading { + margin: var(--dbv-kit-dialog-heading-margin); + font-size: var(--dbv-kit-dialog-heading-font-size); + font-weight: var(--dbv-kit-dialog-heading-font-weight); + line-height: var(--dbv-kit-dialog-heading-line-height); + color: var(--dbv-kit-dialog-heading-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .dbv-kit-dialog__description { + margin: var(--dbv-kit-dialog-description-margin); + font-size: var(--dbv-kit-dialog-description-font-size); + color: var(--dbv-kit-dialog-description-color); + } + + .dbv-kit-dialog__dismiss { + background-color: var(--dbv-kit-dialog-dismiss-background); + color: var(--dbv-kit-dialog-dismiss-foreground); + padding: 0.5rem 1rem; + border-radius: 0.375rem; + border: none; + cursor: pointer; + font-weight: 500; + font-size: 0.875rem; + transition: background-color 150ms var(--tw-ease-in-out); + + &:hover:not([aria-disabled='true']) { + background-color: var(--dbv-kit-dialog-dismiss-hover-background); + } + + &[aria-disabled='true'] { + opacity: var(--dbv-kit-control-disabled-opacity, 0.5); + cursor: not-allowed; + } + } +} diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.tsx b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.tsx new file mode 100644 index 00000000000..b8e4e66ddb3 --- /dev/null +++ b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/Dialog.tsx @@ -0,0 +1,92 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + Dialog as AriakitDialog, + DialogDescription as AriakitDialogDescription, + DialogDisclosure as AriakitDialogDisclosure, + DialogDismiss as AriakitDialogDismiss, + DialogHeading as AriakitDialogHeading, + DialogProvider, + type DialogDescriptionProps, + type DialogDismissProps, + type DialogDisclosureProps, + type DialogHeadingProps, + type DialogProps, + type DialogProviderProps, + type DialogStore, + type DialogStoreProps, + type DialogStoreState, + useDialogContext, + useDialogStore, +} from '@ariakit/react'; +import clsx from 'clsx'; +import type { ComponentPropsWithoutRef, JSX } from 'react'; + +import './Dialog.css'; + +interface ExtendedDialogProps extends DialogProps { + animated?: boolean; +} + +function Dialog({ className, backdrop, animated = true, ...props }: ExtendedDialogProps): JSX.Element { + const backdropElement = backdrop === true ?
: backdrop; + + return ; +} + +function DialogDisclosure({ className, ...props }: DialogDisclosureProps): JSX.Element { + return ; +} + +function DialogHeader({ className, ...props }: ComponentPropsWithoutRef<'header'>): JSX.Element { + return
; +} + +function DialogBody({ className, ...props }: ComponentPropsWithoutRef<'div'>): JSX.Element { + return
; +} + +function DialogFooter({ className, ...props }: ComponentPropsWithoutRef<'footer'>): JSX.Element { + return
; +} + +function DialogHeading({ className, ...props }: DialogHeadingProps): JSX.Element { + return ; +} + +function DialogDescription({ className, ...props }: DialogDescriptionProps): JSX.Element { + return ; +} + +function DialogDismiss({ className, ...props }: DialogDismissProps): JSX.Element { + return ; +} + +export { + Dialog, + DialogDisclosure, + DialogHeader, + DialogBody, + DialogFooter, + DialogHeading, + DialogDescription, + DialogDismiss, + DialogProvider, + useDialogStore, + useDialogContext, + type ExtendedDialogProps as DialogProps, + type DialogProviderProps, + type DialogDisclosureProps, + type DialogHeadingProps, + type DialogDescriptionProps, + type DialogDismissProps, + type DialogStore, + type DialogStoreProps, + type DialogStoreState, +}; diff --git a/webapp/common-react/@dbeaver/ui-kit/src/Dialog/_base.css b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/_base.css new file mode 100644 index 00000000000..657cc5053a7 --- /dev/null +++ b/webapp/common-react/@dbeaver/ui-kit/src/Dialog/_base.css @@ -0,0 +1,66 @@ +@layer base { + :root { + /* Dialog container */ + --dbv-kit-dialog-content-background: #ffffff; + --dbv-kit-dialog-content-foreground: #353535; + --dbv-kit-dialog-content-border-radius: 0.25rem; + --dbv-kit-dialog-content-shadow: 0 6px 6px -3px #0003, 0 10px 14px 1px #00000024, 0 4px 18px 3px #0000001f; + + /* Dialog layout */ + --dbv-kit-dialog-header-padding: 1rem 1.5rem; + --dbv-kit-dialog-header-border-bottom: none; + --dbv-kit-dialog-body-padding: 1.5rem; + --dbv-kit-dialog-footer-padding: 1rem 1.5rem; + --dbv-kit-dialog-footer-border-top: none; + --dbv-kit-dialog-footer-gap: 0.75rem; + + /* Dialog sizes */ + --dbv-kit-dialog-small-width: 404px; + --dbv-kit-dialog-small-height: 262px; + --dbv-kit-dialog-medium-width: 576px; + --dbv-kit-dialog-medium-height: 374px; + --dbv-kit-dialog-large-width: 720px; + --dbv-kit-dialog-large-height: 468px; + --dbv-kit-dialog-max-width: 720px; + + /* Slide variant (side panel) */ + --dbv-kit-dialog-slide-transition-duration: 300ms; + --dbv-kit-dialog-slide-transition-timing: ease-in-out; + + /* Backdrop */ + --dbv-kit-dialog-backdrop-background: #0000007a; + + /* Heading */ + --dbv-kit-dialog-heading-margin: 0; + --dbv-kit-dialog-heading-font-size: 1.25rem; + --dbv-kit-dialog-heading-font-weight: 400; + --dbv-kit-dialog-heading-line-height: 2rem; + --dbv-kit-dialog-heading-color: var(--dbv-kit-dialog-content-foreground); + + /* Description */ + --dbv-kit-dialog-description-margin: 0; + --dbv-kit-dialog-description-font-size: 0.875rem; + --dbv-kit-dialog-description-color: inherit; + + /* Disclosure */ + --dbv-kit-dialog-disclosure-background: transparent; + --dbv-kit-dialog-disclosure-foreground: inherit; + + /* Dismiss */ + --dbv-kit-dialog-dismiss-background: #f2f2f2; + --dbv-kit-dialog-dismiss-foreground: #353535; + --dbv-kit-dialog-dismiss-hover-background: #e5e5e5; + } + + @media (prefers-color-scheme: dark) { + :root { + --dbv-kit-dialog-content-background: hsl(240deg, 10%, 16%); + --dbv-kit-dialog-content-foreground: #ffffff; + --dbv-kit-dialog-header-border-bottom: none; + --dbv-kit-dialog-footer-border-top: none; + --dbv-kit-dialog-dismiss-background: hsl(240deg, 10%, 18%); + --dbv-kit-dialog-dismiss-foreground: #cbcbcb; + --dbv-kit-dialog-dismiss-hover-background: hsl(240deg, 10%, 22%); + } + } +} diff --git a/webapp/common-react/@dbeaver/ui-kit/src/index.ts b/webapp/common-react/@dbeaver/ui-kit/src/index.ts index 75716f452c2..2ac792e490c 100644 --- a/webapp/common-react/@dbeaver/ui-kit/src/index.ts +++ b/webapp/common-react/@dbeaver/ui-kit/src/index.ts @@ -48,3 +48,4 @@ export * from './utils/clsx.js'; export * from './ComponentProvider.js'; export * from './Menu/Menu.js'; export * from './Disclosure/Disclosure.js'; +export * from './Dialog/Dialog.js'; diff --git a/webapp/packages/core-app/src/AppScreen/RightArea.tsx b/webapp/packages/core-app/src/AppScreen/RightArea.tsx index e70e1f29de4..9d9d3e9194e 100644 --- a/webapp/packages/core-app/src/AppScreen/RightArea.tsx +++ b/webapp/packages/core-app/src/AppScreen/RightArea.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import { Split, useS, useSplitUserState, + SlidePanel, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; @@ -46,7 +47,7 @@ export const RightArea = observer(function RightArea({ className }) { return ( - + @@ -60,13 +61,11 @@ export const RightArea = observer(function RightArea({ className }) { - - - - - - + + + + ); }); diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.module.css b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.module.css index 19f3e2bccf7..21523926864 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.module.css @@ -6,23 +6,18 @@ * you may not use this file except in compliance with the License. */ .body { - flex: 1; box-sizing: content-box; display: flex; max-height: 100%; overflow: auto; + padding: 24px; padding-top: 0px; padding-right: 0px; - flex-shrink: 0; - padding: 24px; &.noBodyPadding { padding: 0px; } - padding-top: 0px; - padding-right: 0px; - &.noBodyPadding + footer { padding-top: 24px; } diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.tsx b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.tsx index fe15e846230..671fa44eecd 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogBody.tsx @@ -1,12 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; +import { DialogBody } from '@dbeaver/ui-kit'; import { s } from '../../s.js'; import { useS } from '../../useS.js'; import styles from './CommonDialogBody.module.css'; @@ -22,11 +23,11 @@ export const CommonDialogBody = observer(function CommonDialogBody({ noBo const computedStyles = useS(styles); return ( -
+
{children}
{!noOverflow &&
}
-
+
); }); diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.module.css b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.module.css index db3f95b8cf5..b2e6f9ba60c 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.module.css @@ -6,8 +6,7 @@ * you may not use this file except in compliance with the License. */ .footer { - flex-shrink: 0; - padding: 0 24px 24px 24px; + padding: 0 24px 24px; display: flex; z-index: 0; box-sizing: border-box; diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.tsx b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.tsx index 16dadafa9c3..0a0765838ba 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogFooter.tsx @@ -1,12 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; +import { DialogFooter } from '@dbeaver/ui-kit'; import { s } from '../../s.js'; import { useS } from '../../useS.js'; import styles from './CommonDialogFooter.module.css'; @@ -19,5 +20,5 @@ interface Props { export const CommonDialogFooter = observer(function CommonDialogFooter({ children, className }) { const computedStyles = useS(styles); - return
{children}
; + return {children}; }); diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css index de47bb038aa..4b0ae89e7e9 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.module.css @@ -9,12 +9,11 @@ position: relative; display: grid; grid-template-columns: max-content 1fr; - flex-shrink: 0; - padding: 24px; - padding-right: 20px; + padding: 24px !important; + padding-right: 20px !important; &.noPadding { - padding: 0px; + padding: 0px !important; } } diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx index d0257130632..e513e7f3ffd 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogHeader.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { IconButton } from '@dbeaver/ui-kit'; +import { DialogHeader, IconButton } from '@dbeaver/ui-kit'; import { Icon } from '../../Icon.js'; import { IconOrImage } from '../../IconOrImage.js'; import { useTranslate } from '../../localization/useTranslate.js'; @@ -40,7 +40,7 @@ export const CommonDialogHeader = observer(function CommonDialogHeader({ const computedStyles = useS(styles); return ( -
+
{icon && }
@@ -53,6 +53,6 @@ export const CommonDialogHeader = observer(function CommonDialogHeader({ )}
{subTitle &&
{typeof subTitle === 'string' ? translate(subTitle) : subTitle}
} -
+ ); }); diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.module.css b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.module.css index 2a3e392b803..71c7abd8f41 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.module.css @@ -5,69 +5,47 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -.container { - box-sizing: border-box; - display: flex; - outline: none; -} - .dialog { composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global; - border-radius: 0.25rem; - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; - margin: 0; - border: none; - height: auto; - max-height: 100%; - max-width: 748px; - padding: 0px; - outline: none; - &.small { + &.fixedSize[data-size='small'] { + width: 404px; + height: 262px; min-width: 404px; min-height: 262px; - max-height: max(var(--app-height, 100vh) - 48px, 262px); + } - &.fixedSize { - width: 404px; - height: 262px; - } - &.fixedWidth { - width: 404px; - } + &.fixedWidth[data-size='small'] { + width: 404px; + min-width: 404px; } - &.medium { + + &.fixedSize[data-size='medium'] { + width: 576px; + height: 374px; min-width: 576px; min-height: 374px; - max-height: max(var(--app-height, 100vh) - 48px, 374px); + } - &.fixedSize { - width: 576px; - height: 374px; - } - &.fixedWidth { - width: 576px; - } + &.fixedWidth[data-size='medium'] { + width: 576px; + min-width: 576px; } - &.large { + + &.fixedSize[data-size='large'] { + width: 720px; + height: 468px; min-width: 720px; min-height: 468px; - max-height: max(var(--app-height, 100vh) - 48px, 468px); + } - &.fixedSize { - width: 720px; - height: 468px; - } - &.fixedWidth { - width: 720px; - } + &.fixedWidth[data-size='large'] { + width: 720px; + min-width: 720px; } &.freeHeight { - min-height: unset; + min-height: unset !important; } } diff --git a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx index a5251b27f14..5d94bdeb7f1 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx @@ -1,17 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { forwardRef, useContext, useEffect } from 'react'; -import { Dialog, useDialogState } from 'reakit'; +import { forwardRef, useContext } from 'react'; +import { Dialog } from '@dbeaver/ui-kit'; import { Loader } from '../../Loader/Loader.js'; import { s } from '../../s.js'; -import { useFocus } from '../../useFocus.js'; import { useS } from '../../useS.js'; import { DialogContext } from '../DialogContext.js'; import styles from './CommonDialogWrapper.module.css'; @@ -24,50 +23,53 @@ export interface CommonDialogWrapperProps { fixedSize?: boolean; fixedWidth?: boolean; freeHeight?: boolean; - autofocus?: boolean; className?: string; children?: React.ReactNode; + autoFocusOnHide?: boolean | ((element: HTMLElement | null) => boolean) | undefined; + autoFocusOnShow?: boolean | ((element: HTMLElement | null) => boolean) | undefined; + initialFocus?: HTMLElement | React.RefObject | null | undefined; } export const CommonDialogWrapper = observer( forwardRef(function CommonDialogWrapper( - { size = 'medium', fixedSize, fixedWidth, freeHeight, autofocus = true, 'aria-label': ariaLabel, className, children }, + { + size = 'medium', + fixedSize, + fixedWidth, + freeHeight, + 'aria-label': ariaLabel, + autoFocusOnHide = true, + autoFocusOnShow = true, + className, + initialFocus, + children, + }, ref, ) { - const [focusedRef] = useFocus({ autofocus }); const computedStyles = useS(styles); const context = useContext(DialogContext); - const dialogState = useDialogState({ visible: true }); - useEffect(() => { - if (!dialogState.visible && !context.dialog.options?.persistent) { + function handleClose() { + if (!context.dialog.options?.persistent) { context.reject(); } - }); + } return ( | null | undefined} + onClose={handleClose} > - - - {children} - - + + {children} + ); }), diff --git a/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.module.css b/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.module.css index bb063318fd1..9f99726b352 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.module.css +++ b/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.module.css @@ -9,6 +9,11 @@ height: 100%; } +.error { + composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global; + border-radius: 0.25rem; +} + .backdrop { box-sizing: border-box; background-color: rgba(0, 0, 0, 0.48); @@ -35,8 +40,3 @@ display: none; } } - -.error { - composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global; - border-radius: 0.25rem; -} diff --git a/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.tsx b/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.tsx index 5268006709a..6b6c8aa1df3 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/DialogsPortal.tsx @@ -6,8 +6,7 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useLayoutEffect, useMemo, useRef } from 'react'; -import { DialogBackdrop } from 'reakit'; +import { useMemo } from 'react'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, type DialogInternal } from '@cloudbeaver/core-dialogs'; @@ -23,7 +22,6 @@ import style from './DialogsPortal.module.css'; export const DialogsPortal = observer(function DialogsPortal() { const styles = useS(style); const commonDialogService = useService(CommonDialogService); - const focusedElementRef = useRef(null); let activeDialog: DialogInternal | undefined; @@ -43,48 +41,19 @@ export const DialogsPortal = observer(function DialogsPortal() { commonDialogService.resolveDialog(this.dialog.promise, result); } }, - backdropClick(e: React.MouseEvent) { - if (e.target !== e.currentTarget) { - return; - } - - e.preventDefault(); // prevent focus loss - if (!this.dialog?.options?.persistent && e.currentTarget.isEqualNode(e.target as HTMLElement)) { - this.reject(); - } - }, }), { dialog: activeDialog, }, - ['reject', 'resolve', 'backdropClick'], + ['reject', 'resolve'], ); - useMemo(() => { - if (!activeDialog) { - return; - } - - // capture focused element before dialog open - if (document.activeElement instanceof HTMLElement) { - focusedElementRef.current = document.activeElement; - } - }, [activeDialog]); - - useLayoutEffect(() => { - if (!activeDialog) { - return; - } - - return () => { - // restore focus after dialog close - focusedElementRef.current?.focus(); - focusedElementRef.current = null; - }; - }, [activeDialog]); + if (!activeDialog) { + return null; + } return ( - +
{commonDialogService.dialogs.map((dialog, i, arr) => ( @@ -92,7 +61,7 @@ export const DialogsPortal = observer(function DialogsPortal() { ))}
- +
); }); diff --git a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx index eddf103901f..7071c43f876 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx @@ -7,7 +7,7 @@ */ import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; import { throttleAsync } from '@cloudbeaver/core-utils'; @@ -19,7 +19,6 @@ import { Form } from '../FormControls/Form.js'; import { InputField } from '../FormControls/InputField/InputField.js'; import { useTranslate } from '../localization/useTranslate.js'; import { s } from '../s.js'; -import { useFocus } from '../useFocus.js'; import { useObservableRef } from '../useObservableRef.js'; import { useS } from '../useS.js'; import { CommonDialogBody } from './CommonDialog/CommonDialogBody.js'; @@ -57,7 +56,7 @@ export const RenameDialog: DialogComponent = observ className, }) { const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const ref = useRef(null); const styles = useS(style); const { icon, subTitle, bigIcon, viewBox, name, objectName, create, confirmActionText } = payload; @@ -103,12 +102,19 @@ export const RenameDialog: DialogComponent = observ const errorMessage = state.valid ? ' ' : translate(state.message ?? 'ui_rename_taken_or_invalid'); return ( - + -
resolveDialog(state.name)}> + resolveDialog(state.name)}> - state.validate().catch(() => {})}> + state.validate().catch(() => {})} + > {translate('ui_name')} diff --git a/webapp/packages/core-blocks/src/ErrorBoundary.tsx b/webapp/packages/core-blocks/src/ErrorBoundary.tsx index bfa60b9b784..26015fc4dd2 100644 --- a/webapp/packages/core-blocks/src/ErrorBoundary.tsx +++ b/webapp/packages/core-blocks/src/ErrorBoundary.tsx @@ -14,6 +14,7 @@ import { DisplayError } from './DisplayError.js'; import style from './ErrorBoundary.module.css'; import { ErrorContext, type IExceptionContext } from './ErrorContext.js'; import { ExceptionMessage } from './ExceptionMessage.js'; +import { Spinner } from '@dbeaver/ui-kit'; interface Props { simple?: boolean; @@ -144,7 +145,7 @@ export class ErrorBoundary extends React.Component - Loading...}>{children} + }>{children} ); } diff --git a/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx b/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx index 12ebbf72cc2..1cd14b82433 100644 --- a/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx +++ b/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx @@ -88,7 +88,7 @@ export const ExportImageDialog = observer )} - + diff --git a/webapp/packages/core-blocks/src/Slide/SlideBox.tsx b/webapp/packages/core-blocks/src/Slide/SlideBox.tsx index 3a0ee17bfcb..78b303e9b57 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideBox.tsx +++ b/webapp/packages/core-blocks/src/Slide/SlideBox.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/core-blocks/src/Slide/SlideElement.tsx b/webapp/packages/core-blocks/src/Slide/SlideElement.tsx index 0cd567e8696..1ed41d61a6b 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideElement.tsx +++ b/webapp/packages/core-blocks/src/Slide/SlideElement.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -15,10 +15,15 @@ interface Props { className?: string; children?: React.ReactNode; open?: boolean; + inert?: boolean; } -export const SlideElement = observer(function SlideElement({ children, className }) { +export const SlideElement = observer(function SlideElement({ children, className, inert }) { const styles = useS(style); - return
{children}
; + return ( +
+ {children} +
+ ); }); diff --git a/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx b/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx index 6719e2dab19..0bffdcee1c9 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx +++ b/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx @@ -1,13 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { Icon } from '../Icon.js'; import { s } from '../s.js'; import { useS } from '../useS.js'; import style from './SlideOverlay.module.css'; @@ -21,11 +20,5 @@ interface Props { export const SlideOverlay = observer(function SlideOverlay({ className, onClick }) { const styles = useS(style); - return ( -
-
- -
-
- ); + return
; }); diff --git a/webapp/packages/core-blocks/src/Slide/SlidePanel.module.css b/webapp/packages/core-blocks/src/Slide/SlidePanel.module.css new file mode 100644 index 00000000000..54c95eb2716 --- /dev/null +++ b/webapp/packages/core-blocks/src/Slide/SlidePanel.module.css @@ -0,0 +1,25 @@ +.iconBtn { + position: absolute; + left: 0; + top: 50%; + z-index: 1; + transform: translateY(-50%) translateX(-175%) rotate(90deg); + background-color: var(--theme-surface); + box-sizing: border-box; + width: 48px; + height: 48px; + padding: 15px; + border-radius: 50%; + color: var(--theme-on-surface); + overflow: hidden; + align-items: center; + + &:hover, + &:focus { + background-color: color-mix(in srgb, var(--theme-on-surface), var(--theme-surface) 90%); + } +} + +.loader { + height: 100%; +} diff --git a/webapp/packages/core-blocks/src/Slide/SlidePanel.tsx b/webapp/packages/core-blocks/src/Slide/SlidePanel.tsx new file mode 100644 index 00000000000..83dacace898 --- /dev/null +++ b/webapp/packages/core-blocks/src/Slide/SlidePanel.tsx @@ -0,0 +1,45 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { IconButton } from '@dbeaver/ui-kit'; +import { SlideElement, Icon, Loader, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +interface SlidePanelProps { + children?: React.ReactNode; + isOpen: boolean; + onClose: () => void; +} + +import style from './SlidePanel.module.css'; + +export const SLIDE_PANEL_CLOSE_BUTTON_ID = 'slide-panel-close-button'; + +export function SlidePanel({ children, isOpen, onClose }: SlidePanelProps): React.ReactElement { + const styles = useS(style); + const t = useTranslate(); + + return ( + + {isOpen && ( + + + + )} + + {children} + + + ); +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 01abe042c44..63f5becea75 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -92,6 +92,7 @@ export * from './PropertiesTable/IProperty.js'; export * from './Slide/SlideBox.js'; export * from './Slide/SlideElement.js'; export * from './Slide/SlideOverlay.js'; +export * from './Slide/SlidePanel.js'; export * from './Split/SplitControls.js'; export * from './Split/Pane.js'; diff --git a/webapp/packages/core-blocks/src/locales/en.ts b/webapp/packages/core-blocks/src/locales/en.ts index df9d23e4549..fe98dd2c2c0 100644 --- a/webapp/packages/core-blocks/src/locales/en.ts +++ b/webapp/packages/core-blocks/src/locales/en.ts @@ -16,4 +16,5 @@ export default [ ['core_blocks_export_image_dialog_title', 'Export as Image'], ['core_blocks_export_image_dialog_format', 'File Format'], ['core_blocks_export_image_dialog_transparent_background', 'Transparent background'], + ['core_blocks_dialog_element_close_tooltip', 'Close panel'], ]; diff --git a/webapp/packages/core-blocks/src/locales/fr.ts b/webapp/packages/core-blocks/src/locales/fr.ts index 1a7a6111a52..84dd464f37e 100644 --- a/webapp/packages/core-blocks/src/locales/fr.ts +++ b/webapp/packages/core-blocks/src/locales/fr.ts @@ -15,4 +15,5 @@ export default [ ['core_blocks_export_image_dialog_title', "Exporter en tant qu'image"], ['core_blocks_export_image_dialog_format', 'Format de fichier'], ['core_blocks_export_image_dialog_transparent_background', 'Fond transparent'], + ['core_blocks_dialog_element_close_tooltip', 'Fermer le panneau'], ]; diff --git a/webapp/packages/core-blocks/src/locales/it.ts b/webapp/packages/core-blocks/src/locales/it.ts index 7f301950078..3bbca948354 100644 --- a/webapp/packages/core-blocks/src/locales/it.ts +++ b/webapp/packages/core-blocks/src/locales/it.ts @@ -10,4 +10,5 @@ export default [ ['core_blocks_export_image_dialog_title', 'Esporta come immagine'], ['core_blocks_export_image_dialog_format', 'Formato file'], ['core_blocks_export_image_dialog_transparent_background', 'Sfondo trasparente'], + ['core_blocks_dialog_element_close_tooltip', 'Chiudi pannello'], ]; diff --git a/webapp/packages/core-blocks/src/locales/ru.ts b/webapp/packages/core-blocks/src/locales/ru.ts index c2092da0ebf..e86ad99e8b9 100644 --- a/webapp/packages/core-blocks/src/locales/ru.ts +++ b/webapp/packages/core-blocks/src/locales/ru.ts @@ -13,4 +13,5 @@ export default [ ['core_blocks_export_image_dialog_title', 'Экспортировать как изображение'], ['core_blocks_export_image_dialog_format', 'Формат файла'], ['core_blocks_export_image_dialog_transparent_background', 'Прозрачный фон'], + ['core_blocks_dialog_element_close_tooltip', 'Закрыть панель'], ]; diff --git a/webapp/packages/core-blocks/src/locales/vi.ts b/webapp/packages/core-blocks/src/locales/vi.ts index 1a4da9bfe5e..aa6a8f995cb 100644 --- a/webapp/packages/core-blocks/src/locales/vi.ts +++ b/webapp/packages/core-blocks/src/locales/vi.ts @@ -16,4 +16,5 @@ export default [ ['core_blocks_export_image_dialog_title', 'Xuất dưới dạng hình ảnh'], ['core_blocks_export_image_dialog_format', 'Định dạng tệp'], ['core_blocks_export_image_dialog_transparent_background', 'Nền trong suốt'], + ['core_blocks_dialog_element_close_tooltip', 'Đóng bảng điều khiển'], ]; diff --git a/webapp/packages/core-blocks/src/locales/zh.ts b/webapp/packages/core-blocks/src/locales/zh.ts index 06524303baf..20b008fcbb5 100644 --- a/webapp/packages/core-blocks/src/locales/zh.ts +++ b/webapp/packages/core-blocks/src/locales/zh.ts @@ -16,4 +16,5 @@ export default [ ['core_blocks_export_image_dialog_title', '导出为图片'], ['core_blocks_export_image_dialog_format', '文件格式'], ['core_blocks_export_image_dialog_transparent_background', '透明背景'], + ['core_blocks_dialog_element_close_tooltip', '关闭面板'], ]; diff --git a/webapp/packages/plugin-administration/src/Administration/Administration.tsx b/webapp/packages/plugin-administration/src/Administration/Administration.tsx index 3cbfc3268c3..4c3701e6cb8 100644 --- a/webapp/packages/plugin-administration/src/Administration/Administration.tsx +++ b/webapp/packages/plugin-administration/src/Administration/Administration.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ import { type IAdministrationItemRoute, } from '@cloudbeaver/core-administration'; import { - Loader, s, SContext, SlideBox, @@ -26,6 +25,7 @@ import { ToolsPanelStyles, useAutoLoad, useS, + SlidePanel, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { OptionsPanelService, TabList, TabListStyles, TabsState, TabStyles } from '@cloudbeaver/core-ui'; @@ -140,19 +140,17 @@ export const Administration = observer>(function {children} - +
- -
- - -
- -
-
+ + +
+ +
+
diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 6ee2d1cc3d5..9e8ceb72880 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -150,10 +150,10 @@ export const AuthDialog: DialogComponent = observer(function AuthD }} > = observer(function }); return ( - + { + const finalFocus = document.getElementById(SLIDE_PANEL_CLOSE_BUTTON_ID); + finalFocus?.focus(); + + return false; + }} + fixedSize + > diff --git a/webapp/packages/plugin-projects/src/FolderDialog.tsx b/webapp/packages/plugin-projects/src/FolderDialog.tsx index a178327af91..6a770179ef4 100644 --- a/webapp/packages/plugin-projects/src/FolderDialog.tsx +++ b/webapp/packages/plugin-projects/src/FolderDialog.tsx @@ -7,7 +7,7 @@ */ import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { Button, @@ -21,7 +21,6 @@ import { InputField, s, Translate, - useFocus, useObservableRef, useResource, useS, @@ -78,7 +77,7 @@ export const FolderDialog: DialogComponent({ focusFirstChild: true }); + const inputRef = useRef(null); const { icon, folder, bigIcon, viewBox, value, projectId, selectProject, objectName, create, confirmActionText, filterProject } = payload; let { title } = payload; @@ -162,12 +161,13 @@ export const FolderDialog: DialogComponent + - + setOpen(false)} data-size="medium"> + + Dialog Title + + + Dialog content goes here + + + + + +; + +// Using DialogProvider +const dialog = useDialogStore(); + + + + + + + + Title + + + Content + + + Close + + +; +``` + +## Components + +- **Dialog** - Main dialog container, renders a modal window +- **DialogDisclosure** - Button to open the dialog (requires DialogProvider) +- **DialogHeader** - Header container that sticks to the top, typically contains DialogHeading +- **DialogBody** - Scrollable body container that fills available space +- **DialogFooter** - Footer container that sticks to the bottom, typically contains action buttons +- **DialogHeading** - Dialog heading/title element (accessible label) +- **DialogDescription** - Dialog description element (accessible description) +- **DialogDismiss** - Button to close the dialog +- **DialogProvider** - Provider for managing dialog state with `useDialogStore` + +## Sizes + +The Dialog component supports three predefined sizes via the `data-size` attribute: + +- **small** - 404px × 262px +- **medium** - 576px × 396px - default +- **large** - 720px × 556px + +```tsx +... +... +... +``` + +## Variants + +### Slide Variant (Side Panel) + +The Dialog also supports a `slide` variant that creates a side panel appearing from the right: + +```tsx + +
+ Settings Panel + Configure your preferences here... +
+
+``` + +**Slide variant features:** + +- Slides in from the right side of the screen +- Fills 100% width and height of the parent container +- Use `portal={false}` to render within parent instead of body +- Slower animation: 300ms vs 150ms +- Perfect for settings panels, detailed forms, or content requiring more space + +**Note:** When using `data-variant="slide"`, the `data-size` attribute is ignored. + +## Class names + +- `.dbv-kit-dialog` - the main class name for the dialog container. +- `.dbv-kit-dialog__disclosure` - the class name for the disclosure/trigger button. +- `.dbv-kit-dialog__header` - the class name for the header container. +- `.dbv-kit-dialog__body` - the class name for the scrollable body container. +- `.dbv-kit-dialog__footer` - the class name for the footer container. +- `.dbv-kit-dialog__heading` - the class name for the heading element. +- `.dbv-kit-dialog__description` - the class name for the description element. +- `.dbv-kit-dialog__dismiss` - the class name for the dismiss/close button. + +## Styling + +The Dialog component supports customization through CSS variables. + +```css +/* Container */ +--dbv-kit-dialog-content-background /* #ffffff */ +--dbv-kit-dialog-content-foreground /* #353535 */ +--dbv-kit-dialog-content-border-radius /* 0.25rem */ +--dbv-kit-dialog-content-shadow /* 0 6px 6px -3px #0003, 0 10px 14px 1px #00000024, 0 4px 18px 3px #0000001f */ + +/* Layout */ +--dbv-kit-dialog-header-padding /* 1rem 1.5rem */ +--dbv-kit-dialog-header-border-bottom /* none */ +--dbv-kit-dialog-body-padding /* 1.5rem */ +--dbv-kit-dialog-footer-padding /* 1rem 1.5rem */ +--dbv-kit-dialog-footer-border-top /* none */ +--dbv-kit-dialog-footer-gap /* 0.75rem */ + +/* Sizes */ +--dbv-kit-dialog-small-width /* 404px */ +--dbv-kit-dialog-small-height /* 262px */ +--dbv-kit-dialog-medium-width /* 576px */ +--dbv-kit-dialog-medium-height /* 374px */ +--dbv-kit-dialog-large-width /* 748px */ +--dbv-kit-dialog-large-height /* 468px */ +--dbv-kit-dialog-max-width /* 748px */ + +/* Slide variant */ +--dbv-kit-dialog-slide-transition-duration /* 300ms */ +--dbv-kit-dialog-slide-transition-timing /* ease-in-out */ + +/* Backdrop */ +--dbv-kit-dialog-backdrop-background /* #0000007a */ + +/* Heading */ +--dbv-kit-dialog-heading-margin /* 0 */ +--dbv-kit-dialog-heading-font-size /* 1.25rem */ +--dbv-kit-dialog-heading-font-weight /* 400 */ +--dbv-kit-dialog-heading-line-height /* 2rem */ +--dbv-kit-dialog-heading-color /* var(--dbv-kit-dialog-content-foreground) */ + +/* Description */ +--dbv-kit-dialog-description-margin /* 0 */ +--dbv-kit-dialog-description-font-size /* 0.875rem */ +--dbv-kit-dialog-description-color /* inherit */ + +/* Disclosure */ +--dbv-kit-dialog-disclosure-background /* transparent */ +--dbv-kit-dialog-disclosure-foreground /* inherit */ + +/* Dismiss */ +--dbv-kit-dialog-dismiss-background /* #f2f2f2 */ +--dbv-kit-dialog-dismiss-foreground /* #353535 */ +--dbv-kit-dialog-dismiss-hover-background /* #e5e5e5 */ +``` + +### Backdrop Styling + +The dialog backdrop can be styled using the `[data-backdrop]` selector: + +```css +[data-backdrop] { + background-color: rgba(0, 0, 0, 0.5); +} +``` + +## Hooks + +The following hooks are re-exported for advanced scenarios: + +- `useDialogStore` — create or control a dialog store programmatically +- `useDialogContext` — access the current dialog context/store + +## Props + +All components are thin wrappers around Ariakit: + +### Dialog + +Inherits Ariakit `Dialog` props. Accepts the following properties: + +- `open` - Controls dialog visibility +- `onClose` - Callback when dialog is closed +- `modal` - Whether the dialog is modal (default: true) +- `portal` - Whether to render in a portal (default: true) +- `animated` - Whether to animate dialog appearance (default: true). Set to `false` to disable animations, useful when combining with other animations. Note: animations are automatically disabled when user has `prefers-reduced-motion: reduce` enabled. +- `data-size` - Dialog size: `'small'`, `'medium'`, or `'large'` +- `data-variant` - Dialog variant: `'slide'` for side panel +- Other standard Ariakit dialog properties + +### DialogHeading + +Inherits Ariakit `DialogHeading` props. + +### DialogDescription + +Inherits Ariakit `DialogDescription` props. + +### DialogDismiss + +Inherits Ariakit `DialogDismiss` props. + +### DialogDisclosure + +Inherits Ariakit `DialogDisclosure` props. + +### DialogProvider + +Inherits Ariakit `DialogProvider` props and provides the dialog store context. + +## Accessibility + +The Dialog component follows the [WAI-ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/): + +- Focus is trapped inside the dialog when open +- Escape key closes the dialog +- Focus returns to the trigger element when closed +- Backdrop click closes the dialog +- `DialogHeading` provides accessible label via `aria-labelledby` +- `DialogDescription` provides accessible description via `aria-describedby` +- **Animations automatically disabled** when user has `prefers-reduced-motion: reduce` setting enabled +- Proper ARIA attributes for screen readers +- Support for both modal and modeless dialogs +- Keyboard navigation support + +## Underlying Components + +For more advanced usage, see the [Ariakit Dialog documentation](https://ariakit.org/components/dialog). diff --git a/webapp/packages/storybook/src/stories/components/Dialog/dialog.stories.tsx b/webapp/packages/storybook/src/stories/components/Dialog/dialog.stories.tsx new file mode 100644 index 00000000000..9c53e81f7f7 --- /dev/null +++ b/webapp/packages/storybook/src/stories/components/Dialog/dialog.stories.tsx @@ -0,0 +1,305 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + Dialog, + DialogBody, + DialogDescription, + DialogDisclosure, + DialogDismiss, + DialogFooter, + DialogHeader, + DialogHeading, + DialogProvider, + useDialogStore, + Button, +} from '@dbeaver/ui-kit'; +import { Meta } from '@storybook/react-vite'; +import { JSX, useState } from 'react'; + +const meta = { + component: Dialog, +} satisfies Meta; + +export default meta; + +export function Default(): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> + + Dialog Title + + + This is a simple dialog component with basic content using the new layout system. + + + + + + + + ); +} + +export function WithProvider(): JSX.Element { + const dialog = useDialogStore(); + + return ( + + + + + + + Dialog with Provider + + + This dialog is managed by DialogProvider using useDialogStore hook. + + + Close + + + + ); +} + +export function WithMultipleActions(): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> + + Confirm Action + + + Are you sure you want to proceed with this action? This action cannot be undone. + + + + + + + + ); +} + +export function WithScrollableContent(): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> + + Long Content Dialog + + + + {Array.from({ length: 20 }, (_, i) => ( +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +
+ ))} +
+
+ + Close + +
+ + ); +} + +export function SmallSize(): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> + + Small Dialog + + + This is a small dialog (404x262px) + + + Close + + + + ); +} + +export function SlideVariant(): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> +
+ Slide Panel + + This is a slide panel that appears from the right side of the screen, covering almost the entire viewport (leaving 120px on the left). +
+
+ Perfect for detailed forms, settings panels, or any content that needs more space than a regular dialog. +
+
+ + +
+
+
+ + ); +} + +export function NestedDialogs(): JSX.Element { + const [formValue, setFormValue] = useState(''); + const [formOpen, setFormOpen] = useState(false); + const [warningOpen, setWarningOpen] = useState(false); + + // Reset the form value whenever the dialog is closed + if (!formOpen && formValue) { + setFormValue(''); + } + + return ( + <> + + { + // If there's unsaved content, show warning dialog instead of closing + if (formValue.trim()) { + event.preventDefault(); + setWarningOpen(true); + } else { + setFormOpen(false); + } + }} + > + + Create Post + + + { + event.preventDefault(); + setFormOpen(false); + }} + > +