From 9b530d891143af22089a731699d492fde2160b7d Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Mon, 8 Sep 2025 23:17:14 +0300 Subject: [PATCH] feat(Dialog): update layout --- .../Dialog/ButtonClose/ButtonClose.scss | 14 +- src/components/Dialog/Dialog.scss | 21 ++- src/components/Dialog/Dialog.tsx | 11 +- .../Dialog/DialogBody/DialogBody.scss | 9 +- .../Dialog/DialogDivider/DialogDivider.scss | 6 +- .../Dialog/DialogFooter/DialogFooter.scss | 10 +- .../Dialog/DialogHeader/DialogHeader.scss | 14 +- .../Dialog/__stories__/Dialog.stories.tsx | 140 ++++++++++++------ .../Dialog/__stories__/DialogShowcase.tsx | 93 ------------ .../Dialog/__stories__/DynamicHeightStory.tsx | 116 +++++++++++++++ src/components/Dialog/variables.scss | 10 ++ 11 files changed, 254 insertions(+), 190 deletions(-) create mode 100644 src/components/Dialog/__stories__/DynamicHeightStory.tsx create mode 100644 src/components/Dialog/variables.scss diff --git a/src/components/Dialog/ButtonClose/ButtonClose.scss b/src/components/Dialog/ButtonClose/ButtonClose.scss index ab48918c09..84f2110321 100644 --- a/src/components/Dialog/ButtonClose/ButtonClose.scss +++ b/src/components/Dialog/ButtonClose/ButtonClose.scss @@ -1,14 +1,8 @@ -@use '../../variables'; +@use '../variables'; -$block: '.#{variables.$ns}dialog-btn-close'; - -#{$block} { +#{variables.$closeBtnBlock} { position: absolute; - // Offset from icon to the edges should be 20px (36px is Button's size, 8px is half of Button's padding) - // Icon size = 20px - // Button size = 36px - // Offset = 20 - ((36 - 20) / 2) = 14 - inset-block-start: 14px; - inset-inline-end: 14px; + inset-block-start: 12px; + inset-inline-end: 16px; z-index: 1; } diff --git a/src/components/Dialog/Dialog.scss b/src/components/Dialog/Dialog.scss index 2818ea0397..3e47b05712 100644 --- a/src/components/Dialog/Dialog.scss +++ b/src/components/Dialog/Dialog.scss @@ -1,15 +1,16 @@ -@use '../variables'; +@use './variables'; -$block: '.#{variables.$ns}dialog'; - -#{$block} { +#{variables.$block} { --_--side-padding: 32px; + --_--block-start-padding: 20px; + --_--block-end-padding: 24px; --_--close-button-space: 0px; position: relative; display: flex; flex-direction: column; width: var(--g-dialog-width, var(--_--width)); + padding-block: var(--_--block-start-padding) var(--_--block-end-padding); &_has-scroll { overflow-y: auto; @@ -32,7 +33,15 @@ $block: '.#{variables.$ns}dialog'; } } - &_has-close { - --_--close-button-space: 24px; + &:has(#{variables.$closeBtnBlock}) { + --_--close-button-space: 26px; + } + + &:has(#{variables.$headerBlock}) { + padding-block-start: 0; + } + + &:has(#{variables.$footerBlock}) { + padding-block-end: 0; } } diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index d1d34a171f..31bb371bcb 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -138,16 +138,7 @@ export function Dialog({ qa={qa} disableHeightTransition > -
+
{children} diff --git a/src/components/Dialog/DialogBody/DialogBody.scss b/src/components/Dialog/DialogBody/DialogBody.scss index 05ef64c450..c8ca5af742 100644 --- a/src/components/Dialog/DialogBody/DialogBody.scss +++ b/src/components/Dialog/DialogBody/DialogBody.scss @@ -1,9 +1,8 @@ -@use '../../variables'; +@use '../variables'; -$block: '.#{variables.$ns}dialog-body'; - -#{$block} { - padding: 10px var(--_--side-padding); +#{variables.$bodyBlock} { + min-height: 24px; + padding: 4px var(--_--side-padding); flex: 1 1 auto; overflow: auto; transition: height 0.35s ease-in-out; diff --git a/src/components/Dialog/DialogDivider/DialogDivider.scss b/src/components/Dialog/DialogDivider/DialogDivider.scss index ab83c0b14e..424c688551 100644 --- a/src/components/Dialog/DialogDivider/DialogDivider.scss +++ b/src/components/Dialog/DialogDivider/DialogDivider.scss @@ -1,8 +1,6 @@ -@use '../../variables'; +@use '../variables'; -$block: '.#{variables.$ns}dialog-divider'; - -#{$block} { +#{variables.$dividerBlock} { border-block-start: 1px solid var(--g-color-line-generic); margin: 0 calc(-1 * var(--_--side-padding)); } diff --git a/src/components/Dialog/DialogFooter/DialogFooter.scss b/src/components/Dialog/DialogFooter/DialogFooter.scss index a6af985630..7eb040e91b 100644 --- a/src/components/Dialog/DialogFooter/DialogFooter.scss +++ b/src/components/Dialog/DialogFooter/DialogFooter.scss @@ -1,15 +1,13 @@ -@use '../../variables'; +@use '../variables'; -$block: '.#{variables.$ns}dialog-footer'; - -#{$block} { - padding: 28px var(--_--side-padding); +#{variables.$footerBlock} { + padding: 24px var(--_--side-padding) 28px; display: flex; align-items: center; &__bts-wrapper { display: flex; - gap: 10px; + gap: var(--g-spacing-2); } &__children { diff --git a/src/components/Dialog/DialogHeader/DialogHeader.scss b/src/components/Dialog/DialogHeader/DialogHeader.scss index 9592869b62..b97844703f 100644 --- a/src/components/Dialog/DialogHeader/DialogHeader.scss +++ b/src/components/Dialog/DialogHeader/DialogHeader.scss @@ -1,15 +1,11 @@ -@use '../../variables'; +@use '../variables'; @use '../../../../styles/mixins'; -$block: '.#{variables.$ns}dialog-header'; - -#{$block} { - padding-block: 20px 10px; +#{variables.$headerBlock} { + min-height: 36px; + padding-block: 12px 8px; padding-inline: var(--_--side-padding) - calc( - var(--_--side-padding) + var(--_--close-button-space) * var(--g-flow-is-ltr) + - var(--_--close-button-space) * var(--g-flow-is-rtl) - ); + calc(var(--_--side-padding) + var(--_--close-button-space)); line-height: 24px; display: flex; align-items: center; diff --git a/src/components/Dialog/__stories__/Dialog.stories.tsx b/src/components/Dialog/__stories__/Dialog.stories.tsx index beaee035d2..8579892537 100644 --- a/src/components/Dialog/__stories__/Dialog.stories.tsx +++ b/src/components/Dialog/__stories__/Dialog.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {faker} from '@faker-js/faker/locale/en'; -import type {Meta, StoryFn} from '@storybook/react-webpack5'; +import type {Meta, StoryObj} from '@storybook/react-webpack5'; import {useUniqId} from '../../../hooks'; import {Button} from '../../Button'; @@ -10,6 +10,7 @@ import {Dialog} from '../Dialog'; import type {DialogProps} from '../Dialog'; import {DialogShowcase} from './DialogShowcase'; +import {DynamicHeightStory} from './DynamicHeightStory'; export default { title: 'Components/Overlays/Dialog', @@ -22,17 +23,31 @@ export default { type: 'boolean', }, }, -} as Meta; +} as Meta; + +type Story = StoryObj; const largeTextLines = Array.from({length: 30}, () => faker.lorem.sentences()); +interface DialogComponentProps { + buttonText: string; + content: React.ReactNode; + showError?: boolean; + withHeader?: boolean; + withFooter?: boolean; + withEmptyBody?: boolean; +} + function DialogComponent({ buttonText, content, - showError, + showError = false, + withHeader = true, + withFooter = true, + withEmptyBody = false, ...args -}: DialogProps & {buttonText: string; content: React.ReactNode; showError: boolean}) { - const titleId = useUniqId(); +}: DialogProps & DialogComponentProps) { + const headerId = useUniqId(); const [open, setOpen] = React.useState(false); return ( @@ -45,55 +60,86 @@ function DialogComponent({ onEnterKeyDown={() => { alert('onEnterKeyDown'); }} - aria-labelledby={titleId} + aria-labelledby={withHeader ? headerId : undefined} > - + {withHeader && } -
- {content} -
+ {withEmptyBody ? null : ( +
+ {content} +
+ )}
- setOpen(false)} - onClickButtonApply={() => alert('onApply')} - textButtonApply="Apply" - textButtonCancel="Cancel" - showError={showError} - errorText="Error text" - /> + {withFooter && ( + setOpen(false)} + onClickButtonApply={() => alert('onApply')} + textButtonApply="Apply" + textButtonCancel="Cancel" + showError={showError} + errorText="Error text" + /> + )}
); } -export const Default: StoryFn = (args) => { - return ( - - - ( -
- {text} -
- ))} - {...args} - /> -
- ); +export const Default: Story = { + render: (args) => { + return ( + + + ( +
+ {text} +
+ ))} + {...args} + /> + + + +
+ ); + }, }; -const ShowcaseTemplate: StoryFn = () => ; -export const Showcase = ShowcaseTemplate.bind({}); +export const DynamicHeight: Story = { + render: (args) => , +}; + +export const Showcase: Story = { + render: () => , +}; diff --git a/src/components/Dialog/__stories__/DialogShowcase.tsx b/src/components/Dialog/__stories__/DialogShowcase.tsx index 90be5c3057..87d016c336 100644 --- a/src/components/Dialog/__stories__/DialogShowcase.tsx +++ b/src/components/Dialog/__stories__/DialogShowcase.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import {Button} from '../../../components/Button'; import {Dialog} from '../../../components/Dialog/Dialog'; -import {Loader} from '../../../components/Loader'; import {Select} from '../../../components/Select'; import {TextInput} from '../../controls'; import {Flex} from '../../layout/Flex/Flex'; @@ -124,95 +123,6 @@ function OtherDialog() { ); } -function DynamicHeightDialog() { - const [open, setOpen] = React.useState(false); - const [isFirstDynamicPartOpen, setIsFirstDynamicPartOpen] = React.useState(false); - const [isSecondDynamicPartOpen, setIsSecondDynamicPartOpen] = React.useState(false); - - const switchVisibility = () => { - setOpen((prevOpen) => !prevOpen); - }; - - React.useEffect(() => { - const timers: number[] = []; - if (open) { - timers[0] = window.setTimeout(() => { - setIsFirstDynamicPartOpen(true); - }, 2000); - timers[1] = window.setTimeout(() => { - setIsSecondDynamicPartOpen(true); - }, 4000); - } else { - setIsFirstDynamicPartOpen(false); - setIsSecondDynamicPartOpen(false); - } - return () => { - timers.forEach((t) => { - window.clearTimeout(t); - }); - }; - }, [open]); - - return ( -
- - - -
-
- This is a dialog with dynamic height -
- {isFirstDynamicPartOpen && ( -
-
Content loaded
-
This is the description
-
More content to come
-
- )} - {(!isFirstDynamicPartOpen || !isSecondDynamicPartOpen) && ( -
- -
- )} - {isSecondDynamicPartOpen && ( -
-
Second part loaded
-
This is the description part 1
-
This is the description part 2
-
This is the description part 3
-
No more content to load
-
- )} -
-
- -
-
- ); -} - export function DialogShowcase() { const buttonRef = React.useRef(null); const [open, setOpen] = React.useState(false); @@ -301,9 +211,6 @@ export function DialogShowcase() {
-
- -
); } diff --git a/src/components/Dialog/__stories__/DynamicHeightStory.tsx b/src/components/Dialog/__stories__/DynamicHeightStory.tsx new file mode 100644 index 0000000000..883780edad --- /dev/null +++ b/src/components/Dialog/__stories__/DynamicHeightStory.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; + +import {Button} from '../../Button'; +import {Loader} from '../../Loader'; +import {Dialog} from '../Dialog'; +import type {DialogProps} from '../Dialog'; + +export function DynamicHeightStory(args: DialogProps) { + const [open, setOpen] = React.useState(false); + const timersRef = React.useRef([]); + const [isFirstDynamicPartOpen, setIsFirstDynamicPartOpen] = React.useState(false); + const [isSecondDynamicPartOpen, setIsSecondDynamicPartOpen] = React.useState(false); + + const switchVisibility = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const play = () => { + if (open) { + timersRef.current[0] = window.setTimeout(() => { + setIsFirstDynamicPartOpen(true); + }, 2000); + timersRef.current[1] = window.setTimeout(() => { + setIsSecondDynamicPartOpen(true); + }, 4000); + } + }; + + const cleanup = () => { + timersRef.current.forEach((t) => { + window.clearTimeout(t); + }); + }; + + const reset = () => { + cleanup(); + setIsFirstDynamicPartOpen(false); + setIsSecondDynamicPartOpen(false); + }; + + React.useEffect(() => { + if (open) { + play(); + } + + return () => { + cleanup(); + }; + }, [open]); + + return ( +
+ + reset()} + qa="dynamicHeight" + > + + +
+ {isFirstDynamicPartOpen && ( +
+
Content loaded
+
This is the description
+
More content to come
+
+ )} + {(!isFirstDynamicPartOpen || !isSecondDynamicPartOpen) && ( +
+ +
+ )} + {isSecondDynamicPartOpen && ( +
+
Second part loaded
+
This is the description part 1
+
This is the description part 2
+
This is the description part 3
+
No more content to load
+
+ )} +
+
+ { + reset(); + play(); + }} + onClickButtonCancel={() => setOpen(false)} + /> +
+
+ ); +} diff --git a/src/components/Dialog/variables.scss b/src/components/Dialog/variables.scss new file mode 100644 index 0000000000..9d478aee6b --- /dev/null +++ b/src/components/Dialog/variables.scss @@ -0,0 +1,10 @@ +@forward '../variables'; + +@use '../variables'; + +$block: '.#{variables.$ns}dialog'; +$headerBlock: '.#{variables.$ns}dialog-header'; +$bodyBlock: '.#{variables.$ns}dialog-body'; +$footerBlock: '.#{variables.$ns}dialog-footer'; +$dividerBlock: '.#{variables.$ns}dialog-divider'; +$closeBtnBlock: '.#{variables.$ns}dialog-btn-close';