From 1daf55bb5e432b14dd729a91983cdb3fa9e466a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9F=D1=8F?= =?UTF-8?q?=D1=82=D0=BA=D0=BE=D0=B2?= <48599460+JackUait@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:22:31 +0300 Subject: [PATCH] feat(SingleToast): add new component (#3314) --- .../components/SingleToast/SingleToast.md | 35 ++++++ .../components/SingleToast/SingleToast.tsx | 21 ++++ .../__tests__/SingleToast.test.tsx | 65 +++++++++++ .../react-ui/components/SingleToast/index.ts | 1 + packages/react-ui/components/Toast/Toast.md | 105 ------------------ packages/react-ui/components/Toast/Toast.tsx | 30 ++++- .../__tests__/PropsForwarding-test.tsx | 2 +- packages/react-ui/index.ts | 1 + 8 files changed, 148 insertions(+), 112 deletions(-) create mode 100644 packages/react-ui/components/SingleToast/SingleToast.md create mode 100644 packages/react-ui/components/SingleToast/SingleToast.tsx create mode 100644 packages/react-ui/components/SingleToast/__tests__/SingleToast.test.tsx create mode 100644 packages/react-ui/components/SingleToast/index.ts diff --git a/packages/react-ui/components/SingleToast/SingleToast.md b/packages/react-ui/components/SingleToast/SingleToast.md new file mode 100644 index 0000000000..98c6605a53 --- /dev/null +++ b/packages/react-ui/components/SingleToast/SingleToast.md @@ -0,0 +1,35 @@ +`` можно добавить в единственном месте в проекте, а статические методы будут всегда использовать последний отрендеренный экземпляр ``. + +_На внешний вид этого примера влияет следующий пример_ + +```jsx harmony +import { Button, SingleToast } from '@skbkontur/react-ui'; + +
+ + +
+``` + +`` можно кастомизировать с помощью переменных темы для `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") + }); +}; + +
+ + +
+``` diff --git a/packages/react-ui/components/SingleToast/SingleToast.tsx b/packages/react-ui/components/SingleToast/SingleToast.tsx new file mode 100644 index 0000000000..4340965b3d --- /dev/null +++ b/packages/react-ui/components/SingleToast/SingleToast.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { Toast, ToastProps } from '../Toast/Toast'; + +/** + * Позволяет вызывать тосты с помощью статических методов. В отличие от статических методов из компонента `` - их можно кастомизировать и они работают с `React@18`. + */ +export class SingleToast extends React.Component { + public static ref = React.createRef(); + 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 ; + }; +} diff --git a/packages/react-ui/components/SingleToast/__tests__/SingleToast.test.tsx b/packages/react-ui/components/SingleToast/__tests__/SingleToast.test.tsx new file mode 100644 index 0000000000..03aa32aa33 --- /dev/null +++ b/packages/react-ui/components/SingleToast/__tests__/SingleToast.test.tsx @@ -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( + <> + + + , + ); + + 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( + <> + + + , + ); + + userEvent.click(screen.getByRole('button', { name: buttonName })); + + expect(screen.getByTestId(ToastDataTids.action)).toHaveAttribute('aria-label', ariaLabel); + }); + }); +}); diff --git a/packages/react-ui/components/SingleToast/index.ts b/packages/react-ui/components/SingleToast/index.ts new file mode 100644 index 0000000000..0d369fbb9d --- /dev/null +++ b/packages/react-ui/components/SingleToast/index.ts @@ -0,0 +1 @@ +export * from './SingleToast'; diff --git a/packages/react-ui/components/Toast/Toast.md b/packages/react-ui/components/Toast/Toast.md index 3cb79e3703..1e41ee341e 100644 --- a/packages/react-ui/components/Toast/Toast.md +++ b/packages/react-ui/components/Toast/Toast.md @@ -67,108 +67,3 @@ class Toaster extends React.Component { ; ``` - -### SuperToast - -Вы можете объединить удобство статических методов и кастамизируемость классического способа через `ref`. -Для этого можно добавить обёртку, которая позволяет Toast работать по примеру GlobalLoader. - -Т.е. кастомный Toast можно добавить в единственном месте в проекте, а статические методы будут всегда использовать последний отрендеренный экземпляр Toast: - -Также в обёртке можно изменить логику появления всплывашки, по рекомендации Гайдов: -https://guides.kontur.ru/components/toast/#08 -```static -«Всегда показывается только 1 тост. Перед появлением следующего тоста, текущий скрывается, даже если время его показа еще не истекло» -``` - -Для этого немного изменим метод `SuperToast.close` с помощью специального метода `ReactDOM.flushSync`. - -Итоговый вариант: -```js static -const SuperToast = (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 { - public static ref = React.createRef(); - 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 ; - }; -} -``` - - -```jsx harmony -import { Button, Toast, ThemeContext, ThemeFactory, THEME_2022 } from '@skbkontur/react-ui'; -import ReactDOM from 'react-dom'; - -const SuperToast = (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 = () => ( - - {(theme) => { - return - - - }} - -); - -const rand = () => "Пример красного тоста №" + Math.round(Math.random() * 100).toString(); - -const push = () => { - SuperToast.push(rand(), { - label: "Cancel", - handler: () => SuperToast.push("Canceled") - }); -}; - -
- - - - - -
-; -``` diff --git a/packages/react-ui/components/Toast/Toast.tsx b/packages/react-ui/components/Toast/Toast.tsx index bed0a9319a..fa40133199 100644 --- a/packages/react-ui/components/Toast/Toast.tsx +++ b/packages/react-ui/components/Toast/Toast.tsx @@ -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'; @@ -28,6 +31,11 @@ export interface ToastState { export interface ToastProps extends Pick, CommonProps { onPush?: (notification: string, action?: Action) => void; onClose?: (notification: string, action?: Action) => void; + /** + * Обычный объект с переменными темы. + * Он будет объединён с темой из контекста. + */ + theme?: ThemeIn; } export const ToastDataTids = { @@ -41,15 +49,16 @@ export const ToastDataTids = { * Показывает уведомления. * * Доступен статический метод: `Toast.push(notification, action?, showTime?)`. - * Однако, при его использовании не работает кастомизация и могут быть проблемы - * с перекрытием уведомления другими элементами страницы. + * Однако, при его использовании не работает кастомизация, они не поддерживаются в `React@18`, а также могут быть проблемы с перекрытием уведомления другими элементами страницы. + * + * Для статических тостов рекомендуется использовать компонент [SingleToast](https://tech.skbkontur.ru/react-ui/#/Components/SingleToast) - в нём исправлены эти проблемы. * - * Рекомендуется использовать Toast через `ref` (см. примеры). */ @rootNode export class Toast extends React.Component { public static __KONTUR_REACT_UI__ = 'Toast'; private setRootNode!: TSetRootNode; + private theme!: Theme; public static push(notification: string, action?: Nullable, showTime?: number) { ToastStatic.push(notification, action, showTime); @@ -79,9 +88,18 @@ export class Toast extends React.Component { public render() { return ( - - {this._renderToast()} - + + {(theme) => { + this.theme = this.props.theme ? ThemeFactory.create(this.props.theme as Theme, theme) : theme; + return ( + + + {this._renderToast()} + + + ); + }} + ); } diff --git a/packages/react-ui/components/__tests__/PropsForwarding-test.tsx b/packages/react-ui/components/__tests__/PropsForwarding-test.tsx index 13cf422c3e..c470df94a1 100644 --- a/packages/react-ui/components/__tests__/PropsForwarding-test.tsx +++ b/packages/react-ui/components/__tests__/PropsForwarding-test.tsx @@ -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 diff --git a/packages/react-ui/index.ts b/packages/react-ui/index.ts index 107de95636..1c9019afdc 100644 --- a/packages/react-ui/index.ts +++ b/packages/react-ui/index.ts @@ -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';