Repository: gravity-ui/uikit
Pull request #659 (2023-05-07 17:12:40)

docs: fixed typo in "must" word
There were some typos in docs/code comments ("mast" instead of "must")

By the way, there is [code spell checker plugin](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) for VSCode

---

I hereby agree to the terms of the CLA available at: [link](https://yandex.ru/legal/cla/?lang=ru).

gravity-ui-bot: [Preview](https://preview.gravity-ui.com/uikit/659/) is ready.
<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a690cf7e..375cda74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## [4.12.0](https://github.com/gravity-ui/uikit/compare/v4.11.1...v4.12.0) (2023-05-17)
+
+
+### Features
+
+* **Button:** add xs Button size ([#663](https://github.com/gravity-ui/uikit/issues/663)) ([efbe9fc](https://github.com/gravity-ui/uikit/commit/efbe9fc70e48466961830994269ecc60ea03400e))
+
 ## [4.11.1](https://github.com/gravity-ui/uikit/compare/v4.11.0...v4.11.1) (2023-05-12)
 
 
diff --git a/package-lock.json b/package-lock.json
index 2a6fe47a..5b26bfce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
     "name": "@gravity-ui/uikit",
-    "version": "4.11.1",
+    "version": "4.12.0",
     "lockfileVersion": 1,
     "requires": true,
     "dependencies": {
diff --git a/package.json b/package.json
index e12e4e72..759f0f1d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@gravity-ui/uikit",
-    "version": "4.11.1",
+    "version": "4.12.0",
     "description": "Gravity UI base styling and components",
     "license": "MIT",
     "repository": {
diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss
index 18caab46..d7909252 100644
--- a/src/components/Button/Button.scss
+++ b/src/components/Button/Button.scss
@@ -85,6 +85,30 @@ $block: '.#{variables.$ns}button';
     }
 
     &_size {
+        &_xs {
+            --yc-button-height: 20px;
+
+            font-size: 13px;
+
+            #{$block}__text {
+                margin: 0 6px;
+            }
+
+            #{$block}__icon {
+                width: var(--yc-button-height);
+
+                &_side_left ~ #{$block}__text {
+                    margin-left: 18px;
+                }
+
+                &_side_right ~ #{$block}__text {
+                    margin-right: 18px;
+                }
+            }
+
+            --yc-button-border-radius: var(--yc-border-radius-xs);
+        }
+
         &_s {
             --yc-button-height: 24px;
 
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 7d9ba1eb..a87e2a6f 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -23,7 +23,7 @@ export type ButtonView =
     | 'outlined-contrast' // outlined button appearance with contrast background
     | 'flat-contrast'; // flat button appearance with contrast background
 
-export type ButtonSize = 's' | 'm' | 'l' | 'xl';
+export type ButtonSize = 'xs' | 's' | 'm' | 'l' | 'xl';
 
 export type ButtonPin =
     | 'round-round'
diff --git a/src/components/Button/README.md b/src/components/Button/README.md
index 43814fe9..aaeacff4 100644
--- a/src/components/Button/README.md
+++ b/src/components/Button/README.md
@@ -82,7 +82,7 @@ type ButtonView =
   | 'outlined-contrast'
   | 'flat-contrast';
 
-type ButtonSize = 's' | 'm' | 'l' | 'xl';
+type ButtonSize = 'xs' | 's' | 'm' | 'l' | 'xl';
 
 type ButtonPin =
   | 'round-round'
diff --git a/src/components/Button/__stories__/Button.new.stories.tsx b/src/components/Button/__stories__/Button.new.stories.tsx
index 52159efd..57d1028e 100644
--- a/src/components/Button/__stories__/Button.new.stories.tsx
+++ b/src/components/Button/__stories__/Button.new.stories.tsx
@@ -29,7 +29,7 @@ export default {
             defaultValue: 'normal',
         },
         size: {
-            options: ['s', 'm', 'l', 'xl'],
+            options: ['xs', 's', 'm', 'l', 'xl'],
             control: {type: 'radio'},
             defaultValue: 'm',
         },
diff --git a/src/components/Button/__stories__/Button.stories.tsx b/src/components/Button/__stories__/Button.stories.tsx
index 20822854..74233c25 100644
--- a/src/components/Button/__stories__/Button.stories.tsx
+++ b/src/components/Button/__stories__/Button.stories.tsx
@@ -15,6 +15,10 @@ export const Default = DefaultTemplate.bind({});
 
 const SizeTemplate: Story<ButtonProps> = (args) => (
     <>
+        <Button {...args} size="xs">
+            Size xs
+        </Button>
+        <span style={{margin: '16px'}} />
         <Button {...args} size="s">
             Size s
         </Button>
diff --git a/src/components/Button/__stories__/ButtonShowcase.tsx b/src/components/Button/__stories__/ButtonShowcase.tsx
index 6e30ccef..e795d907 100644
--- a/src/components/Button/__stories__/ButtonShowcase.tsx
+++ b/src/components/Button/__stories__/ButtonShowcase.tsx
@@ -16,6 +16,8 @@ export function ButtonShowcase() {
             {renderViewGrid()}
             <ShowcaseItem title="size">
                 <p>
+                    <Button size="xs">Size xs</Button>
+                    <span style={{margin: '8px'}} />
                     <Button size="s">Size s</Button>
                     <span style={{margin: '8px'}} />
                     <Button size="m">Size m</Button>
diff --git a/src/components/Button/__stories__/examples/ButtonExampleView/ButtonExampleView.tsx b/src/components/Button/__stories__/examples/ButtonExampleView/ButtonExampleView.tsx
index 266af18a..ff449106 100644
--- a/src/components/Button/__stories__/examples/ButtonExampleView/ButtonExampleView.tsx
+++ b/src/components/Button/__stories__/examples/ButtonExampleView/ButtonExampleView.tsx
@@ -238,6 +238,7 @@ export function ButtonExampleState() {
 export function ButtonExampleSize() {
     return (
         <DocsExample gap="l" space="l">
+            <Button size="xs">XS-size</Button>
             <Button size="s">S-size</Button>
             <Button size="m">M-size</Button>
             <Button size="l">L-size</Button>
diff --git a/src/components/Table/hoc/withTableActions/withTableActions.tsx b/src/components/Table/hoc/withTableActions/withTableActions.tsx
index 81202f44..f1f91046 100644
--- a/src/components/Table/hoc/withTableActions/withTableActions.tsx
+++ b/src/components/Table/hoc/withTableActions/withTableActions.tsx
@@ -3,7 +3,7 @@ import _memoize from 'lodash/memoize';
 import {block} from '../../../utils/cn';
 import {getComponentName} from '../../../utils/getComponentName';
 import {Icon} from '../../../Icon';
-import {Button, ButtonSize} from '../../../Button';
+import {Button} from '../../../Button';
 import {Popup} from '../../../Popup';
 import {Menu, MenuItemProps} from '../../../Menu';
 import {DotsIcon} from '../../../icons/DotsIcon';
@@ -52,9 +52,14 @@ export interface TableActionGroup<I> {
 
 export type TableActionConfig<I> = TableAction<I> | TableActionGroup<I>;
 
+/**
+ * common sizes for Menu and Button
+ */
+export type TableRowActionsSize = 's' | 'm' | 'l' | 'xl';
+
 export interface WithTableActionsProps<I> {
     getRowActions: (item: I, index: number) => TableActionConfig<I>[];
-    rowActionsSize?: ButtonSize;
+    rowActionsSize?: TableRowActionsSize;
 }
 
 interface WithTableActionsState<I> {
diff --git a/src/components/Toaster/Provider/ToasterProvider.tsx b/src/components/Toaster/Provider/ToasterProvider.tsx
index f5b0cc7f..2d79588c 100644
--- a/src/components/Toaster/Provider/ToasterProvider.tsx
+++ b/src/components/Toaster/Provider/ToasterProvider.tsx
@@ -22,7 +22,14 @@ export const ToasterProvider = React.forwardRef<ToasterPublicMethods, Props>(
                     nextToasts = removeToast(toasts, name);
                 }
 
-                return [...nextToasts, {...toast, addedAt: Date.now()}];
+                return [
+                    ...nextToasts,
+                    {
+                        ...toast,
+                        addedAt: Date.now(),
+                        ref: React.createRef<HTMLDivElement>(),
+                    },
+                ];
             });
         }, []);
 
@@ -51,7 +58,6 @@ export const ToasterProvider = React.forwardRef<ToasterPublicMethods, Props>(
                             ...toasts[index],
                             ...override,
                             isOverride: true,
-                            updatesCounter: (toasts[index].updatesCounter ?? 0) + 1,
                         },
                         ...toasts.slice(index + 1),
                     ];
diff --git a/src/components/Toaster/Toast/Toast.scss b/src/components/Toaster/Toast/Toast.scss
index be0933c2..98e369df 100644
--- a/src/components/Toaster/Toast/Toast.scss
+++ b/src/components/Toaster/Toast/Toast.scss
@@ -3,26 +3,19 @@
 
 $block: '.#{variables.$ns}toast';
 
-// Function to get animation that will trigger action in `onAnimationEnd` event handler
-// It always should be at the end of animations list
-@function get-trailing-keyframe($keyframe, $delay) {
-    // Short animation duration to make sure that onAnimationEnd would be triggered
-    @return $keyframe 0.0001s forwards $delay;
-}
-
 #{$block} {
     $containerClass: #{&}__container;
     $iconClass: #{&}__icon;
 
-    // Deprecated. This custom property can be removed in next major release
+    // These custom properties used in ToastAnimation
+    --yc-toaster-margin: 10px;
     --yc-toaster-padding: 16px;
 
     display: flex;
     box-sizing: border-box;
-    position: absolute;
-    opacity: 0;
+    position: relative;
     width: inherit;
-    margin-bottom: 10px;
+    margin-bottom: var(--yc-toaster-margin);
     padding: var(--yc-toaster-padding);
     font-size: 13px;
     border-radius: 8px;
@@ -35,52 +28,6 @@ $block: '.#{variables.$ns}toast';
         width: 100%;
     }
 
-    // Since the "opacity" needs to be overridden when toast appearing
-    // The "created" modifier must be higher in code than the "appearing" modifier.
-    &_created {
-        position: relative;
-        opacity: 1;
-    }
-
-    &_appearing {
-        opacity: 0;
-        height: 0;
-        margin-bottom: 0;
-        padding: 0;
-        transition: height 0.35s ease;
-    }
-
-    &_show-animation {
-        $indents-duration: 0.35s;
-        $move-duration: 0.25s;
-
-        animation: toast-set-indents $indents-duration ease forwards,
-            toast-move-left $move-duration ease forwards $indents-duration,
-            get-trailing-keyframe(toast-display-end, $indents-duration + $move-duration);
-    }
-
-    &_show-animation#{&}_mobile {
-        animation: toast-set-indents 0.35s ease forwards, toast-move-top 0.25s ease forwards 0.35s;
-    }
-
-    &_hide-animation {
-        $move-duration: 0.25s;
-        $remove-duration: 0.35s;
-
-        animation: toast-move-right $move-duration ease forwards,
-            toast-remove-height $remove-duration ease forwards $move-duration,
-            get-trailing-keyframe(toast-hide-end, $move-duration + $remove-duration);
-    }
-
-    &_hide-animation#{&}_mobile {
-        $move-duration: 0.25s;
-        $remove-duration: 0.35s;
-
-        animation: toast-move-bottom $move-duration ease forwards,
-            toast-remove-height $remove-duration ease forwards $move-duration,
-            get-trailing-keyframe(toast-hide-end, $move-duration + $remove-duration);
-    }
-
     &_default {
         background-color: var(--yc-color-base-float);
     }
@@ -169,70 +116,3 @@ $block: '.#{variables.$ns}toast';
         right: 14px;
     }
 }
