Skip to content

Commit 94ad2d0

Browse files
authored
feat(Toaster): add support for rendering user component in actions (#2462)
1 parent 9511abb commit 94ad2d0

File tree

5 files changed

+35
-21
lines changed

5 files changed

+35
-21
lines changed

src/components/Toaster/README-ru.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const FoobarWithToaster = withToaster()(FoobarComponent);
109109
| content | `node` | | `undefined` | Содержимое уведомления. Оно может включать [все, что можно отобразить: числа, строки, элементы или массив](https://reactjs.org/docs/typechecking-with-proptypes.html#proptypes). |
110110
| theme | `string` | | `"normal"` | Тема уведомления. Возможные значения: `"normal"`, `"info"`, `"success"`, `"warning"`, `danger`, `"utility"`. Если для `theme` установить любое значение, кроме `"normal"`, в заголовок уведомления добавится иконка. _По умолчанию иконка отсутствует_. |
111111
| isClosable | `boolean` | | `true` | Конфигурация, управляющая отображением иконки X, которая позволяет пользователю закрыть уведомление. |
112-
| actions | `ToastAction[]` | | `undefined` | Массив [действий](./types.ts#L9), отображаемых после `content`. |
112+
| actions | `ToastAction[] \| () => ReactElement` | | `undefined` | Массив [действий](./types.ts#L9), отображаемых после `content`, или коллбек, возвращающий элемент React. |
113113
| renderIcon | `(toastProps: ToastProps) => ReactNode` | | `undefined` | Используется для кастомизации иконки тоста. По умолчанию используется поведение на основе типа. |
114114

115115
Каждое действие (`action`) — это объект со следующими параметрами:

src/components/Toaster/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ It accepts the `toastOptions` argument with the ongoing notification details:
111111
| content | `node` | | `undefined` | Notification content. This may be [anything that can be rendered: numbers, strings, elements, or an array](https://reactjs.org/docs/typechecking-with-proptypes.html#proptypes) |
112112
| theme | `string` | | `"normal"` | Notification theme. The possible values are `"normal"`, `"info"`, `"success"`, `"warning"`, `danger`, and `"utility"`. If `theme` is set to anything but `"normal"`, the icon will be added into the notification title. _By default, there is no icon_. |
113113
| isClosable | `boolean` | | `true` | Configuration that manages the visibility of the X icon, which allows the user to close the notification |
114-
| actions | `ToastAction[]` | | `undefined` | Array of [actions](./types.ts#L9) that are displayed after `content` |
114+
| actions | `ToastAction[] \| () => ReactElement` | | `undefined` | Array of [actions](./types.ts#L9) that are displayed after `content`, or callback, that returned required actions |
115115
| renderIcon | `(toastProps: ToastProps) => ReactNode` | | `undefined` | Used to customize the toast icon. Type-based behavior is used by default |
116116

117117
Every `action` is an object with following parameters:

src/components/Toaster/Toast/Toast.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@ interface ToastInnerProps {
3333
interface ToastUnitedProps extends InternalToastProps, ToastInnerProps {}
3434

3535
interface RenderActionsProps {
36-
actions?: ToastAction[];
36+
actions?: (() => React.ReactElement) | ToastAction[];
3737
onClose: VoidFunction;
3838
}
3939

4040
function renderActions({actions, onClose}: RenderActionsProps) {
41-
if (!actions || !actions.length) {
42-
return null;
43-
}
41+
let component: React.ReactElement | React.ReactElement[] | undefined;
4442

45-
return (
46-
<div className={b('actions')}>
47-
{actions.map(({label, onClick, view = 'outlined', removeAfterClick = true}, index) => {
43+
if (Array.isArray(actions)) {
44+
component = actions.map(
45+
({label, onClick, view = 'outlined', removeAfterClick = true}, index) => {
4846
const onActionClick = () => {
4947
onClick();
5048
if (removeAfterClick) {
@@ -55,7 +53,6 @@ function renderActions({actions, onClose}: RenderActionsProps) {
5553
return (
5654
<Button
5755
key={`${label}__${index}`}
58-
className={b('action')}
5956
onClick={onActionClick}
6057
type="button"
6158
size="l"
@@ -65,9 +62,21 @@ function renderActions({actions, onClose}: RenderActionsProps) {
6562
{label}
6663
</Button>
6764
);
68-
})}
69-
</div>
70-
);
65+
},
66+
);
67+
68+
if (!actions.length) {
69+
return null;
70+
}
71+
} else {
72+
component = actions?.();
73+
74+
if (!component) {
75+
return null;
76+
}
77+
}
78+
79+
return <div className={b('actions')}>{component}</div>;
7180
}
7281

7382
interface RenderIconProps {

src/components/Toaster/__tests__/Toast.visual.test.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ import type * as React from 'react';
33
import {test} from '~playwright/core';
44

55
import type {Toast} from '../Toast/Toast';
6+
import type {ToastAction} from '../types';
67

78
import {ToastStories} from './helpersPlaywright';
89

910
const wrapperOptions = {
1011
width: 312,
1112
};
1213

13-
function getToastActions(
14+
function getToastActions({
1415
contrastButton = true,
15-
): Required<React.ComponentProps<typeof Toast>>['actions'] {
16+
firstLabel = 'Action',
17+
}: {
18+
contrastButton?: boolean;
19+
firstLabel?: string;
20+
} = {}): ToastAction[] {
1621
return [
17-
{onClick() {}, label: 'Action', view: contrastButton ? 'normal-contrast' : 'normal'},
22+
{onClick() {}, label: firstLabel, view: contrastButton ? 'normal-contrast' : 'normal'},
1823
{onClick() {}, label: 'Something More', view: 'outlined'},
1924
];
2025
}
@@ -33,7 +38,7 @@ test.describe('Toast', {tag: '@Toaster'}, () => {
3338
await mount(
3439
<ToastStories.ToastPlayground
3540
{...simpleToastProps}
36-
actions={getToastActions(false)}
41+
actions={getToastActions({contrastButton: false})}
3742
theme="normal"
3843
/>,
3944
wrapperOptions,
@@ -88,9 +93,9 @@ test.describe('Toast', {tag: '@Toaster'}, () => {
8893
});
8994

9095
test('actions wrap', async ({mount, expectScreenshot}) => {
91-
const actions = getToastActions();
92-
93-
actions[0].label = 'Really long button text that cause buttons to wrap';
96+
const actions = getToastActions({
97+
firstLabel: 'Really long button text that cause buttons to wrap',
98+
});
9499

95100
await mount(
96101
<ToastStories.ToastPlayground

src/components/Toaster/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type ToastProps = {
2424
content?: React.ReactNode;
2525
theme?: ToastTheme;
2626
isClosable?: boolean;
27-
actions?: ToastAction[];
27+
actions?: (() => React.ReactElement) | ToastAction[];
2828

2929
onClose?: () => void;
3030

0 commit comments

Comments
 (0)