Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SingleToast): add new component #3314

Merged
merged 13 commits into from
Dec 4, 2023
53 changes: 53 additions & 0 deletions packages/react-ui/components/SingleToast/SingleToast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
`<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 CustomToast = () => (
<ThemeContext.Consumer>
{(theme) => {
return <ThemeContext.Provider
value={ThemeFactory.create(
{
toastBg: "#000000",
},
theme
)}
>
<SingleToast />
</ThemeContext.Provider>
}}
</ThemeContext.Consumer>
lossir marked this conversation as resolved.
Show resolved Hide resolved
);

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

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

<div>
<CustomToast />
<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>
;
```
6 changes: 3 additions & 3 deletions packages/react-ui/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export const ToastDataTids = {
* Показывает уведомления.
*
* Доступен статический метод: `Toast.push(notification, action?, showTime?)`.
* Однако, при его использовании не работает кастомизация и могут быть проблемы
* с перекрытием уведомления другими элементами страницы.
* Однако, при его использовании не работает кастомизация, они не поддерживаются в `React@18`, а также могут быть проблемы с перекрытием уведомления другими элементами страницы.
*
* Для статических тостов <u>рекомендуется</u> использовать компонент [SingleToast](https://tech.skbkontur.ru/react-ui/#/Components/SingleToast) - в нём исправлены эти проблемы.
lossir marked this conversation as resolved.
Show resolved Hide resolved
*
* Рекомендуется использовать Toast через `ref` (см. примеры).
*/
@rootNode
export class Toast extends React.Component<ToastProps, ToastState> {
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