-
-@keyframes toast-move-right {
-    from {
-        transform: translateX(0);
-    }
-
-    to {
-        opacity: 0;
-        transform: translateX(10px);
-    }
-}
-
-@keyframes toast-move-left {
-    from {
-        opacity: 0;
-        transform: translateX(10px);
-    }
-
-    to {
-        opacity: 1;
-        transform: translateX(0);
-    }
-}
-
-@keyframes toast-display-end {
-}
-
-@keyframes toast-move-top {
-    from {
-        opacity: 0;
-        transform: translateY(10px);
-    }
-
-    to {
-        opacity: 1;
-        transform: translateY(0);
-    }
-}
-
-@keyframes toast-move-bottom {
-    from {
-        transform: translateX(0);
-    }
-
-    to {
-        opacity: 0;
-        transform: translateY(10px);
-    }
-}
-
-@keyframes toast-remove-height {
-    to {
-        height: 0;
-        margin-bottom: 0;
-        padding: 0;
-    }
-}
-
-@keyframes toast-hide-end {
-}
-
-@keyframes toast-set-indents {
-    to {
-        margin-bottom: 10px;
-        padding: 16px;
-    }
-}
diff --git a/src/components/Toaster/Toast/Toast.tsx b/src/components/Toaster/Toast/Toast.tsx
index c8a64df8..3db8b906 100644
--- a/src/components/Toaster/Toast/Toast.tsx
+++ b/src/components/Toaster/Toast/Toast.tsx
@@ -10,8 +10,6 @@ import i18n from '../i18n';
 import './Toast.scss';
 
 const b = block('toast');
