diff --git a/.eslintrc b/.eslintrc
index f11af1b6e1..b38843c8a8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -31,7 +31,8 @@
"import/no-extraneous-dependencies": "off",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"@typescript-eslint/prefer-ts-expect-error": "error",
- "@typescript-eslint/consistent-type-imports": ["error", {"prefer": "type-imports", "fixStyle": "separate-type-imports"}]
+ "@typescript-eslint/consistent-type-imports": ["error", {"prefer": "type-imports", "fixStyle": "separate-type-imports"}],
+ "complexity": "off"
},
"overrides": [
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 995d14e03d..c8545d1fb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog
+## [6.37.0](https://github.com/gravity-ui/uikit/compare/v6.36.0...v6.37.0) (2024-11-27)
+
+
+### Features
+
+* **Breadcrunbs:** allow items to be disabled independently ([#1962](https://github.com/gravity-ui/uikit/issues/1962)) ([301e4ab](https://github.com/gravity-ui/uikit/commit/301e4ab365639188e010390d5b19da1df13d75fa))
+* **Select:** new label and value resize behaviour ([#1896](https://github.com/gravity-ui/uikit/issues/1896)) ([2be5eb8](https://github.com/gravity-ui/uikit/commit/2be5eb8dc21679154bbb924af5e1e1eefa8a7a58))
+
+## [6.36.0](https://github.com/gravity-ui/uikit/compare/v6.35.2...v6.36.0) (2024-11-25)
+
+
+### Features
+
+* **PasswordInput:** add component ([#1745](https://github.com/gravity-ui/uikit/issues/1745)) ([2e7f2c7](https://github.com/gravity-ui/uikit/commit/2e7f2c731c8cb2fd08993fc30ffed8b06a5f0ea2))
+
+
+### Bug Fixes
+
+* **Select:** do not reserve space for clear if empty ([#1956](https://github.com/gravity-ui/uikit/issues/1956)) ([11dd537](https://github.com/gravity-ui/uikit/commit/11dd537feaa230133f8051fd6c370e6e3ec7d54f))
+* **Toc:** correctly display content of no items.length and add event forward ([#1939](https://github.com/gravity-ui/uikit/issues/1939)) ([8d456c3](https://github.com/gravity-ui/uikit/commit/8d456c3d77d63674f20ebac82913d8a26c14f997))
+
## [6.35.2](https://github.com/gravity-ui/uikit/compare/v6.35.1...v6.35.2) (2024-11-14)
diff --git a/package-lock.json b/package-lock.json
index c10e57b538..f46cb70378 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@gravity-ui/uikit",
- "version": "6.35.2",
+ "version": "6.37.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@gravity-ui/uikit",
- "version": "6.35.2",
+ "version": "6.37.0",
"license": "MIT",
"dependencies": {
"@bem-react/classname": "^1.6.0",
diff --git a/package.json b/package.json
index 8aeefdf126..a4aeb181c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@gravity-ui/uikit",
- "version": "6.35.2",
+ "version": "6.37.0",
"description": "Gravity UI base styling and components",
"keywords": [
"component",
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 6ffa7adef4..173ffa2282 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -178,7 +178,6 @@ const isButtonIconComponent = isOfType(ButtonIcon);
const isSpan = isOfType<{className?: string}>('span');
const buttonIconClassRe = RegExp(`^${b('icon')}($|\\s+\\w)`);
-// eslint-disable-next-line complexity
function prepareChildren(children: React.ReactNode) {
const items = React.Children.toArray(children);
diff --git a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-chromium-linux.png b/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-chromium-linux.png
deleted file mode 100644
index 45d40ecffd..0000000000
Binary files a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-chromium-linux.png and /dev/null differ
diff --git a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-webkit-linux.png b/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-webkit-linux.png
deleted file mode 100644
index ae79506d9d..0000000000
Binary files a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-dark-webkit-linux.png and /dev/null differ
diff --git a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-chromium-linux.png b/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-chromium-linux.png
deleted file mode 100644
index b820409953..0000000000
Binary files a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-chromium-linux.png and /dev/null differ
diff --git a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-webkit-linux.png b/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-webkit-linux.png
deleted file mode 100644
index 04c500af53..0000000000
Binary files a/src/components/Select/__snapshots__/Select.visual.test.tsx-snapshots/Select-control-with-parent-flex-basis-0-light-webkit-linux.png and /dev/null differ
diff --git a/src/components/Select/__tests__/Select.visual.test.tsx b/src/components/Select/__tests__/Select.visual.test.tsx
deleted file mode 100644
index 5ef9a5e027..0000000000
--- a/src/components/Select/__tests__/Select.visual.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-import {test} from '~playwright/core';
-
-import {Select} from '../index';
-import type {SelectOption} from '../index';
-
-test.describe('Select', {tag: '@Select'}, () => {
- test('control-with-parent-flex-basis-0', async ({mount, expectScreenshot}) => {
- const options: SelectOption[] = [{value: '1', content: 'Value 1'}];
- await mount(
-
,
- );
- await expectScreenshot();
- });
-});
diff --git a/src/components/Select/components/SelectClear/SelectClear.scss b/src/components/Select/components/SelectClear/SelectClear.scss
index 3949b639ad..dc844d6a5a 100644
--- a/src/components/Select/components/SelectClear/SelectClear.scss
+++ b/src/components/Select/components/SelectClear/SelectClear.scss
@@ -10,6 +10,7 @@ $block: '.#{variables.$ns}select-clear';
align-items: center;
margin-inline-start: auto;
z-index: 1;
+ flex-shrink: 0;
&:focus-visible {
border: 1px solid var(--g-color-line-generic-active);
diff --git a/src/components/Select/components/SelectControl/SelectControl.scss b/src/components/Select/components/SelectControl/SelectControl.scss
index 8fae1ea32f..dd2e9a6b55 100644
--- a/src/components/Select/components/SelectControl/SelectControl.scss
+++ b/src/components/Select/components/SelectControl/SelectControl.scss
@@ -48,22 +48,6 @@ $blockButton: '.#{variables.$ns}select-control__button';
}
}
-@mixin block_clear_reserved_width() {
- // reserving place for clear icon to fix width changing on displaying clear
- #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_s & {
- padding-inline-end: calc(#{variables.$s-height} + var(--_--text-right-padding));
- }
- #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_m & {
- padding-inline-end: calc(#{variables.$m-height} + var(--_--text-right-padding));
- }
- #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_l & {
- padding-inline-end: calc(#{variables.$l-height} + var(--_--text-right-padding));
- }
- #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_xl & {
- padding-inline-end: calc(#{variables.$xl-height} + var(--_--text-right-padding));
- }
-}
-
@mixin block_clear_reserved_disabled_width() {
// reserving place for clear icon to fix width changing on displaying clear for disabled select
#{$block}_has-clear#{$block}_size_s #{$blockButton}_disabled & {
@@ -116,8 +100,10 @@ $blockButton: '.#{variables.$ns}select-control__button';
@include mixins.button-reset();
@include mixins.pin($blockButton, ('::before', '::after'), var(--_--border-radius));
- display: inline-flex;
+ display: inline-grid;
+ grid-template-columns: auto auto;
align-items: center;
+ justify-content: flex-start;
overflow: hidden;
width: 100%;
height: 100%;
@@ -213,26 +199,22 @@ $blockButton: '.#{variables.$ns}select-control__button';
@include mixins.text-accent;
@include mixins.overflow-ellipsis();
- flex-shrink: 0;
- max-width: 50%;
margin-inline-end: 4px;
- white-space: nowrap;
}
&__placeholder,
&__option-text {
@include mixins.overflow-ellipsis();
- @include block_clear_reserved_disabled_width();
padding-inline-end: var(--_--text-right-padding);
}
+ &__option-text {
+ @include block_clear_reserved_disabled_width();
+ }
+
&__placeholder {
color: var(--g-color-text-hint);
-
- #{$blockButton}:not(#{$blockButton}_disabled) & {
- @include block_clear_reserved_width();
- }
}
&__chevron-icon {
diff --git a/src/components/Toaster/Provider/ToasterProvider.tsx b/src/components/Toaster/Provider/ToasterProvider.tsx
index f6aaf3629e..89b85adf59 100644
--- a/src/components/Toaster/Provider/ToasterProvider.tsx
+++ b/src/components/Toaster/Provider/ToasterProvider.tsx
@@ -2,105 +2,32 @@
import React from 'react';
-import type {InternalToastProps, ToastProps, ToasterPublicMethods} from '../types';
-import {getToastIndex} from '../utilities/getToastIndex';
-import {hasToast} from '../utilities/hasToast';
-import {removeToast} from '../utilities/removeToast';
+import type {ToasterSingleton} from '../ToasterSingleton';
+import type {InternalToastProps} from '../types';
import {ToasterContext} from './ToasterContext';
import {ToastsContext} from './ToastsContext';
-type Props = React.PropsWithChildren<{}>;
+type Props = React.PropsWithChildren<{
+ toaster: ToasterSingleton;
+}>;
-export const ToasterProvider = React.forwardRef(
- function ToasterProvider({children}: Props, ref) {
- const [toasts, setToasts] = React.useState([]);
+export const ToasterProvider = ({toaster, children}: Props) => {
+ const [toasts, setToasts] = React.useState([]);
- const add = React.useCallback((toast: ToastProps) => {
- const {name} = toast;
+ React.useEffect(() => {
+ const unsubscribe = toaster.subscribe(setToasts);
- setToasts((toasts) => {
- let nextToasts = toasts;
+ return () => {
+ unsubscribe();
+ };
+ }, [toaster]);
- if (hasToast(toasts, name)) {
- nextToasts = removeToast(toasts, name);
- }
-
- return [
- ...nextToasts,
- {
- ...toast,
- addedAt: Date.now(),
- ref: React.createRef(),
- },
- ];
- });
- }, []);
-
- const remove = React.useCallback((toastName: ToastProps['name']) => {
- setToasts((toasts) => {
- return removeToast(toasts, toastName);
- });
- }, []);
-
- const removeAll = React.useCallback(() => {
- setToasts(() => []);
- }, []);
-
- const update = React.useCallback(
- (toastName: ToastProps['name'], override: Partial) => {
- setToasts((toasts) => {
- if (!hasToast(toasts, toastName)) {
- return toasts;
- }
-
- const index = getToastIndex(toasts, toastName);
-
- return [
- ...toasts.slice(0, index),
- {
- ...toasts[index],
- ...override,
- },
- ...toasts.slice(index + 1),
- ];
- });
- },
- [],
- );
-
- const toastsRef = React.useRef(toasts);
- React.useEffect(() => {
- toastsRef.current = toasts;
- }, [toasts]);
- const has = React.useCallback((toastName: ToastProps['name']) => {
- return toastsRef.current ? hasToast(toastsRef.current, toastName) : false;
- }, []);
-
- const toasterContext = React.useMemo(() => {
- return {
- add,
- remove,
- removeAll,
- update,
- has,
- };
- }, [add, remove, removeAll, update, has]);
-
- React.useImperativeHandle(ref, () => ({
- add,
- remove,
- removeAll,
- update,
- has,
- }));
-
- return (
-
- {children}
-
- );
- },
-);
+ return (
+
+ {children}
+
+ );
+};
ToasterProvider.displayName = 'ToasterProvider';
diff --git a/src/components/Toaster/README.md b/src/components/Toaster/README.md
index f4b8de7990..7f2eeb682a 100644
--- a/src/components/Toaster/README.md
+++ b/src/components/Toaster/README.md
@@ -11,11 +11,13 @@ Component for adjustable notifications.
```jsx
import React from 'react';
import ReactDOMClient from 'react-dom/client';
-import {ToasterComponent, ToasterProvider} from '@gravity-ui/uikit';
+import {Toaster, ToasterComponent, ToasterProvider} from '@gravity-ui/uikit';
+
+const toaster = new Toaster();
const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(
-
+
,
@@ -66,8 +68,6 @@ const FoobarWithToaster = withToaster()(FoobarComponent);
Toaster has singleton, so when it is initialized in different parts of the application, the same instance will be returned.
On initialization, it is possible to transmit a className that will be assigned to dom-element which wrap all toasts.
-### React < 18
-
```js
import {Toaster} from '@gravity-ui/uikit';
const toaster = new Toaster();
@@ -79,34 +79,13 @@ or
import {toaster} from '@gravity-ui/uikit/toaster-singleton';
```
-### React 18
-
-```js
-import ReactDOMClient from 'react-dom/client';
-import {Toaster} from '@gravity-ui/uikit';
-Toaster.injectReactDOMClient(ReactDOMClient);
-const toaster = new Toaster();
-```
-
-or
-
-```js
-import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';
-```
-
-## Constructor arguments
-
-| Parameter | Type | Default | Description |
-| :-------- | :-------- | :---------- | :-------------------------------------------------- |
-| className | `string` | `undefined` | Custom class name to add to the component container |
-| mobile | `boolean` | `false` | Configuration that manages mobile/desktop views |
-
## Methods
| Method name | Params | Description |
| :---------------------------- | :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| add(toastOptions) | `Object` | Creates a new notification |
| remove(name) | `string` | Manually deletes an existing notification |
+| removeAll() | | Deletes all existing notifications |
| update(name, overrideOptions) | `string`, `Object` | Changes already rendered notification content. In `overrideOptions`, the following fields are optional: `title`, `type`, `content`, `actions` |
| has(name) | `string` | Checks fora toast with the given name in the list of displayed toasts |
diff --git a/src/components/Toaster/ToasterSingleton.tsx b/src/components/Toaster/ToasterSingleton.tsx
index 15096e70c5..f332afebc1 100644
--- a/src/components/Toaster/ToasterSingleton.tsx
+++ b/src/components/Toaster/ToasterSingleton.tsx
@@ -1,19 +1,11 @@
'use client';
-import React from 'react';
-
-import get from 'lodash/get';
-import ReactDOM from 'react-dom';
-
-import {block} from '../utils/cn';
-
-import {ToasterProvider} from './Provider/ToasterProvider';
-import {ToasterComponent} from './ToasterComponent/ToasterComponent';
-import type {ToastProps, ToasterArgs, ToasterPublicMethods} from './types';
+import type {InternalToastProps, ToastProps} from './types';
+import {getToastIndex} from './utilities/getToastIndex';
+import {hasToast} from './utilities/hasToast';
+import {removeToast} from './utilities/removeToast';
const TOASTER_KEY: unique symbol = Symbol('Toaster instance key');
-const bToaster = block('toaster');
-let ReactDOMClient: any;
declare global {
interface Window {
@@ -22,95 +14,86 @@ declare global {
}
export class ToasterSingleton {
- static injectReactDOMClient(client: any) {
- ReactDOMClient = client;
- }
+ private toasts: InternalToastProps[] = [];
+ private listeners: ((toasts: InternalToastProps[]) => void)[] = [];
- private rootNode!: HTMLDivElement;
- private reactRoot!: any;
- private className = '';
- private mobile = false;
- private componentAPI: null | ToasterPublicMethods = null;
+ constructor() {
+ if (window[TOASTER_KEY] instanceof ToasterSingleton) {
+ return window[TOASTER_KEY];
+ }
- constructor(args?: ToasterArgs) {
- const className = get(args, ['className'], '');
- const mobile = get(args, ['mobile'], false);
+ window[TOASTER_KEY] = this;
+ }
- if (window[TOASTER_KEY] instanceof ToasterSingleton) {
- const me = window[TOASTER_KEY];
- me.className = className;
- me.mobile = mobile;
- me.setRootNodeClassName();
- return me;
+ add(toast: ToastProps) {
+ let nextToasts = this.toasts;
+
+ if (hasToast(nextToasts, toast.name)) {
+ nextToasts = removeToast(nextToasts, toast.name);
}
- this.className = className;
- this.mobile = mobile;
- this.createRootNode();
- this.createReactRoot();
- this.render();
+ this.toasts = [
+ ...nextToasts,
+ {
+ ...toast,
+ addedAt: Date.now(),
+ ref: {current: null},
+ },
+ ];
- window[TOASTER_KEY] = this;
+ this.notify();
}
- destroy() {
- // eslint-disable-next-line react/no-deprecated
- ReactDOM.unmountComponentAtNode(this.rootNode);
- document.body.removeChild(this.rootNode);
+ remove(name: string) {
+ this.toasts = removeToast(this.toasts, name);
+
+ this.notify();
}
- add = (options: ToastProps) => {
- this.componentAPI?.add(options);
- };
+ removeAll() {
+ this.toasts = [];
- remove = (name: string) => {
- this.componentAPI?.remove(name);
- };
+ this.notify();
+ }
- removeAll = () => {
- this.componentAPI?.removeAll();
- };
+ update(name: string, overrideOptions: Partial) {
+ if (!hasToast(this.toasts, name)) {
+ return;
+ }
- update = (name: string, overrideOptions: Partial) => {
- this.componentAPI?.update(name, overrideOptions);
- };
+ const index = getToastIndex(this.toasts, name);
- has = (name: string) => {
- return this.componentAPI?.has(name) ?? false;
- };
+ this.toasts = [
+ ...this.toasts.slice(0, index),
+ {
+ ...this.toasts[index],
+ ...overrideOptions,
+ },
+ ...this.toasts.slice(index + 1),
+ ];
- private createRootNode() {
- this.rootNode = document.createElement('div');
- this.setRootNodeClassName();
- document.body.appendChild(this.rootNode);
+ this.notify();
}
- private createReactRoot() {
- if (ReactDOMClient) {
- this.reactRoot = ReactDOMClient.createRoot(this.rootNode);
- }
+ has(name: string) {
+ return hasToast(this.toasts, name);
}
- private render() {
- const container = (
- {
- this.componentAPI = api;
- }}
- >
-
-
- );
-
- if (this.reactRoot) {
- this.reactRoot.render(container);
- } else {
- // eslint-disable-next-line react/no-deprecated
- ReactDOM.render(container, this.rootNode, () => Promise.resolve());
+ subscribe(listener: (toasts: InternalToastProps[]) => void) {
+ if (typeof listener === 'function') {
+ this.listeners.push(listener);
}
+
+ return () => {
+ this.listeners = this.listeners.filter(
+ (currentListener) => listener !== currentListener,
+ );
+ };
}
- private setRootNodeClassName() {
- this.rootNode.className = bToaster({mobile: this.mobile}, this.className);
+ private notify() {
+ for (const listener of this.listeners) {
+ listener(this.toasts);
+ }
}
}
diff --git a/src/components/Toaster/__stories__/Toaster.stories.tsx b/src/components/Toaster/__stories__/Toaster.stories.tsx
index 6ab0d4f961..eea7fede6c 100644
--- a/src/components/Toaster/__stories__/Toaster.stories.tsx
+++ b/src/components/Toaster/__stories__/Toaster.stories.tsx
@@ -8,6 +8,7 @@ import {BUTTON_VIEWS} from '../../Button/constants';
import {ToasterProvider} from '../Provider/ToasterProvider';
import {Toast} from '../Toast/Toast';
import {ToasterComponent} from '../ToasterComponent/ToasterComponent';
+import {ToasterSingleton} from '../ToasterSingleton';
import {TOAST_THEMES} from '../constants';
import {useToaster} from '../hooks/useToaster';
import type {ToastAction} from '../types';
@@ -53,13 +54,15 @@ function booleanControl(label: string) {
};
}
+const toasterInstance = new ToasterSingleton();
+
export default {
title: 'Components/Feedback/Toaster',
component: Toast,
decorators: [
function withToasters(Story) {
return (
-
+
);
diff --git a/src/components/Toaster/__stories__/ToasterShowcase.tsx b/src/components/Toaster/__stories__/ToasterShowcase.tsx
index 8f166a51a3..2fc921bc3a 100644
--- a/src/components/Toaster/__stories__/ToasterShowcase.tsx
+++ b/src/components/Toaster/__stories__/ToasterShowcase.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import {faker} from '@faker-js/faker/locale/en';
import {CircleCheck, CircleInfo, Thunderbolt, TriangleExclamation} from '@gravity-ui/icons';
-import {ToasterComponent, useToaster} from '..';
+import {Toaster, ToasterComponent, useToaster} from '..';
import type {ToastAction, ToastProps} from '..';
import {Button} from '../../Button';
import type {ButtonView} from '../../Button';
@@ -14,6 +14,8 @@ import './ToasterShowcase.scss';
const b = cn('toaster-showcase');
+export const toasterInstance = new Toaster();
+
const CONTENT = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci, atque!';
const ACTIONS = [
@@ -371,6 +373,22 @@ export const ToasterDemo = ({
);
+ const singletonToasterBtn = (
+
+ );
+
const component = React.useMemo(() => , []);
return (
@@ -385,6 +403,7 @@ export const ToasterDemo = ({
{toastWithLongContent}
{dynamicallyUpdatingToast}
{overrideToastBtn}
+ {singletonToasterBtn}
{clearBtn}
{component}
diff --git a/src/components/Toaster/__tests__/ToasterProvider.test.tsx b/src/components/Toaster/__tests__/ToasterProvider.test.tsx
index 78494ae8e2..30280e99f6 100644
--- a/src/components/Toaster/__tests__/ToasterProvider.test.tsx
+++ b/src/components/Toaster/__tests__/ToasterProvider.test.tsx
@@ -7,38 +7,20 @@ import {ToasterComponent} from '../ToasterComponent/ToasterComponent';
import {fireAnimationEndEvent} from '../__mocks__/fireAnimationEndEvent';
import {getToast} from '../__mocks__/getToast';
import {tick} from '../__mocks__/tick';
-import {useToaster} from '../hooks/useToaster';
-import type {ToasterPublicMethods} from '../types';
+import {Toaster} from '../index';
-function ToastAPI({onMount}: {onMount: (api: ToasterPublicMethods) => void}) {
- const toaster = useToaster();
-
- React.useEffect(() => {
- onMount(toaster);
- }, []);
-
- return null;
-}
+const toasterInstance = new Toaster();
function setup() {
- let providerAPI: undefined | ToasterPublicMethods;
-
render(
-
- {
- providerAPI = api;
- }}
- />
+
,
);
- if (!providerAPI) {
+ if (!toasterInstance) {
throw new Error('Failed to setup test');
}
-
- return providerAPI;
}
const toastTimeout = 1000;
@@ -54,10 +36,10 @@ describe('api.add', () => {
// We test that after adding toast the next add will remove
// previous toast from DOM and add it again
it('should override already added toast', async function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add(toastProps);
+ toasterInstance.add(toastProps);
});
let toast = getToast();
@@ -67,7 +49,7 @@ describe('api.add', () => {
jest.advanceTimersByTime(1);
act(() => {
- providerAPI.add(toastProps);
+ toasterInstance.add(toastProps);
});
fireAnimationEndEvent(toast, 'toast-hide-end');
@@ -81,10 +63,10 @@ describe('api.add', () => {
describe('api.remove', () => {
it('should remove toast', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
@@ -94,7 +76,7 @@ describe('api.remove', () => {
expect(toast).toBeInTheDocument();
act(() => {
- providerAPI.remove(toastProps.name);
+ toasterInstance.remove(toastProps.name);
});
tick(toast, 0);
@@ -104,10 +86,10 @@ describe('api.remove', () => {
});
it('should remove toast after timeout', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
@@ -132,10 +114,10 @@ it('should remove toast after timeout', function () {
});
it('should preserve toast on hover', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
@@ -170,10 +152,10 @@ it('should preserve toast on hover', function () {
describe('api.update', () => {
it('should update toast', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
@@ -185,7 +167,7 @@ describe('api.update', () => {
expect(screen.queryByRole('button', {name: 'Toast Button'})).not.toBeInTheDocument();
act(() => {
- providerAPI.update(toastProps.name, {
+ toasterInstance.update(toastProps.name, {
content: 'Test Content of the toast',
actions: [
{
@@ -202,10 +184,10 @@ describe('api.update', () => {
});
it('should bypass update of unexisted toasts', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
@@ -214,7 +196,7 @@ describe('api.update', () => {
const toast = getToast();
act(() => {
- providerAPI.update(`unexisted ${toastProps.name}`, {
+ toasterInstance.update(`unexisted ${toastProps.name}`, {
content: 'Test Content of the toast',
actions: [
{
@@ -231,11 +213,11 @@ describe('api.update', () => {
describe('api.removeAll', () => {
it('should remove all toasts', function () {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add(toastProps);
- providerAPI.add({
+ toasterInstance.add(toastProps);
+ toasterInstance.add({
...toastProps,
name: `${toastProps.name}2`,
title: `${toastProps.title}2`,
@@ -249,7 +231,7 @@ describe('api.removeAll', () => {
expect(toast2).toBeInTheDocument();
act(() => {
- providerAPI.removeAll();
+ toasterInstance.removeAll();
});
[toast1, toast2].forEach((toast) => fireAnimationEndEvent(toast, 'toast-hide-end'));
@@ -261,45 +243,45 @@ describe('api.removeAll', () => {
describe('api.has', () => {
it('should return false when toast is not added', () => {
- const providerAPI = setup();
- expect(providerAPI.has('unexisted toasts')).toBe(false);
+ setup();
+ expect(toasterInstance.has('unexisted toasts')).toBe(false);
});
it('should return false when toast is removed by code', () => {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
});
- expect(providerAPI.has(toastProps.name)).toBe(true);
+ expect(toasterInstance.has(toastProps.name)).toBe(true);
act(() => {
- providerAPI.remove(toastProps.name);
+ toasterInstance.remove(toastProps.name);
});
- expect(providerAPI.has(toastProps.name)).toBe(false);
+ expect(toasterInstance.has(toastProps.name)).toBe(false);
});
it('should return false when toast is removed by timer', () => {
- const providerAPI = setup();
+ setup();
act(() => {
- providerAPI.add({
+ toasterInstance.add({
...toastProps,
autoHiding: toastTimeout,
});
});
- expect(providerAPI.has(toastProps.name)).toBe(true);
+ expect(toasterInstance.has(toastProps.name)).toBe(true);
act(() => {
jest.advanceTimersByTime(toastTimeout);
});
- expect(providerAPI.has(toastProps.name)).toBe(false);
+ expect(toasterInstance.has(toastProps.name)).toBe(false);
});
});
@@ -322,16 +304,10 @@ describe('modal remains open after toaster close', () => {
};
function setup() {
- let providerAPI: undefined | ToasterPublicMethods;
let openModal: undefined | (() => void);
render(
-
- {
- providerAPI = api;
- }}
- />
+
{
openModal = _openModal;
@@ -341,15 +317,15 @@ describe('modal remains open after toaster close', () => {
,
);
- if (!providerAPI || !openModal) {
+ if (!toasterInstance || !openModal) {
throw new Error('Failed to setup test');
}
- return {providerAPI, openModal};
+ return {openModal};
}
it('Toaster was opened after Modal', async () => {
- const {providerAPI, openModal} = setup();
+ const {openModal} = setup();
act(openModal);
@@ -357,7 +333,7 @@ describe('modal remains open after toaster close', () => {
expect(modal).toBeInTheDocument();
act(() => {
- providerAPI.add({...toastProps, isClosable: true});
+ toasterInstance.add({...toastProps, isClosable: true});
});
const toast = getToast();
@@ -380,10 +356,10 @@ describe('modal remains open after toaster close', () => {
});
it('Toaster was opened before Modal', async () => {
- const {providerAPI, openModal} = setup();
+ const {openModal} = setup();
act(() => {
- providerAPI.add({...toastProps, isClosable: true});
+ toasterInstance.add({...toastProps, isClosable: true});
});
const toast = getToast();
@@ -411,12 +387,12 @@ describe('modal remains open after toaster close', () => {
});
it('Toaster calls onClose callback when close icon is clicked', async () => {
- const {providerAPI} = setup();
+ setup();
const mockOnCloseFn = jest.fn();
act(() => {
- providerAPI.add({...toastProps, isClosable: true, onClose: mockOnCloseFn});
+ toasterInstance.add({...toastProps, isClosable: true, onClose: mockOnCloseFn});
});
const toast = getToast();
diff --git a/src/components/Toaster/hooks/useToaster.ts b/src/components/Toaster/hooks/useToaster.ts
index 4d069aaa79..527a4ec101 100644
--- a/src/components/Toaster/hooks/useToaster.ts
+++ b/src/components/Toaster/hooks/useToaster.ts
@@ -10,5 +10,14 @@ export function useToaster(): ToasterPublicMethods {
throw new Error('Toaster: `useToaster` hook is used out of context');
}
- return React.useMemo(() => toaster, [toaster]);
+ return React.useMemo(
+ () => ({
+ add: toaster.add.bind(toaster),
+ remove: toaster.remove.bind(toaster),
+ removeAll: toaster.removeAll.bind(toaster),
+ update: toaster.update.bind(toaster),
+ has: toaster.has.bind(toaster),
+ }),
+ [toaster],
+ );
}
diff --git a/src/components/Toc/Toc.tsx b/src/components/Toc/Toc.tsx
index a29575dbe1..30e9b65dab 100644
--- a/src/components/Toc/Toc.tsx
+++ b/src/components/Toc/Toc.tsx
@@ -15,10 +15,11 @@ export interface TocProps extends QAProps {
items: TocItemType[];
value?: string;
onUpdate?: (value: string) => void;
+ onItemClick?: (event: React.MouseEvent) => void;
}
export const Toc = React.forwardRef(function Toc(props, ref) {
- const {value: activeValue, items, className, onUpdate, qa} = props;
+ const {value: activeValue, items, className, onUpdate, onItemClick, qa} = props;
return (