Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

Commit

Permalink
feat(UIKIT-1363,ConfirmAction): Реализован компонент подверждения дей…
Browse files Browse the repository at this point in the history
…ствий (#1144)
  • Loading branch information
mfrolov89 authored Sep 19, 2024
1 parent ae15979 commit 980c379
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 0 deletions.
118 changes: 118 additions & 0 deletions packages/components/src/ConfirmAction/ConfirmAction.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { BinOutlineMd } from '@astral/icons';

import { IconButton } from '../IconButton';

import { ConfirmAction } from './ConfirmAction';

/**
* Используется для подтверждения действия пользователя.
*
* ### [Figma](https://www.figma.com/design/3ghN4WjSgkKx5rETR64jqh/Sirius-Design-System-(%D0%90%D0%9A%D0%A2%D0%A3%D0%90%D0%9B%D0%AC%D0%9D%D0%9E)?node-id=30167-461154&node-type=frame&t=GgA8Lk5RPtTzJk3I-0)
* ### [Guide]()
*/
const meta: Meta<typeof ConfirmAction> = {
title: 'Components/ConfirmAction',
component: ConfirmAction,
};

export default meta;

type Story = StoryObj<typeof ConfirmAction>;

export const Interaction: Story = {
args: {
text: 'Уверены, что хотите отменить запрос на подписание?',
confirmButtonProps: {
text: 'Да, отменить запрос',
},
actionComponent: (props) => (
<IconButton variant="light" {...props}>
<BinOutlineMd />
</IconButton>
),
onConfirm: () => {},
},
parameters: {
docs: {
disable: true,
},
},
};

export const Example = () => {
return (
<ConfirmAction
actionComponent={(props) => (
<IconButton variant="light" {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => alert('Delete')}
/>
);
};

/**
* Пропс `text` позволяет добавить поясняющий текст
*/
export const WithText = () => {
return (
<ConfirmAction
text="Уверены, что хотите отменить запрос на подписание?"
confirmButtonProps={{
text: 'Да, отменить запрос',
}}
actionComponent={(props) => (
<IconButton variant="light" {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => alert('Delete')}
/>
);
};

/**
* При осуществлении важных действий, например при удалении, можно добавить акцент кнопке подтверждения
*/
export const AccentedConfirmationButton = () => {
return (
<ConfirmAction
text="Если вы удалите черновик, то черновик с такими же данными нужно будет создать заново. Удалить черновик из списка?"
confirmButtonProps={{
text: 'Да, удалить',
isAccented: true,
}}
actionComponent={(props) => (
<IconButton variant="light" {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => alert('Delete')}
/>
);
};

export const PopoverProps = () => {
return (
<ConfirmAction
actionComponent={(props) => (
<IconButton variant="light" {...props}>
<BinOutlineMd />
</IconButton>
)}
popoverProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'center',
},
transformOrigin: {
vertical: 'top',
horizontal: 'center',
},
}}
onConfirm={() => alert('Delete')}
/>
);
};
150 changes: 150 additions & 0 deletions packages/components/src/ConfirmAction/ConfirmAction.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it, vi } from 'vitest';
import { renderWithTheme, screen, userEvents } from '@astral/tests';
import { BinOutlineMd } from '@astral/icons';

import { IconButton } from '../IconButton';

import { ConfirmAction } from './ConfirmAction';

describe('ConfirmAction', () => {
it('Окно с подтверждением отображается при нажатии на кнопку действия', async () => {
renderWithTheme(
<ConfirmAction
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => {}}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const popover = screen.queryByRole('presentation');

expect(popover).toBeVisible();
});

it('Поясняющий текст отображается', async () => {
renderWithTheme(
<ConfirmAction
text="Text"
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => {}}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const text = screen.queryByText('Text');

expect(text).toBeVisible();
});

it('Кастомный текст кнопки подтверждения отображается', async () => {
renderWithTheme(
<ConfirmAction
confirmButtonProps={{
text: 'Да, удалить',
}}
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => {}}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const text = screen.queryByRole('button', { name: 'Да, удалить' });

expect(text).toBeVisible();
});

it('Окно с подтверждением закрывается при нажатии на кнопку отмены', async () => {
renderWithTheme(
<ConfirmAction
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => {}}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const cancelButton = screen.getByRole('button', { name: 'Отмена' });

await userEvents.click(cancelButton);

const popover = screen.queryByRole('presentation');

expect(popover).not.toBeInTheDocument();
});

it('Окно с подтверждением закрывается при нажатии на кнопку подтверждения', async () => {
renderWithTheme(
<ConfirmAction
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={() => {}}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const confirmButton = screen.getByRole('button', { name: 'Подтвердить' });

await userEvents.click(confirmButton);

const popover = screen.queryByRole('presentation');

expect(popover).not.toBeInTheDocument();
});

it('OnConfirm вызывается при нажатии на кнопку подтверждения', async () => {
const onConfirmSpy = vi.fn();

renderWithTheme(
<ConfirmAction
actionComponent={(props) => (
<IconButton {...props}>
<BinOutlineMd />
</IconButton>
)}
onConfirm={onConfirmSpy}
/>,
);

const button = screen.getByRole('button');

await userEvents.click(button);

const confirmButton = screen.getByRole('button', { name: 'Подтвердить' });

await userEvents.click(confirmButton);
expect(onConfirmSpy).toBeCalled();
});
});
97 changes: 97 additions & 0 deletions packages/components/src/ConfirmAction/ConfirmAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { type ReactNode, type SyntheticEvent } from 'react';

import { Button } from '../Button';
import { Popover, type PopoverProps } from '../Popover';

import { DEFAULT_ANCHOR_ORIGIN, DEFAULT_TRANSFORM_ORIGIN } from './constants';
import { useLogic } from './useLogic';
import { Actions, StyledTypography, Wrapper } from './styles';

type ActionComponentParams = {
onClick: (event: SyntheticEvent) => void;
};

export type ConfirmActionProps = {
/**
* Поясняющий текст
*/
text?: string;

/**
* Параметры кнопки подтверждения действия
*/
confirmButtonProps?: {
/**
* Текст кнопки
*/
text?: string;

/**
* Если `true`, кнопка будет иметь акцент на критичность действия. Стоит использовать для важных действий, например при удалении.
* @default 'false'
*/
isAccented?: boolean;
};

/**
* Параметры всплывающего окна
*/
popoverProps?: Pick<PopoverProps, 'anchorOrigin' | 'transformOrigin'>;

/**
* Кнопка, действие которой необходимо подтвердить
*/
actionComponent: (params: ActionComponentParams) => ReactNode;

/**
* Целевое действие, которое должно произойти после подтверждения
*/
onConfirm: () => void;
};

export const ConfirmAction = (props: ConfirmActionProps) => {
const {
actionComponentProps,
popoverProps,
cancelButtonProps,
confirmButtonProps,
} = useLogic(props);

const {
text,
confirmButtonProps: externalConfirmButtonProps,
popoverProps: externalPopoverProps,
actionComponent,
} = props;
const { text: confirmButtonText = 'Подтвердить' } =
externalConfirmButtonProps || {};

const {
anchorOrigin = DEFAULT_ANCHOR_ORIGIN,
transformOrigin = DEFAULT_TRANSFORM_ORIGIN,
} = externalPopoverProps || {};

return (
<>
{actionComponent(actionComponentProps)}

<Popover
anchorOrigin={anchorOrigin as PopoverProps['anchorOrigin']}
transformOrigin={transformOrigin as PopoverProps['transformOrigin']}
{...popoverProps}
>
<Wrapper>
{text && <StyledTypography>{text}</StyledTypography>}

<Actions>
<Button variant="text" {...cancelButtonProps}>
Отмена
</Button>

<Button {...confirmButtonProps}>{confirmButtonText}</Button>
</Actions>
</Wrapper>
</Popover>
</>
);
};
9 changes: 9 additions & 0 deletions packages/components/src/ConfirmAction/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const DEFAULT_ANCHOR_ORIGIN = {
vertical: 'bottom',
horizontal: 'right',
};

export const DEFAULT_TRANSFORM_ORIGIN = {
vertical: 'top',
horizontal: 'right',
};
1 change: 1 addition & 0 deletions packages/components/src/ConfirmAction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ConfirmAction';
17 changes: 17 additions & 0 deletions packages/components/src/ConfirmAction/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { styled } from '../styles';
import { Typography } from '../Typography';

export const Wrapper = styled.div`
max-width: 380px;
padding: ${({ theme }) => theme.spacing(4)};
`;

export const StyledTypography = styled(Typography)`
margin-bottom: ${({ theme }) => theme.spacing(3)};
`;

export const Actions = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(3)};
justify-content: end;
`;
1 change: 1 addition & 0 deletions packages/components/src/ConfirmAction/useLogic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useLogic';
Loading

0 comments on commit 980c379

Please sign in to comment.