-const FADE_IN_LAST_ANIMATION_NAME = 'toast-display-end';
-const FADE_OUT_LAST_ANIMATION_NAME = 'toast-hide-end';
 const DEFAULT_TIMEOUT = 5000;
 const CROSS_ICON_SIZE = 12;
 const TITLE_ICONS: Record<ToastType, IconProps['data']> = {
@@ -22,109 +20,12 @@ const TITLE_ICONS: Record<ToastType, IconProps['data']> = {
 };
 
 interface ToastInnerProps {
-    removeCallback: VoidFunction;
+    removeCallback: (name: string) => void;
     mobile?: boolean;
 }
 
 interface ToastUnitedProps extends InternalToastProps, ToastInnerProps {}
 
-enum ToastStatus {
-    Creating = 'creating',
-    ShowingIndents = 'showing-indents',
-    ShowingHeight = 'showing-height',
-    Hiding = 'hiding',
-    Shown = 'shown',
-}
-
-interface UseToastHeightProps {
-    isOverride: boolean;
-    status: ToastStatus;
-    updatesCounter: number;
-}
-
-function getHeight(ref: React.RefObject<HTMLDivElement>) {
-    return ref.current?.offsetHeight;
-}
-
-function useToastHeight({isOverride, status, updatesCounter}: UseToastHeightProps) {
-    const [height, setHeight] = React.useState<number | undefined>(undefined);
-
-    const ref = React.useRef<HTMLDivElement>(null);
-
-    React.useEffect(() => {
-        // ATTENTION: getting `offsetHeight` is important for correct transaction of `height`
-        // HOW THIS WORKS:
-        // On the step of changing state to `ShowingIndent` we set class with height `transition`
-        // We need now to apply this styles, to achieve this we're calling `offsetHeight`, to force repaint
-        // Now we call changing state to `ShowingHeight` and changing height now happen with transition
-        if (status === ToastStatus.ShowingIndents) {
-            getHeight(ref);
-        }
-    }, [status]);
-
-    React.useEffect(() => {
-        if (
-            status === ToastStatus.ShowingIndents ||
-            status === ToastStatus.ShowingHeight ||
-            status === ToastStatus.Hiding
-        ) {
-            return; // Not update height during animation
-        }
-
-        const newHeight = getHeight(ref);
-        setHeight(newHeight);
-    }, [isOverride, updatesCounter]);
-
-    const style: React.CSSProperties = {};
-    if (height && status !== ToastStatus.ShowingIndents && status !== ToastStatus.Shown) {
-        style.height = height;
-    }
-
-    return {style, ref};
-}
-
-interface UseToastStatusProps {
-    onRemove: VoidFunction;
-}
-
-function useToastStatus({onRemove}: UseToastStatusProps) {
-    const [status, setStatus] = React.useState<ToastStatus>(ToastStatus.Creating);
-
-    React.useEffect(() => {
-        if (status === ToastStatus.Creating) {
-            setStatus(ToastStatus.ShowingIndents);
-        } else if (status === ToastStatus.ShowingIndents) {
-            setStatus(ToastStatus.ShowingHeight);
-        }
-    }, [status]);
-
-    const onFadeInAnimationEnd: React.AnimationEventHandler<HTMLDivElement> = (e) => {
-        if (e.animationName === FADE_IN_LAST_ANIMATION_NAME) {
-            setStatus(ToastStatus.Shown);
-        }
-    };
-
-    const onFadeOutAnimationEnd: React.AnimationEventHandler<HTMLDivElement> = (e) => {
-        if (e.animationName === FADE_OUT_LAST_ANIMATION_NAME) {
-            onRemove();
-        }
-    };
-
-    let onAnimationEnd;
-    if (status === ToastStatus.ShowingHeight) {
-        onAnimationEnd = onFadeInAnimationEnd;
-    }
-    if (status === ToastStatus.Hiding) {
-        onAnimationEnd = onFadeOutAnimationEnd;
-    }
-
-    const handleClose = React.useCallback(() => {
-        setStatus(ToastStatus.Hiding);
-    }, []);
-
-    return {status, containerProps: {onAnimationEnd}, handleClose};
-}
-
 interface RenderActionsProps {
     actions?: ToastAction[];
     onClose: VoidFunction;
@@ -173,8 +74,9 @@ function renderIconByType({type}: RenderIconProps) {
     return <Icon data={TITLE_ICONS[type]} className={b('icon', {[type]: true})} />;
 }
 
-export function Toast(props: ToastUnitedProps) {
+export const Toast = React.forwardRef<HTMLDivElement, ToastUnitedProps>(function Toast(props, ref) {
     const {
+        name,
         content,
         actions,
         title,
@@ -183,40 +85,23 @@ export function Toast(props: ToastUnitedProps) {
         renderIcon,
         autoHiding: timeoutProp = DEFAULT_TIMEOUT,
         isClosable = true,
-        isOverride = false,
-        updatesCounter = 0,
         mobile = false,
+        removeCallback,
     } = props;
 
-    const {
-        status,
-        containerProps: statusProps,
-        handleClose,
-    } = useToastStatus({onRemove: props.removeCallback});
-
-    const heightProps = useToastHeight({isOverride, status, updatesCounter});
-
+    const onClose = React.useCallback(() => removeCallback(name), [removeCallback, name]);
     const timeout = typeof timeoutProp === 'number' ? timeoutProp : undefined;
-    const closeOnTimeoutProps = useCloseOnTimeout<HTMLDivElement>({onClose: handleClose, timeout});
+    const closeOnTimeoutProps = useCloseOnTimeout<HTMLDivElement>({onClose, timeout});
 
     const mods = {
         mobile,
-        appearing: status === ToastStatus.ShowingIndents || status === ToastStatus.ShowingHeight,
-        'show-animation': status === ToastStatus.ShowingHeight,
-        'hide-animation': status === ToastStatus.Hiding,
-        created: status !== ToastStatus.Creating,
         [type || 'default']: true,
     };
 
     const icon = renderIcon ? renderIcon(props) : renderIconByType({type});
 
     return (
-        <div
-            className={b(mods, className)}
-            {...statusProps}
-            {...heightProps}
-            {...closeOnTimeoutProps}
-        >
+        <div ref={ref} className={b(mods, className)} {...closeOnTimeoutProps}>
             {icon && <div className={b('icon-container')}>{icon}</div>}
             <div className={b('container')}>
                 <h3 className={b('title')}>{title}</h3>
@@ -224,15 +109,15 @@ export function Toast(props: ToastUnitedProps) {
                     <Button
                         view="flat-secondary"
                         className={b('btn-close')}
-                        onClick={handleClose}
+                        onClick={onClose}
                         extraProps={{'aria-label': i18n('label_close-button')}}
                     >
                         <Icon data={CrossIcon} size={CROSS_ICON_SIZE} />
                     </Button>
                 )}
                 {Boolean(content) && <div className={b('content')}>{content}</div>}
-                {renderActions({actions, onClose: handleClose})}
+                {renderActions({actions, onClose})}
             </div>
         </div>
     );
-}
+});
diff --git a/src/components/Toaster/ToastList/ToastAnimation.scss b/src/components/Toaster/ToastList/ToastAnimation.scss
new file mode 100644
index 00000000..8a9a5a67
--- /dev/null
+++ b/src/components/Toaster/ToastList/ToastAnimation.scss
@@ -0,0 +1,80 @@
+@use '../../variables';
+
+$transition-distance: 10px;
+$animation-duration: 0.6s;
+
+@mixin hidden-toast-height {
+    margin-bottom: 0;
+    padding: 0;
+    height: 0;
+}
+
+@mixin visible-toast-height {
+    margin-bottom: var(--yc-toaster-margin);
+    padding: var(--yc-toaster-padding);
+    height: var(--yc-toast-height);
+}
+
+@mixin hidden-toast-move($platform) {
+    opacity: 0;
+    @if ($platform == 'mobile') {
+        transform: translateY($transition-distance);
+    } @else {
+        transform: translateX($transition-distance);
+    }
+}
+
+@mixin visible-toast-move {
+    opacity: 1;
+    transform: translateX(0);
+}
+
+@each $platform in ('mobile', 'desktop') {
+    #{'.#{variables.$ns}toast-animation-#{$platform}'} {
+        &_enter {
+            opacity: 0;
+            position: absolute;
+        }
+
+        &_enter_active {
+            animation: #{variables.$ns}toast-enter-#{$platform}
+                $animation-duration
+                ease-out
+                forwards;
+            position: relative;
+        }
+
+        &_exit_active {
+            animation: #{variables.$ns}toast-exit-#{$platform} $animation-duration ease-in forwards;
+        }
+    }
+
+    @keyframes #{variables.$ns}toast-enter-#{$platform} {
+        0% {
+            @include hidden-toast-height;
+            @include hidden-toast-move($platform);
+        }
+        50% {
+            @include visible-toast-height;
+            @include hidden-toast-move($platform);
+        }
+        100% {
+            @include visible-toast-move;
+        }
+    }
+
+    @keyframes #{variables.$ns}toast-exit-#{$platform} {
+        0% {
+            @include visible-toast-height;
+            @include visible-toast-move;
+        }
+        50% {
+            @include visible-toast-height;
+            @include hidden-toast-move($platform);
+        }
+        100% {
+            @include hidden-toast-height;
+            @include hidden-toast-move($platform);
+        }
+    }
+}
diff --git a/src/components/Toaster/ToastList/ToastList.tsx b/src/components/Toaster/ToastList/ToastList.tsx
index ea008207..a25d43b8 100644
--- a/src/components/Toaster/ToastList/ToastList.tsx
+++ b/src/components/Toaster/ToastList/ToastList.tsx
@@ -1,8 +1,15 @@
 import React from 'react';
