Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Meta } from '@storybook/addon-docs/blocks';
import LinkTo from '@storybook/addon-links/react';
import { TableInterface } from '~storybook/components/TableInterface';

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

# ConfirmationDialog API

```js
import { ConfirmationDialog } from '@esfront/react';
```

## Component name

The name `ESConfirmationDialog` can be used when providing default props.

## Props

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

<br />

## Demos

<ul>
<li>
<LinkTo kind="components-ConfirmationDialog" story="demo">
<code>ConfirmationDialog</code>
</LinkTo>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ComponentProps } from 'react';

import { Meta, StoryContext, StoryObj } from '@storybook/react-vite';

import { ConfirmationDialog } from './ConfirmationDialog';

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

const getOpenButtonText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Open dialog window' : 'Открыть диалоговое окно';
};

const getHeadingText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Close without saving?' : 'Закрыть без сохранения?';
};

const getContentText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en'
? 'The changes you have made will not be saved'
: 'Внесённые изменения не сохранятся';
};

const getCancelButtonText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Cancel' : 'Отменить';
};

const getConfirmButtonText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Close' : 'Закрыть';
};

const getCloseText = (context: StoryContext<unknown>) => {
return context.globals.locale === 'en' ? 'Close' : 'Закрыть';
};

type Args = ComponentProps<typeof ConfirmationDialog>;

const meta: Meta<Args> = {
tags: ['autodocs'],
component: ConfirmationDialog,
parameters: {
references: ['ConfirmationDialog', 'DialogStack'],
},
argTypes: {
children: {
table: {
disable: true,
},
},
action: {
table: {
disable: true,
},
},
close: {
table: {
disable: true,
},
},
title: {
table: {
disable: true,
},
},
icon: {
table: {
disable: true,
},
},
open: {
table: {
disable: true,
},
},
BackdropProps: {
table: {
disable: true,
},
},
},
args: {
severity: 'error',
disabled: false,
},
};

export default meta;

type Story = StoryObj<Args>;

