diff --git a/packages/react/src/components/Dialog/Dialog.stories.tsx b/packages/react/src/components/Dialog/Dialog.stories.tsx index c6c5e55a1..90767587c 100644 --- a/packages/react/src/components/Dialog/Dialog.stories.tsx +++ b/packages/react/src/components/Dialog/Dialog.stories.tsx @@ -13,6 +13,7 @@ import { DialogTitle } from './DialogTitle'; import { Button } from '../Button'; import { useDialogStack } from '../DialogStack'; +import { useDialogStackV2 } from '../DialogStackV2'; const getOpenButtonText = (context: StoryContext) => { return context.globals.locale === 'en' ? 'Open dialog window' : 'Открыть диалоговое окно'; @@ -308,3 +309,42 @@ export const Stack: Story = { ); }, }; + +export const DialogStackV2: Story = { + name: 'Stack V2', + render: function Render(args, context) { + const dialogStack = useDialogStackV2(); + + const onOpen = (i: number) => () => { + dialogStack.open(({ close }) => ( + close()}> + + {getHeadingText(context)} {i + 1} + + + + Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget + quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum. + + + + + + + + )); + }; + + return ( + + ); + }, +}; diff --git a/packages/react/src/components/DialogStackV2/DialogStack.api.mdx b/packages/react/src/components/DialogStackV2/DialogStack.api.mdx new file mode 100644 index 000000000..f673443cb --- /dev/null +++ b/packages/react/src/components/DialogStackV2/DialogStack.api.mdx @@ -0,0 +1,36 @@ +import { Meta } from '@storybook/blocks'; +import LinkTo from '@storybook/addon-links/react'; +import { TableInterface } from '~storybook/components/TableInterface'; +import { TableFunction } from '~storybook/components/TableFunction'; + + + +# DialogStack API + +```js +import { DialogStack, useDialogStackV2 as useDialogStack } from '@esfront/react'; +``` + +## Props + + + +
+ +## `useDialogStack` + + + + + +
+ +## Demos + +
    +
  • + + Dialog + +
  • +
