diff --git a/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.api.mdx b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.api.mdx
new file mode 100644
index 000000000..a61416577
--- /dev/null
+++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.api.mdx
@@ -0,0 +1,31 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+import LinkTo from '@storybook/addon-links/react';
+import { TableInterface } from '~storybook/components/TableInterface';
+
+
+
+# ConfirmationDialog API
+
+```js
+import { ConfirmationDialog } from '@esfront/react';
+```
+
+## Component name
+
+The name `ESConfirmationDialog` can be used when providing default props.
+
+## Props
+
+
+
+
+
+## Demos
+
+
+ -
+
+
ConfirmationDialog
+
+
+
diff --git a/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx
new file mode 100644
index 000000000..e6b718b4a
--- /dev/null
+++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx
@@ -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) => {
+ return context.globals.locale === 'en' ? 'Open dialog window' : 'Открыть диалоговое окно';
+};
+
+const getHeadingText = (context: StoryContext) => {
+ return context.globals.locale === 'en' ? 'Close without saving?' : 'Закрыть без сохранения?';
+};
+
+const getContentText = (context: StoryContext) => {
+ return context.globals.locale === 'en'
+ ? 'The changes you have made will not be saved'
+ : 'Внесённые изменения не сохранятся';
+};
+
+const getCancelButtonText = (context: StoryContext) => {
+ return context.globals.locale === 'en' ? 'Cancel' : 'Отменить';
+};
+
+const getConfirmButtonText = (context: StoryContext) => {
+ return context.globals.locale === 'en' ? 'Close' : 'Закрыть';
+};
+
+const getCloseText = (context: StoryContext) => {
+ return context.globals.locale === 'en' ? 'Close' : 'Закрыть';
+};
+
+type Args = ComponentProps;
+
+const meta: Meta = {
+ 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;
+
+export const Demo: Story = {
+ render: function Render(args, context) {
+ const dialogStack = useDialogStackV2();
+
+ const onOpen = () => {
+ dialogStack
+ .open(({ close }) => (
+
+ 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)}
+
+ ))
+ .afterClosed.then((data) => {
+ console.info(data);
+ });
+ };
+
+ return (
+
+ );
+ },
+};
diff --git a/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.tsx
new file mode 100644
index 000000000..4427e7b6b
--- /dev/null
+++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.tsx
@@ -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 = {
+ error: ,
+ primary: ,
+};
+
+/**
+ * ConfirmationDialog asks the user to approve an operation before it is performed.
+ */
+export const ConfirmationDialog = ({ ref, ...inProps }: ConfirmationDialogProps & RefAttributes) => {
+ const {
+ children,
+ className,
+ severity = 'error',
+ title,
+ icon,
+ iconClose = ,
+ 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 (
+ }
+ className={clsx('es-confirmation-dialog', `es-confirmation-dialog--severity--${severity}`, className)}
+ fullWidth={fullWidth}
+ maxWidth={maxWidth}
+ onClose={onClose}
+ {...props}
+ >
+
+
+ {title}
+
+ {children}
+
+
+ {!!labelConfirm && (
+
+ {labelConfirm}
+
+ )}
+
+
+ );
+};
diff --git a/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.types.ts b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.types.ts
new file mode 100644
index 000000000..8e6869775
--- /dev/null
+++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.types.ts
@@ -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 {
+ 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;
+ /** 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>;
+
+ /** 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;
+}
diff --git a/packages/react/src/components/ConfirmationDialog/index.ts b/packages/react/src/components/ConfirmationDialog/index.ts
new file mode 100644
index 000000000..9e5a9b9c3
--- /dev/null
+++ b/packages/react/src/components/ConfirmationDialog/index.ts
@@ -0,0 +1,6 @@
+export { ConfirmationDialog } from './ConfirmationDialog';
+export type {
+ ConfirmationDialogProps,
+ ConfirmationDialogPropsSeverityOverrides,
+ ConfirmationDialogSeverity,
+} from './ConfirmationDialog.types';
diff --git a/packages/react/src/components/Dialog/DialogActions/DialogActions.tsx b/packages/react/src/components/Dialog/DialogActions/DialogActions.tsx
index 44f64acc4..1885aff6c 100644
--- a/packages/react/src/components/Dialog/DialogActions/DialogActions.tsx
+++ b/packages/react/src/components/Dialog/DialogActions/DialogActions.tsx
@@ -13,7 +13,14 @@ import { useDefaultProps } from '../../../theming';
* @see `Dialog`
*/
export const DialogActions = ({ ref, ...inProps }: DialogActionsProps & RefAttributes) => {
- const { className, style, sticky, children } = useDefaultProps({
+ const {
+ className,
+ style,
+ sticky,
+ direction = 'horizontal',
+ fullWidth = false,
+ children,
+ } = useDefaultProps({
props: inProps,
name: 'ESDialogActions',
});
@@ -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
diff --git a/packages/react/src/components/Dialog/DialogActions/DialogActions.types.ts b/packages/react/src/components/Dialog/DialogActions/DialogActions.types.ts
index cb5c44edf..6b3486ce1 100644
--- a/packages/react/src/components/Dialog/DialogActions/DialogActions.types.ts
+++ b/packages/react/src/components/Dialog/DialogActions/DialogActions.types.ts
@@ -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;
}
diff --git a/packages/react/src/components/Dialog/DialogTitle/DialogTitle.tsx b/packages/react/src/components/Dialog/DialogTitle/DialogTitle.tsx
index 3e3e91ca5..2d35667d9 100644
--- a/packages/react/src/components/Dialog/DialogTitle/DialogTitle.tsx
+++ b/packages/react/src/components/Dialog/DialogTitle/DialogTitle.tsx
@@ -13,7 +13,14 @@ import { useDefaultProps } from '../../../theming';
* @see `Dialog`
*/
export const DialogTitle = ({ ref, ...inProps }: DialogTitleProps & RefAttributes) => {
- const { className, style, sticky, children } = useDefaultProps({
+ const {
+ className,
+ style,
+ sticky,
+ icon,
+ align = 'start',
+ children,
+ } = useDefaultProps({
props: inProps,
name: 'ESDialogTitle',
});
@@ -27,6 +34,7 @@ export const DialogTitle = ({ ref, ...inProps }: DialogTitleProps & RefAttribute
ref={ref}
className={clsx(
'es-dialog-title',
+ `es-dialog-title--align--${align}`,
sticky && 'es-dialog-title--sticky',
stuck && 'es-dialog-title--stuck',
'h4',
@@ -34,7 +42,8 @@ export const DialogTitle = ({ ref, ...inProps }: DialogTitleProps & RefAttribute
)}
style={style}
>
- {children}
+ {!!icon && {icon}
}
+ {children}
>
);
diff --git a/packages/react/src/components/Dialog/DialogTitle/DialogTitle.types.ts b/packages/react/src/components/Dialog/DialogTitle/DialogTitle.types.ts
index d2d039a28..6816d6c4c 100644
--- a/packages/react/src/components/Dialog/DialogTitle/DialogTitle.types.ts
+++ b/packages/react/src/components/Dialog/DialogTitle/DialogTitle.types.ts
@@ -9,4 +9,11 @@ export interface DialogTitleProps {
style?: CSSProperties;
/** Whether the title should be sticky. */
sticky?: boolean;
+ /** The icon displayed next to the title. */
+ icon?: ReactNode;
+ /**
+ * The title layout. `center` stacks the icon above the text and centers both.
+ * @default 'start'
+ */
+ align?: 'start' | 'center';
}
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index b4a0e2c76..8d40caad0 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -21,6 +21,7 @@ export * from './Chips';
export * from './ClearButton';
export * from './ClickAwayListener';
export * from './Collapse';
+export * from './ConfirmationDialog';
export * from './DateAdapter';
export * from './DateInput';
export * from './Dialog';
diff --git a/packages/react/src/overrides.ts b/packages/react/src/overrides.ts
index 7983554e5..9dcf009f2 100644
--- a/packages/react/src/overrides.ts
+++ b/packages/react/src/overrides.ts
@@ -24,6 +24,7 @@ import { ChipsProps } from './components/Chips';
import { ClearButtonProps } from './components/ClearButton';
import { ClickAwayListenerProps } from './components/ClickAwayListener';
import { CollapseProps } from './components/Collapse';
+import { ConfirmationDialogProps } from './components/ConfirmationDialog';
import { DateInputProps } from './components/DateInput';
import {
DialogActionsProps,
@@ -215,6 +216,7 @@ declare module './theming/DefaultPropsProvider/DefaultPropsProvider.types' {
ESClearButton: ClearButtonProps;
ESClickAwayListener: ClickAwayListenerProps;
ESCollapse: CollapseProps;
+ ESConfirmationDialog: ConfirmationDialogProps;
ESDateInput: DateInputProps;
ESDialog: DialogProps;
ESDialogActions: DialogActionsProps;
diff --git a/packages/theme/lib/components/_confirmation-dialog.scss b/packages/theme/lib/components/_confirmation-dialog.scss
new file mode 100644
index 000000000..c3dac8141
--- /dev/null
+++ b/packages/theme/lib/components/_confirmation-dialog.scss
@@ -0,0 +1,85 @@
+@use '../helpers/breakpoints';
+
+@mixin include() {
+ .es-confirmation-dialog {
+ &--severity--error .es-dialog-title__icon {
+ color: var(--es-error-300);
+ }
+
+ &--severity--primary .es-dialog-title__icon {
+ color: var(--es-primary-300);
+ }
+
+ & .es-dialog__paper {
+ position: relative;
+ }
+
+ & .es-dialog-close {
+ display: none;
+
+ @include breakpoints.up('sm') {
+ display: flex;
+ }
+ }
+
+ &__close {
+ --icon: var(--es-mono-a-a700);
+
+ position: absolute;
+ right: 8px;
+ top: 8px;
+ z-index: 1;
+
+ @include breakpoints.up('sm') {
+ display: none;
+ }
+ }
+
+ & .es-dialog-title {
+ padding: 32px 16px 12px;
+
+ @include breakpoints.up('sm') {
+ align-items: center;
+ flex-direction: row;
+ gap: 8px;
+ padding: 22px 24px 14px;
+ text-align: left;
+ }
+ }
+
+ & .es-dialog-title__icon .es-svg-icon {
+ height: 56px !important;
+ width: 56px !important;
+
+ @include breakpoints.up('sm') {
+ height: 24px !important;
+ width: 24px !important;
+ }
+ }
+
+ & .es-dialog-content {
+ color: var(--es-mono-a-a800);
+ padding: 0 16px;
+ text-align: center;
+
+ @include breakpoints.up('sm') {
+ padding: 0 24px;
+ text-align: left;
+ }
+ }
+
+ & .es-dialog-actions {
+ padding: 20px 16px 16px;
+
+ @include breakpoints.up('sm') {
+ padding: 16px 24px 24px;
+ }
+
+ & > * {
+ @include breakpoints.up('sm') {
+ flex: 0 0 auto;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/theme/lib/components/_dialog.scss b/packages/theme/lib/components/_dialog.scss
index a14bd0563..a28806abb 100644
--- a/packages/theme/lib/components/_dialog.scss
+++ b/packages/theme/lib/components/_dialog.scss
@@ -123,11 +123,22 @@
.es-dialog-actions {
display: flex;
+ gap: 16px;
justify-content: flex-end;
padding: 16px 24px 24px;
- & > :not(:first-of-type) {
- margin-left: 16px;
+ &--direction--vertical {
+ flex-direction: column;
+ }
+
+ &--full-width > * {
+ flex: 1 0 0;
+ min-width: 0;
+ }
+
+ &--direction--vertical.es-dialog-actions--full-width > * {
+ flex: 0 0 auto;
+ width: 100%;
}
&--sticky {
@@ -251,9 +262,35 @@
.es-dialog-title {
color: var(--es-mono-a-a900);
+ display: flex;
padding: 24px;
padding-bottom: 6px;
+ &--align--start {
+ align-items: center;
+ gap: 8px;
+
+ & > .es-dialog-title__text {
+ flex: 1 0 0;
+ min-width: 0;
+ }
+ }
+
+ &--align--center {
+ align-items: center;
+ flex-direction: column;
+ gap: 12px;
+ text-align: center;
+
+ & > .es-dialog-title__text {
+ width: 100%;
+ }
+ }
+
+ &__icon {
+ display: flex;
+ }
+
&--sticky {
background-color: var(--es-surface-600);
border-top-left-radius: 8px;
diff --git a/packages/theme/lib/components/_index.scss b/packages/theme/lib/components/_index.scss
index 784731912..8bf9d551e 100644
--- a/packages/theme/lib/components/_index.scss
+++ b/packages/theme/lib/components/_index.scss
@@ -19,6 +19,7 @@
@use './chips';
@use './clear-button';
@use './collapse';
+@use './confirmation-dialog';
@use './date-input';
@use './dialog';
@use './divider';
@@ -102,6 +103,7 @@
@include chips.include;
@include clear-button.include;
@include collapse.include;
+ @include confirmation-dialog.include;
@include date-input.include;
@include dialog.include;
@include divider.include;