Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/react/src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DialogTitle } from './DialogTitle';

import { Button } from '../Button';
import { useDialogStack } from '../DialogStack';
import { useDialogStackV2 } from '../DialogStackV2';

const getOpenButtonText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Open dialog window' : 'Открыть диалоговое окно';
Expand Down Expand Up @@ -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 }) => (
<Dialog fullWidth maxWidth="700px" onClose={() => close()}>
<DialogTitle>
{getHeadingText(context)} {i + 1}
</DialogTitle>
<DialogContent>
<Typography variant="body200">
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.
</Typography>
</DialogContent>
<DialogActions>
<Button color="tertiary" size="500" variant="outlined" onClick={() => close()}>
{getCancelButtonText(context)}
</Button>
<Button color="primary" size="500" variant="contained" onClick={onOpen(i + 1)}>
{getOpenButtonText(context)}
</Button>
</DialogActions>
</Dialog>
));
};

return (
<Button color="primary" variant="contained" onClick={onOpen(0)}>
{getOpenButtonText(context)}
</Button>
);
},
};
36 changes: 36 additions & 0 deletions packages/react/src/components/DialogStackV2/DialogStack.api.mdx
Original file line number Diff line number Diff line change
@@ -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';

<Meta title="Components API/DialogStack" />

# DialogStack API

```js
import { DialogStack, useDialogStackV2 as useDialogStack } from '@esfront/react';
```

## Props

<TableInterface name="DialogStackProps" variant="props" />

<br />

## `useDialogStack`

<TableFunction name="useDialogStackV2" />

<TableInterface name="DialogStackContextValue" variant="props" />

<br />

## Demos

<ul>
<li>
<LinkTo kind="components-Dialog" story="demo">
<code>Dialog</code>
</LinkTo>
</li>
</ul>
80 changes: 80 additions & 0 deletions packages/react/src/components/DialogStackV2/DialogStack.state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ReactElement } from 'react';

import { DialogProps } from '../Dialog';

export type DialogStackComponentInterface = Pick<DialogProps, 'open' | 'BackdropProps' | 'TransitionProps'>;

export type DialogData = {
id: number | string;
open: boolean;
onExited: () => void;
component: ReactElement<DialogStackComponentInterface>;
};

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<DialogStackComponentInterface>,
params?: { id?: string }
) => {
const dialogId = params?.id || this.dialogId++;
let close: (data?: any) => void;

const afterClosed = new Promise<any>((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();
52 changes: 52 additions & 0 deletions packages/react/src/components/DialogStackV2/DialogStack.tsx
Original file line number Diff line number Diff line change
@@ -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<DialogData[]>([]);

useEffect(() => {
const unsubscribe = dialogStackState.subscribe(setDialogs);

return () => {
unsubscribe();
};
}, []);

return (
<>
{dialogs.map((dialog, index) => {
if (!isValidElement(dialog.component)) {
return null;
}

return (
<Fragment key={dialog.id}>
{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)}
</Fragment>
);
})}
</>
);
};

DialogStack.dialogId = 1;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DialogStack.dialogId = 1;

4 changes: 4 additions & 0 deletions packages/react/src/components/DialogStackV2/index.ts
Original file line number Diff line number Diff line change
@@ -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';
32 changes: 32 additions & 0 deletions packages/react/src/components/DialogStackV2/useDialogStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactElement, useEffect, useRef } from 'react';

import { DialogStackComponentInterface, dialogStackState } from './DialogStack.state';

export const useDialogStackV2 = () => {
const dialogs = useRef<Array<number | string>>([]);

useEffect(() => {
return () => {
dialogs.current.forEach((id) => {
dialogStackState.closeDialogById(id);
});
};
}, []);

return {
close: dialogStackState.closeDialogById,
open: (
dialog: (props: { close: (data?: any) => void }) => ReactElement<DialogStackComponentInterface>,
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;
},
};
};
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/testing/Theme/Theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -45,6 +46,7 @@ export const Theme = ({ children, isDarkMode, locale }: IThemeProps) => {
<DialogStackProvider enableHistoryOverride>
<DateAdapterProvider adapter={DateFnsAdapter} locale={locale === 'ru' ? dateRU : dateEN}>
{children}
<DialogStack />
</DateAdapterProvider>
</DialogStackProvider>
</ThemeProvider>
Expand Down
Loading