+import {TransitionGroup, CSSTransition} from 'react-transition-group';
+import {block} from '../../utils/cn';
+import {getCSSTransitionClassNames} from '../../utils/transition';
 import {InternalToastProps} from '../types';
 import {Toast} from '../Toast/Toast';
 
 import './ToastList.scss';
+import './ToastAnimation.scss';
+
+const desktopTransitionClassNames = getCSSTransitionClassNames(block('toast-animation-desktop'));
+const mobileTransitionClassNames = getCSSTransitionClassNames(block('toast-animation-mobile'));
 
 type ToastListProps = {
     removeCallback: (name: string) => void;
@@ -14,15 +21,30 @@ export function ToastList(props: ToastListProps) {
     const {toasts, mobile, removeCallback} = props;
 
     return (
-        <React.Fragment>
+        <TransitionGroup component={null}>
             {toasts.map((toast) => (
-                <Toast
-                    {...toast}
+                <CSSTransition
                     key={`${toast.name}_${toast.addedAt}`}
-                    mobile={mobile}
-                    removeCallback={() => removeCallback(toast.name)}
-                />
+                    nodeRef={toast.ref}
+                    classNames={mobile ? mobileTransitionClassNames : desktopTransitionClassNames}
+                    addEndListener={(done) =>
+                        toast.ref?.current?.addEventListener('animationend', done)
+                    }
+                    onEnter={() => updateToastHeightCssProperty(toast)}
+                    onExit={() => updateToastHeightCssProperty(toast)}
+                >
+                    <Toast {...toast} mobile={mobile} removeCallback={removeCallback} />
+                </CSSTransition>
             ))}
-        </React.Fragment>
+        </TransitionGroup>
     );
 }
+
+function updateToastHeightCssProperty(toast: InternalToastProps) {
+    if (toast.ref?.current) {
+        toast.ref.current.style.setProperty(
+            '--yc-toast-height',
+            `${toast.ref.current.offsetHeight}px`,
+        );
+    }
+}
diff --git a/src/components/Toaster/__stories__/ToasterShowcase.tsx b/src/components/Toaster/__stories__/ToasterShowcase.tsx
index 11bdf56f..4ba86727 100644
--- a/src/components/Toaster/__stories__/ToasterShowcase.tsx
+++ b/src/components/Toaster/__stories__/ToasterShowcase.tsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import {faker} from '@faker-js/faker/locale/en';
 import {block} from '../../utils/cn';
 import {Alarm, Info, Success} from '../../icons';
 import {ToasterComponent, ToastProps, useToaster} from '..';
@@ -208,6 +209,29 @@ export const ToasterDemo = ({
         setState((state) => ({...state, lastToastName: toastProps.name}));
     };
 
+    const createDynamicallyUpdatingToast = () => {
+        const toastProps = getToastProps({
+            name: 'UpdatingToast',
+            title: 'Updating Toast',
+            content: faker.lorem.sentences(),
+        });
+
+        toaster.add(toastProps);
+        setState((state) => ({...state, lastToastName: toastProps.name}));
+
+        const updateInterval = 75;
+        const update = () => {
+            if (!toaster.has(toastProps.name)) {
+                return;
+            }
+
+            const content = faker.lorem.sentences();
+            toaster.update(toastProps.name, {content});
+            window.setTimeout(update, updateInterval);
+        };
+        window.setTimeout(update, updateInterval);
+    };
+
     const overrideLastToast = () => {
         toaster.update(lastToastName, {
             title: 'Here is information not about payments at all...',
@@ -270,6 +294,12 @@ export const ToasterDemo = ({
         </Button>
     );
 
+    const dynamicallyUpdatingToast = (
+        <Button size="l" onClick={createDynamicallyUpdatingToast} style={btnStyle}>
+            Create dynamically updating toast
+        </Button>
+    );
+
     const overrideToastBtn = (
         <Button size="l" onClick={overrideLastToast} disabled={!lastToastName} style={btnStyle}>
             Override last toast
@@ -292,6 +322,7 @@ export const ToasterDemo = ({
             <p>{warningToastBtn}</p>
             <p>{errorToastBtn}</p>
             <p>{customToastBtn}</p>
+            <p>{dynamicallyUpdatingToast}</p>
             <p>{overrideToastBtn}</p>
             <p>{clearBtn}</p>
 
diff --git a/src/components/Toaster/types.ts b/src/components/Toaster/types.ts
index 2c9f8dbc..80755960 100644
--- a/src/components/Toaster/types.ts
+++ b/src/components/Toaster/types.ts
@@ -34,7 +34,7 @@ export type ToastProps = {
 
 export type InternalToastProps = ToastProps & {
     addedAt?: number;
-    updatesCounter?: number;
+    ref?: React.RefObject<HTMLDivElement>;
 };
 
 export interface ToasterContextMethods {
diff --git a/src/components/layout/Col/Col.tsx b/src/components/layout/Col/Col.tsx
index ecc7077f..87361fab 100644
--- a/src/components/layout/Col/Col.tsx
+++ b/src/components/layout/Col/Col.tsx
@@ -17,7 +17,7 @@ export interface ColProps extends MediaPartial<ColSize> {
 
 /**
  * How many columns of you 12-th column layout will take content.
- * Mast be used as a child of `Row` component.
+ * Must be used as a child of `Row` component.
  *
  * By default component takes all available space.
  * If you wont to specify static size to all media queries use `s` prop. In mobile first layout grid is first passible value.
diff --git a/src/components/layout/Container/Container.tsx b/src/components/layout/Container/Container.tsx
index 715e6d63..05bd278c 100644
--- a/src/components/layout/Container/Container.tsx
+++ b/src/components/layout/Container/Container.tsx
@@ -40,7 +40,7 @@ export interface ContainerProps {
 /**
  * Center you content in horizontal direction.
  *
- * > In most cases mast be one on the page.
+ * > In most cases must be one on the page.
  *
  * ```tsx
  * import {Container, Row, Col} from '@gravity-ui/uikit';
diff --git a/src/components/layout/types.ts b/src/components/layout/types.ts
index f945b6af..eff6600c 100644
--- a/src/components/layout/types.ts
+++ b/src/components/layout/types.ts
@@ -80,7 +80,7 @@ export interface LayoutTheme {
     /**
      * Override default breakpoints values.
      *
-     * @important **you mast override corresponding scss variables**
+     * @important **you must override corresponding scss variables**
      */
     breakpoints: MediaProps<number>;
     /**


Test patch:
diff --git a/src/components/Button/__tests__/Button.test.tsx b/src/components/Button/__tests__/Button.test.tsx
index b9204269..389879a8 100644
--- a/src/components/Button/__tests__/Button.test.tsx
+++ b/src/components/Button/__tests__/Button.test.tsx
@@ -48,12 +48,15 @@ describe('Button', () => {
         expect(button).not.toBeDisabled();
     });
 
-    test.each(new Array<ButtonSize>('s', 'm', 'l', 'xl'))('render with given "%s" size', (size) => {
-        render(<Button size={size} qa={qaId} />);
-        const button = screen.getByTestId(qaId);
+    test.each(new Array<ButtonSize>('xs', 's', 'm', 'l', 'xl'))(
+        'render with given "%s" size',
+        (size) => {
+            render(<Button size={size} qa={qaId} />);
+            const button = screen.getByTestId(qaId);
 
-        expect(button).toHaveClass(`yc-button_size_${size}`);
-    });
+            expect(button).toHaveClass(`yc-button_size_${size}`);
+        },
+    );
 
     test.each(new Array<ButtonView>(...buttonViews))('render with given "%s" view', (view) => {
         render(<Button view={view} qa={qaId} />);
diff --git a/src/components/Toaster/__tests__/ToasterProvider.test.tsx b/src/components/Toaster/__tests__/ToasterProvider.test.tsx
index a68eafe5..b3a1179a 100644
--- a/src/components/Toaster/__tests__/ToasterProvider.test.tsx
+++ b/src/components/Toaster/__tests__/ToasterProvider.test.tsx
@@ -67,6 +67,7 @@ describe('api.add', () => {
         act(() => {
             providerAPI.add(toastProps);
         });
+        fireAnimationEndEvent(toast, 'toast-hide-end');
 
         expect(toast).not.toBeInTheDocument();
 
@@ -239,13 +240,18 @@ describe('api.removeAll', () => {
             });
         });
 
-        expect(screen.getByRole('heading', {name: 'Test Toast'})).toBeInTheDocument();
-        expect(screen.getByRole('heading', {name: 'Test Toast2'})).toBeInTheDocument();
+        const toast1 = screen.getByRole('heading', {name: 'Test Toast'});
+        const toast2 = screen.getByRole('heading', {name: 'Test Toast2'});
+
+        expect(toast1).toBeInTheDocument();
+        expect(toast2).toBeInTheDocument();
 
         act(() => {
             providerAPI.removeAll();
         });
 
+        [toast1, toast2].forEach((toast) => fireAnimationEndEvent(toast, 'toast-hide-end'));
+
         expect(screen.queryByRole('heading', {name: 'Test Toast'})).not.toBeInTheDocument();
         expect(screen.queryByRole('heading', {name: 'Test Toast2'})).not.toBeInTheDocument();
     });
@@ -267,13 +273,11 @@ describe('api.has', () => {
             });
         });
 
-        const toast = getToast();
         expect(providerAPI.has(toastProps.name)).toBe(true);
 
         act(() => {
             providerAPI.remove(toastProps.name);
         });
-        tick(toast, 0);
 
         expect(providerAPI.has(toastProps.name)).toBe(false);
     });
@@ -288,16 +292,11 @@ describe('api.has', () => {
             });
         });
 
-        const toast = getToast();
         expect(providerAPI.has(toastProps.name)).toBe(true);
 
         act(() => {
             jest.advanceTimersByTime(toastTimeout);
         });
-        expect(providerAPI.has(toastProps.name)).toBe(true);
-
-        fireAnimationEndEvent(toast, 'toast-hide-end');
-
         expect(providerAPI.has(toastProps.name)).toBe(false);
     });
 });