diff --git a/packages/react/src/components/DialogStackV2/DialogStack.state.ts b/packages/react/src/components/DialogStackV2/DialogStack.state.ts new file mode 100644 index 000000000..cd83f0683 --- /dev/null +++ b/packages/react/src/components/DialogStackV2/DialogStack.state.ts @@ -0,0 +1,80 @@ +import { ReactElement } from 'react'; + +import { DialogProps } from '../Dialog'; + +export type DialogStackComponentInterface = Pick; + +export type DialogData = { + id: number | string; + open: boolean; + onExited: () => void; + component: ReactElement; +}; + +export class DialogStackState { + private _dialogs: DialogData[] = []; + + private subscribers: Array<(data: DialogData[]) => void> = []; + private dialogId = 0; + + public subscribe = (callback: (data: DialogData[]) => void) => { + this.subscribers.push(callback); + + return () => { + this.subscribers = this.subscribers.filter((s) => s !== callback); + }; + }; + + public get dialogs() { + return this._dialogs; + } + + public set dialogs(value: DialogData[]) { + this._dialogs = value; + + for (const s of this.subscribers) { + s(this._dialogs); + } + } + + public closeDialogById = (id: number | string) => { + const index = this.dialogs.findIndex((e) => e.id === id); + + if (index !== -1) { + const newValue = this.dialogs.slice(); + newValue[index].open = false; + this.dialogs = newValue; + } + }; + + public open = ( + dialog: (props: { close: (data?: any) => void }) => ReactElement, + params?: { id?: string } + ) => { + const dialogId = params?.id || this.dialogId++; + let close: (data?: any) => void; + + const afterClosed = new Promise((resolve) => { + close = (data?: any) => { + const index = this.dialogs.findIndex((e) => e.id === dialogId); + + if (index !== -1 && this.dialogs[index].open) { + this.closeDialogById(dialogId); + resolve(data); + } + }; + + const onExited = () => { + this.dialogs = this.dialogs.filter((dialog) => dialog.id !== dialogId); + }; + + this.dialogs = [...this.dialogs, { id: dialogId, open: true, onExited, component: dialog({ close }) }]; + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return { id: dialogId, close, afterClosed }; + }; +} + +export const dialogStackState = new DialogStackState(); diff --git a/packages/react/src/components/DialogStackV2/DialogStack.tsx b/packages/react/src/components/DialogStackV2/DialogStack.tsx new file mode 100644 index 000000000..80535b93a --- /dev/null +++ b/packages/react/src/components/DialogStackV2/DialogStack.tsx @@ -0,0 +1,52 @@ +import { cloneElement, Fragment, isValidElement, useEffect, useState } from 'react'; + +import { DialogData, dialogStackState } from './DialogStack.state'; + +export const DialogStack = () => { + const [dialogs, setDialogs] = useState([]); + + useEffect(() => { + const unsubscribe = dialogStackState.subscribe(setDialogs); + + return () => { + unsubscribe(); + }; + }, []); + + return ( + <> + {dialogs.map((dialog, index) => { + if (!isValidElement(dialog.component)) { + return null; + } + + return ( + + {cloneElement(dialog.component, { + open: dialog.open, + BackdropProps: { + style: { + opacity: index < dialogs.filter((dialog) => dialog.open).length - 1 ? '0' : '', + ...dialog.component.props.BackdropProps?.style, + }, + ...dialog.component.props.BackdropProps, + }, + TransitionProps: { + ...dialog.component.props.TransitionProps, + onExited: (node: HTMLElement) => { + if (dialog.component.props.TransitionProps?.onExited) { + dialog.component.props.TransitionProps.onExited(node); + } + + dialog.onExited(); + }, + }, + } as never)} + + ); + })} + + ); +}; + +DialogStack.dialogId = 1; diff --git a/packages/react/src/components/DialogStackV2/index.ts b/packages/react/src/components/DialogStackV2/index.ts new file mode 100644 index 000000000..72074a4c7 --- /dev/null +++ b/packages/react/src/components/DialogStackV2/index.ts @@ -0,0 +1,4 @@ +export { DialogStack } from './DialogStack'; +export type { DialogData, DialogStackComponentInterface } from './DialogStack.state'; +export { DialogStackState, dialogStackState } from './DialogStack.state'; +export { useDialogStackV2 } from './useDialogStack'; diff --git a/packages/react/src/components/DialogStackV2/useDialogStack.ts b/packages/react/src/components/DialogStackV2/useDialogStack.ts new file mode 100644 index 000000000..f48cbc08d --- /dev/null +++ b/packages/react/src/components/DialogStackV2/useDialogStack.ts @@ -0,0 +1,32 @@ +import { ReactElement, useEffect, useRef } from 'react'; + +import { DialogStackComponentInterface, dialogStackState } from './DialogStack.state'; + +export const useDialogStackV2 = () => { + const dialogs = useRef>([]); + + useEffect(() => { + return () => { + dialogs.current.forEach((id) => { + dialogStackState.closeDialogById(id); + }); + }; + }, []); + + return { + close: dialogStackState.closeDialogById, + open: ( + dialog: (props: { close: (data?: any) => void }) => ReactElement, + params?: { id?: string } + ) => { + const result = dialogStackState.open(dialog, params); + dialogs.current.push(result.id); + + result.afterClosed.then(() => { + dialogs.current = dialogs.current.filter((id) => id !== result.id); + }); + + return result; + }, + }; +}; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 206915895..f5e7c7d9f 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -20,6 +20,7 @@ export * from './Chips'; export * from './DateAdapter'; export * from './Dialog'; export * from './DialogStack'; +export * from './DialogStackV2'; export * from './Divider'; export * from './Dropzone'; export * from './EmptyState'; diff --git a/packages/react/src/testing/Theme/Theme.tsx b/packages/react/src/testing/Theme/Theme.tsx index 8045bb035..4db803455 100644 --- a/packages/react/src/testing/Theme/Theme.tsx +++ b/packages/react/src/testing/Theme/Theme.tsx @@ -10,6 +10,7 @@ import { enUS, ruRU } from '@mui/material/locale'; import { DateAdapterProvider, en, ru } from '../../components'; import { DialogStackProvider } from '../../components/DialogStack'; +import { DialogStack } from '../../components/DialogStackV2'; import { createTheme, palettes, ThemeProvider } from '../../theming'; function ColorScheme({ isDarkMode }: { isDarkMode?: boolean }) { @@ -45,6 +46,7 @@ export const Theme = ({ children, isDarkMode, locale }: IThemeProps) => { {children} +