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..bb27f315e --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.api.mdx @@ -0,0 +1,37 @@ +import { Meta } from '@storybook/addon-docs'; +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 or style overrides in the theme. + +## Props + + + +
+ +## CSS + + + +
+ +## Demos + + diff --git a/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.classes.ts b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.classes.ts new file mode 100644 index 000000000..7e9268e62 --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.classes.ts @@ -0,0 +1,18 @@ +import { generateUtilityClass, generateUtilityClasses } from '@mui/material'; + +export type ConfirmationDialogClasses = { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the icon element. */ + icon: string; +}; +export type ConfirmationDialogClassKey = keyof ConfirmationDialogClasses; + +export function getConfirmationDialogUtilityClass(slot: string): string { + return generateUtilityClass('ESConfirmationDialog', slot); +} + +export const confirmationDialogClasses: ConfirmationDialogClasses = generateUtilityClasses('ESConfirmationDialog', [ + 'root', + 'icon', +]); 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..c513b919e --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx @@ -0,0 +1,91 @@ +import { ComponentProps } from 'react'; + +import { Meta, StoryContext, StoryObj } from '@storybook/react'; + +import { Typography } from '@mui/material'; + +import { ConfirmationDialog } from '.'; + +import { Button } from '../Button'; +import { DialogClose } from '../Dialog'; +import { useDialogStack } from '../DialogStack'; + +type Args = ComponentProps; + +const getOpenButtonText = (context: StoryContext) => { + return context.globals.locale === 'en' ? 'Open dialog window' : 'Открыть диалоговое окно'; +}; + +const getHeadingText = (context: StoryContext) => { + return context.globals.locale === 'en' ? 'Are you confirm?' : 'Вы подтверждаете согласие?'; +}; + +const getCancelButtonText = (context: StoryContext) => { + return context.globals.locale === 'en' ? 'Cancel' : 'Отмена'; +}; + +const getConfirmButtonText = (context: StoryContext) => { + return context.globals.locale === 'en' ? 'Confirm' : 'Подтвердить'; +}; + +const meta: Meta = { + tags: ['autodocs'], + component: ConfirmationDialog, + parameters: { + references: ['ConfirmationDialog'], + }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + }, + args: { + severity: 'primary', + icon: true, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Demo: Story = { + render: function Render(args, context) { + const dialogStack = useDialogStack(); + + const onOpen = () => { + dialogStack + .open(({ close }) => ( + Promise.resolve(true)} + align="center" + before={ close()} />} + close={() => close()} + icon={args.icon ? undefined : false} + labelCancel={getCancelButtonText(context)} + labelConfirm={getConfirmButtonText(context)} + maxWidth="700px" + severity={args.severity} + title={getHeadingText(context)} + > + + 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. + + + )) + .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..35c2e048a --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,225 @@ +import { forwardRef, useState } from 'react'; + +import { ConfirmationDialogProps } from './ConfirmationDialog.types'; + +import clsx from 'clsx'; +import { getConfirmationDialogUtilityClass } from './ConfirmationDialog.classes'; + +import { unstable_composeClasses as composeClasses } from '@mui/base'; + +import { styled, useThemeProps } from '@mui/material/styles'; +import { buttonBaseClasses } from '@mui/material'; +// eslint-disable-next-line no-restricted-imports +import IconButton from '@mui/material/IconButton'; + +import { IconAlertW500, IconInformation2W500 } from '../../icons'; +import { Button } from '../Button'; +import { Dialog, DialogActions, DialogContent, DialogTitle } from '../Dialog'; +import { LoadingButton } from '../LoadingButton'; +import { svgIconClasses } from '../SvgIcon'; + +type ConfirmationDialogOwnerState = { + classes?: ConfirmationDialogProps['classes']; +}; + +const useUtilityClasses = (ownerState: ConfirmationDialogOwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['root'], + icon: ['icon'], + }; + + return composeClasses(slots, getConfirmationDialogUtilityClass, classes); +}; + +const ConfirmationDialogRoot = styled(Dialog, { + name: 'ESConfirmationDialog', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})(() => ({ + '& .ESDialog-paper': { + position: 'relative', + borderRadius: '16px', + height: '100%', + }, +})); + +const ConfirmationDialogTitle = styled(DialogTitle, { + name: 'ESConfirmationDialog', + slot: 'Title', + overridesResolver: (props, styles) => styles.title, +})<{ ownerState: { severity: 'error' | 'primary' } }>(({ theme, ownerState }) => ({ + padding: '52px 8px 12px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + gap: '12px', + + [theme.breakpoints.up('tabletXS')]: { + gap: '8px', + padding: '22px 24px', + flexDirection: 'row', + }, + [`& .${svgIconClasses.root}`]: { + color: `${theme.vars.palette[ownerState.severity][300]}`, + + [theme.breakpoints.down('tabletXS')]: { + width: '56px !important', + height: '56px !important', + }, + }, +})); + +const ConfirmationDialogIcon = styled('div', { + name: 'ESConfirmationDialog', + slot: 'Icon', + overridesResolver: (props, styles) => styles.icon, +})(() => ({ + paddingTop: '8px', + marginRight: '8px', +})); + +const ConfirmationDialogContent = styled(DialogContent, { + name: 'ESConfirmationDialog', + slot: 'Content', + overridesResolver: (props, styles) => styles.content, +})(({ theme }) => ({ + padding: '0 16px', + color: theme.vars.palette.monoA.A800, + textAlign: 'center', + + [theme.breakpoints.up('tabletXS')]: { + padding: '0 24px', + textAlign: 'left', + }, +})); + +const ConfirmationDialogActions = styled(DialogActions, { + name: 'ESConfirmationDialog', + slot: 'Actions', + overridesResolver: (props, styles) => styles.actions, +})(({ theme }) => ({ + padding: '16px', + + [theme.breakpoints.up('tabletXS')]: { + padding: '24px', + }, + + [`.${buttonBaseClasses.root}`]: { + width: '100%', + + [theme.breakpoints.up('tabletXS')]: { + width: 'auto', + padding: '0 16px', + }, + }, +})); + +const ConfirmationDialogClose = styled(IconButton)(() => ({ + position: 'absolute', + right: '8px', + top: '8px', +})); + +const defaultIconMapping = { + error: , + primary: , +}; + +/** + * ConfirmationDialog asks user to approve requested operation. + */ + +export const ConfirmationDialog = forwardRef( + function Dialog(inProps, ref) { + const { + children, + disabled, + icon, + iconClose, + labelCancel, + labelConfirm, + className, + severity = 'primary', + action, + close, + open, + title, + iconMapping = defaultIconMapping, + ...props + } = useThemeProps({ + props: inProps, + name: 'ESConfirmationDialog', + }); + + const [isSending, setSending] = useState(false); + const ownerState = { + ...props, + severity, + }; + + const onConfirm = () => { + setSending(true); + + action() + .then((data) => { + close(data); + }) + .catch((error) => { + setSending(false); + return Promise.reject(error); + }); + }; + + const onClose = () => { + if (!isSending) { + close(); + } + }; + + const classes = useUtilityClasses(ownerState); + + return ( + + {iconClose && ( + + {iconClose} + + )} + + + {icon !== false && ( + {icon || iconMapping[severity]} + )} + {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..e09459538 --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/ConfirmationDialog.types.ts @@ -0,0 +1,42 @@ +import { ReactNode } from 'react'; + +import { ConfirmationDialogClasses } from './ConfirmationDialog.classes'; + +import { DialogProps } from '../Dialog'; + +export interface ConfirmationDialogProps extends DialogProps { + /** Override or extend the styles applied to the component. */ + classes?: Partial; + /** Confirmation dialog children, usually the included sub-components. */ + children?: React.ReactNode; + /** The icon displayed before the title. */ + icon?: ReactNode; + /** The icon for closing the modal. */ + iconClose?: ReactNode; + /** + * The severity of the confirmation dialog. This defines the color and icon used. + * @default 'primary' + */ + severity?: 'error' | 'primary'; + /** The title text. */ + title: ReactNode; + /** Text for the confirm buttony. */ + labelConfirm?: ReactNode; + /** Text for the cancel buttony. */ + labelCancel?: ReactNode; + /** If true, the button is disabled. */ + disabled?: boolean; + /** + * Callback fired when the user clicks the button. + */ + action: () => Promise; + /** + * Callback fired when the component requests to be closed. + */ + close: (data?: any) => void; + /** + * The component maps the severity prop to a range of different icons. + * If you wish to change this mapping, you can provide your own. + */ + iconMapping?: Record<'error' | 'primary', ReactNode>; +} diff --git a/packages/react/src/components/ConfirmationDialog/index.ts b/packages/react/src/components/ConfirmationDialog/index.ts new file mode 100644 index 000000000..61f201046 --- /dev/null +++ b/packages/react/src/components/ConfirmationDialog/index.ts @@ -0,0 +1,7 @@ +export { ConfirmationDialog } from './ConfirmationDialog'; +export { + ConfirmationDialogClasses, + confirmationDialogClasses, + ConfirmationDialogClassKey, +} from './ConfirmationDialog.classes'; +export { ConfirmationDialogProps } from './ConfirmationDialog.types'; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 21e6de9e8..5ce589be6 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -15,6 +15,7 @@ export * from './Button'; export * from './ButtonBase'; export * from './Calendar'; export * from './Checkbox'; +export * from './ConfirmationDialog'; export * from './DateAdapter'; export * from './Dialog'; export * from './DialogStack'; diff --git a/packages/react/src/overrides.d.ts b/packages/react/src/overrides.d.ts index 4ded9188a..9f4ef6832 100644 --- a/packages/react/src/overrides.d.ts +++ b/packages/react/src/overrides.d.ts @@ -71,6 +71,7 @@ import { CalendarProps, } from './components/Calendar'; import { CheckboxClassKey, CheckboxProps, CheckboxIconClassKey, CheckboxIconProps } from './components/Checkbox'; +import { ConfirmationDialog } from './components/ConfirmationDialog'; import { DialogActionsClassKey, DialogActionsProps, @@ -361,6 +362,7 @@ declare module '@mui/material/styles/props' { ESCalendarHead: CalendarHeadProps; ESCheckbox: CheckboxProps; ESCheckboxIcon: CheckboxIconProps; + ESConfirmationDialog: ConfirmationDialogProps; ESDialog: DialogProps; ESDialogActions: DialogActionsProps; ESDialogArrow: DialogArrowProps; @@ -497,6 +499,7 @@ declare module '@mui/material/styles/overrides' { ESCalendarHead: CalendarHeadClassKey; ESCheckbox: CheckboxClassKey; ESCheckboxIcon: CheckboxIconClassKey; + ESConfirmationDialog: ConfirmationDialogClassKey; ESDialog: DialogClassKey; ESDialogActions: DialogActionsClassKey; ESDialogArrow: DialogArrowClassKey; @@ -716,6 +719,10 @@ declare module '@mui/material/styles/components' { defaultProps?: ComponentsProps['ESCheckboxIcon']; styleOverrides?: ComponentsOverrides['ESCheckboxIcon']; }; + ESConfirmationDialog?: { + defaultProps?: ComponentsProps['ESConfirmationDialog']; + styleOverrides?: ComponentsOverrides['ESConfirmationDialog']; + }; ESDialog?: { defaultProps?: ComponentsProps['ESDialog']; styleOverrides?: ComponentsOverrides['ESDialog'];