Skip to content

Commit

Permalink
feat(SingleToast): add new component (#3314)
Browse files Browse the repository at this point in the history
  • Loading branch information
JackUait authored and zhzz committed Dec 4, 2023
1 parent 2b3ae3a commit 1daf55b
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 112 deletions.
35 changes: 35 additions & 0 deletions packages/react-ui/components/SingleToast/SingleToast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
`<SingleToast>` можно добавить в единственном месте в проекте, а статические методы будут всегда использовать последний отрендеренный экземпляр `<Toast>`.

_На внешний вид этого примера влияет следующий пример_

```jsx harmony
import { Button, SingleToast } from '@skbkontur/react-ui';

<div>
<SingleToast />
<Button onClick={() => SingleToast.push('Статический тост')}>
Показать статический тост
</Button>
</div>
```

`<SingleToast>` можно кастомизировать с помощью переменных темы для `toast`.
```jsx harmony
import { Button, SingleToast, ThemeContext, ThemeFactory, THEME_2022 } from '@skbkontur/react-ui';

const rand = () => "Пример жёлтого тоста № " + Math.round(Math.random() * 100).toString();

const pushToast = () => {
SingleToast.push(rand(), {
label: "Cancel",
handler: () => SingleToast.push("Canceled")
});
};

<div>
<SingleToast theme={{ toastBg: '#f1c40f' }} />
<Button onClick={pushToast}>
Показать тост с жёлтым фоном
</Button>
</div>
```
21 changes: 21 additions & 0 deletions packages/react-ui/components/SingleToast/SingleToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';

import { Toast, ToastProps } from '../Toast/Toast';

/**
* Позволяет вызывать тосты с помощью статических методов. В отличие от статических методов из компонента `<Toast>` - их можно кастомизировать и они работают с `React@18`.
*/
export class SingleToast extends React.Component<ToastProps> {
public static ref = React.createRef<Toast>();
public static push: typeof Toast.push = (...args) => {
SingleToast.close();
SingleToast.ref.current?.push(...args);
};
public static close: typeof Toast.close = () => {
ReactDOM.flushSync(() => SingleToast.ref.current?.close());
};
render = () => {
return <Toast ref={SingleToast.ref} {...this.props} />;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { LangCodes } from '../../../lib/locale';
import { Button } from '../../Button';
import { SingleToast } from '../SingleToast';
import { ToastDataTids } from '../../Toast';
import { ToastLocaleHelper } from '../../Toast/locale';

describe('ToastView', () => {
describe('a11y', () => {
it('has correct aria-label on close button', () => {
function showComplexNotification() {
SingleToast.push(
'Successfully saved',
{
label: 'Cancel',
handler: () => SingleToast.push('Canceled'),
},
15000,
);
}
render(
<>
<SingleToast />
<Button onClick={showComplexNotification}>Show notification</Button>
</>,
);

userEvent.click(screen.getByRole('button'));

expect(screen.getByTestId(ToastDataTids.close)).toHaveAttribute(
'aria-label',
ToastLocaleHelper.get(LangCodes.ru_RU).closeButtonAriaLabel,
);
});

it('sets value for aria-label attribute (action button)', async () => {
const ariaLabel = 'aria-label';
const buttonName = 'button';
function showComplexNotification() {
SingleToast.push(
'Successfully saved',
{
label: 'Cancel',
handler: () => SingleToast.push('Canceled'),
'aria-label': ariaLabel,
},
15000,
);
}
render(
<>
<SingleToast />
<Button onClick={showComplexNotification}>{buttonName}</Button>
</>,
);

userEvent.click(screen.getByRole('button', { name: buttonName }));

expect(screen.getByTestId(ToastDataTids.action)).toHaveAttribute('aria-label', ariaLabel);
});
});
});
1 change: 1 addition & 0 deletions packages/react-ui/components/SingleToast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './SingleToast';
105 changes: 0 additions & 105 deletions packages/react-ui/components/Toast/Toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,108 +67,3 @@ class Toaster extends React.Component {

<Toaster />;
```

### SuperToast

Вы можете объединить удобство статических методов и кастамизируемость классического способа через `ref`.
Для этого можно добавить обёртку, которая позволяет Toast работать по примеру GlobalLoader.

Т.е. кастомный Toast можно добавить в единственном месте в проекте, а статические методы будут всегда использовать последний отрендеренный экземпляр Toast:

Также в обёртке можно изменить логику появления всплывашки, по рекомендации Гайдов:
https://guides.kontur.ru/components/toast/#08
```static
«Всегда показывается только 1 тост. Перед появлением следующего тоста, текущий скрывается, даже если время его показа еще не истекло»
```

Для этого немного изменим метод `SuperToast.close` с помощью специального метода `ReactDOM.flushSync`.

Итоговый вариант:
```js static
const SuperToast = (props) => <Toast ref={SuperToast.ref} {...props} />;
SuperToast.ref = React.createRef();
SuperToast.push = (...args) => {
SuperToast.close();
SuperToast.ref.current && SuperToast.ref.current.push(...args);
};
SuperToast.close = () => {
ReactDOM.flushSync(() => {
SuperToast.ref.current && SuperToast.ref.current.close();
});
};

```


Версия на typescript:
```typescript static
class SuperToast extends React.Component<ToastProps> {
public static ref = React.createRef<Toast>();
public static push: typeof Toast.push = (...args) => {
SuperToast.close();
SuperToast.ref.current?.push(...args);
};
public static close: typeof Toast.close = () => {
ReactDOM.flushSync(() => SuperToast.ref.current?.close());
};
render = () => {
return <Toast ref={SuperToast.ref} {...this.props} />;
};
}
```


```jsx harmony
import { Button, Toast, ThemeContext, ThemeFactory, THEME_2022 } from '@skbkontur/react-ui';
import ReactDOM from 'react-dom';

const SuperToast = (props) => <Toast ref={SuperToast.ref} {...props} />;
SuperToast.ref = React.createRef();
SuperToast.push = (...args) => {
SuperToast.close();
SuperToast.ref.current && SuperToast.ref.current.push(...args);
};
SuperToast.close = () => {
ReactDOM.flushSync(() => {
SuperToast.ref.current && SuperToast.ref.current.close();
});
};

const RedToast = () => (
<ThemeContext.Consumer>
{(theme) => {
return <ThemeContext.Provider
value={ThemeFactory.create(
{
toastBg: "rgba(210, 15, 0, 0.76)",
toastLinkBgActive: "rgba(210, 15, 0, 0.76)"
},
theme
)}
>
<SuperToast />
</ThemeContext.Provider>
}}
</ThemeContext.Consumer>
);

const rand = () => "Пример красного тоста №" + Math.round(Math.random() * 100).toString();

const push = () => {
SuperToast.push(rand(), {
label: "Cancel",
handler: () => SuperToast.push("Canceled")
});
};

<div>
<RedToast />
<span />
<span />
<span />
<Button onClick={push}>
Show super red toast
</Button>
</div>
;
```
30 changes: 24 additions & 6 deletions packages/react-ui/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import React, { AriaAttributes } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { globalObject, SafeTimer } from '@skbkontur/global-object';

import { ThemeFactory } from '../../lib/theming/ThemeFactory';
import { ThemeContext } from '../../lib/theming/ThemeContext';
import { Theme, ThemeIn } from '../../lib/theming/Theme';
import { RenderContainer } from '../../internal/RenderContainer';
import { Nullable } from '../../typings/utility-types';
import { CommonProps, CommonWrapper } from '../../internal/CommonWrapper';
Expand All @@ -28,6 +31,11 @@ export interface ToastState {
export interface ToastProps extends Pick<AriaAttributes, 'aria-label'>, CommonProps {
onPush?: (notification: string, action?: Action) => void;
onClose?: (notification: string, action?: Action) => void;
/**
* Обычный объект с переменными темы.
* Он будет объединён с темой из контекста.
*/
theme?: ThemeIn;
}

export const ToastDataTids = {
Expand All @@ -41,15 +49,16 @@ export const ToastDataTids = {
* Показывает уведомления.
*
* Доступен статический метод: `Toast.push(notification, action?, showTime?)`.
* Однако, при его использовании не работает кастомизация и могут быть проблемы
* с перекрытием уведомления другими элементами страницы.
* Однако, при его использовании не работает кастомизация, они не поддерживаются в `React@18`, а также могут быть проблемы с перекрытием уведомления другими элементами страницы.
*
* Для статических тостов <u>рекомендуется</u> использовать компонент [SingleToast](https://tech.skbkontur.ru/react-ui/#/Components/SingleToast) - в нём исправлены эти проблемы.
*
* Рекомендуется использовать Toast через `ref` (см. примеры).
*/
@rootNode
export class Toast extends React.Component<ToastProps, ToastState> {
public static __KONTUR_REACT_UI__ = 'Toast';
private setRootNode!: TSetRootNode;
private theme!: Theme;

public static push(notification: string, action?: Nullable<Action>, showTime?: number) {
ToastStatic.push(notification, action, showTime);
Expand Down Expand Up @@ -79,9 +88,18 @@ export class Toast extends React.Component<ToastProps, ToastState> {

public render() {
return (
<RenderContainer>
<TransitionGroup>{this._renderToast()}</TransitionGroup>
</RenderContainer>
<ThemeContext.Consumer>
{(theme) => {
this.theme = this.props.theme ? ThemeFactory.create(this.props.theme as Theme, theme) : theme;
return (
<ThemeContext.Provider value={this.theme}>
<RenderContainer>
<TransitionGroup>{this._renderToast()}</TransitionGroup>
</RenderContainer>
</ThemeContext.Provider>
);
}}
</ThemeContext.Consumer>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as ReactUI from '../../index';

// all components that are available for import from the react-ui
const PUBLIC_COMPONENTS = Object.keys(ReactUI).filter((name) => {
return isPublicComponent((ReactUI as any)[name]);
return isPublicComponent((ReactUI as any)[name]) && name !== 'SingleToast';
});

// some components have required props
Expand Down
1 change: 1 addition & 0 deletions packages/react-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export * from './components/Switcher';
export * from './components/Tabs';
export * from './components/Textarea';
export * from './components/Toast';
export * from './components/SingleToast';
export * from './components/Toggle';
export * from './components/Token';
export * from './components/TokenInput';
Expand Down

0 comments on commit 1daf55b

Please sign in to comment.