export const Demo: Story = {
render: function Render(args, context) {
const dialogStack = useDialogStackV2();

const onOpen = () => {
dialogStack
.open(({ close }) => (
<ConfirmationDialog
{...args}
action={() =>
new Promise((resolve) => {
setTimeout(() => resolve(true), 1000);
})
}
close={close}
labelCancel={getCancelButtonText(context)}
labelClose={getCloseText(context)}
labelConfirm={getConfirmButtonText(context)}
labelEscapeKey="Esc"
title={getHeadingText(context)}
>
{getContentText(context)}
</ConfirmationDialog>
))
.afterClosed.then((data) => {
console.info(data);
});
};

return (
<Button color="primary" variant="contained" onClick={onOpen}>
{getOpenButtonText(context)}
</Button>
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import { RefAttributes, useState } from 'react';

import { ConfirmationDialogProps } from './ConfirmationDialog.types';

import clsx from 'clsx';

import { IconAlertFillW500, IconCloseLineW500, IconInformation2FillW500 } from '../../icons';
import { useDefaultProps } from '../../theming';
import { Button, ButtonOwnProps } from '../Button';
import { Dialog, DialogActions, DialogClose, DialogContent, DialogTitle } from '../Dialog';
import { LoadingButton } from '../LoadingButton';

const defaultIconMapping: Record<string, React.ReactNode> = {
error: <IconAlertFillW500 />,
primary: <IconInformation2FillW500 />,
};

/**
* ConfirmationDialog asks the user to approve an operation before it is performed.
*/
export const ConfirmationDialog = ({ ref, ...inProps }: ConfirmationDialogProps & RefAttributes<HTMLDivElement>) => {
const {
children,
className,
severity = 'error',
title,
icon,
iconClose = <IconCloseLineW500 />,
iconMapping = defaultIconMapping,
labelConfirm,
labelCancel,
labelClose,
labelEscapeKey,
disabled = false,
action,
close,
fullWidth = true,
maxWidth = '480px',
...props
} = useDefaultProps({
props: inProps,
name: 'ESConfirmationDialog',
});

const [loading, setLoading] = useState(false);

const onClose = () => {
if (!loading) {
close();
}
};

const onConfirm = () => {
setLoading(true);

action()
.then((data) => {
close(data);
})
.catch((error: unknown) => {
setLoading(false);
return Promise.reject(error);
});
};

return (
<Dialog
ref={ref}
before={<DialogClose label={labelClose} labelEscapeKey={labelEscapeKey} onClick={onClose} />}
className={clsx('es-confirmation-dialog', `es-confirmation-dialog--severity--${severity}`, className)}
fullWidth={fullWidth}
maxWidth={maxWidth}
onClose={onClose}
{...props}
>
<Button aria-label={labelClose} className="es-confirmation-dialog__close" color="tertiary" onClick={onClose}>
{iconClose}
</Button>
<DialogTitle align="center" icon={icon === false ? undefined : icon || iconMapping[severity]}>
{title}
</DialogTitle>
<DialogContent className="body200">{children}</DialogContent>
<DialogActions fullWidth>
<Button color="tertiary" size="500" variant="outlined" onClick={onClose}>
{labelCancel}
</Button>
{!!labelConfirm && (
<LoadingButton
color={severity as ButtonOwnProps['color']}
disabled={disabled}
loading={loading}
size="500"
variant="contained"
onClick={onConfirm}
>
{labelConfirm}
</LoadingButton>
)}
</DialogActions>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */

import { ReactNode } from 'react';

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

export interface ConfirmationDialogPropsSeverityOverrides {}

export type ConfirmationDialogSeverity = OverridableStringUnion<
'error' | 'primary',
ConfirmationDialogPropsSeverityOverrides
>;

export interface ConfirmationDialogProps extends Omit<DialogProps, 'before' | 'after' | 'children' | 'onClose'> {
children?: ReactNode;
/**
* The severity of the operation. It defines the color of the icon and of the confirm button.
* @default 'error'
*/
severity?: ConfirmationDialogSeverity;

/**
* The operation to perform when the user confirms. The dialog shows a loading state until the returned promise
* settles, then closes with its value. A rejection keeps the dialog open and is rethrown.
*/
action: () => Promise<unknown>;
/** Callback fired when the dialog requests to be closed, either with the value of `action` or without a value. */
close: (data?: unknown) => void;

/** The title text. */
title: ReactNode;
/**
* If `true`, the confirm button is disabled.
* @default false
*/
disabled?: boolean;

/** The icon displayed next to the title. Pass `false` to render the title without an icon. */
icon?: ReactNode | false;
/** The component maps the `severity` prop to an icon. Provide your own mapping to change it. */
iconMapping?: Partial<Record<ConfirmationDialogSeverity, ReactNode>>;

/** The icon of the close button rendered inside the dialog on narrow screens. */
iconClose?: ReactNode;

/** Text for the confirm button. The button is not rendered without it. */
labelConfirm?: ReactNode;
/** Text for the cancel button. */
labelCancel?: ReactNode;
/** Text for the close button aria-label. */
labelClose?: string;
/** Text for the escape key hint next to the close button. */
labelEscapeKey?: string;
}
6 changes: 6 additions & 0 deletions packages/react/src/components/ConfirmationDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { ConfirmationDialog } from './ConfirmationDialog';
export type {
ConfirmationDialogProps,
ConfirmationDialogPropsSeverityOverrides,
ConfirmationDialogSeverity,
} from './ConfirmationDialog.types';
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import { useDefaultProps } from '../../../theming';
* @see `Dialog`
*/
export const DialogActions = ({ ref, ...inProps }: DialogActionsProps & RefAttributes<HTMLDivElement>) => {
const { className, style, sticky, children } = useDefaultProps({
const {
className,
style,
sticky,
direction = 'horizontal',
fullWidth = false,
children,
} = useDefaultProps({
props: inProps,
name: 'ESDialogActions',
});
Expand All @@ -26,6 +33,8 @@ export const DialogActions = ({ ref, ...inProps }: DialogActionsProps & RefAttri
ref={ref}
className={clsx(
'es-dialog-actions',
`es-dialog-actions--direction--${direction}`,
fullWidth && 'es-dialog-actions--full-width',
sticky && 'es-dialog-actions--sticky',
stuck && 'es-dialog-actions--stuck',
className
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,14 @@ export interface DialogActionsProps {
style?: CSSProperties;
/** Whether the actions should be sticky. */
sticky?: boolean;
/**
* The direction the actions are laid out in.
* @default 'horizontal'
*/
direction?: 'horizontal' | 'vertical';
/**
* If `true`, the actions stretch to fill the available space instead of hugging their content.
* @default false
*/
fullWidth?: boolean;
}
Loading
Loading