From 3e94231a93009cf16454094fb8a6bf501088b41b Mon Sep 17 00:00:00 2001
From: StepanKirichenko <asv-dgr-2@yandex-team.ru>
Date: Wed, 17 Jul 2024 12:30:31 +0300
Subject: [PATCH 1/3] feat(Drawer): resizer, portal rendering, hide veil

---
 src/components/Drawer/Drawer.scss             |  54 +++++++
 src/components/Drawer/Drawer.tsx              | 126 +++++++++++----
 src/components/Drawer/README.md               |  48 +++++-
 .../Drawer/__stories__/DisablePortal.scss     |  43 +++++
 .../Drawer/__stories__/DisablePortal.tsx      |  60 +++++++
 .../Drawer/__stories__/Drawer.stories.tsx     |  12 ++
 .../Drawer/__stories__/DrawerShowcase.scss    |   1 -
 .../Drawer/__stories__/DrawerShowcase.tsx     |   8 +-
 .../Drawer/__stories__/HideVeil.scss          |  41 +++++
 .../Drawer/__stories__/HideVeil.tsx           |  43 +++++
 .../Drawer/__stories__/ResizableItem.scss     |  40 +++++
 .../Drawer/__stories__/ResizableItem.tsx      |  58 +++++++
 src/components/Drawer/__stories__/moc.tsx     |  22 +++
 src/components/Drawer/utils.ts                | 150 ++++++++++++++++++
 14 files changed, 668 insertions(+), 38 deletions(-)
 create mode 100644 src/components/Drawer/__stories__/DisablePortal.scss
 create mode 100644 src/components/Drawer/__stories__/DisablePortal.tsx
 create mode 100644 src/components/Drawer/__stories__/HideVeil.scss
 create mode 100644 src/components/Drawer/__stories__/HideVeil.tsx
 create mode 100644 src/components/Drawer/__stories__/ResizableItem.scss
 create mode 100644 src/components/Drawer/__stories__/ResizableItem.tsx
 create mode 100644 src/components/Drawer/__stories__/moc.tsx
 create mode 100644 src/components/Drawer/utils.ts

diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss
index 7662a52..15280c0 100644
--- a/src/components/Drawer/Drawer.scss
+++ b/src/components/Drawer/Drawer.scss
@@ -3,6 +3,13 @@
 $block: '.#{variables.$ns}drawer';
 
 #{$block} {
+    pointer-events: none;
+
+    --_--resizer-color: var(--g-color-base-generic);
+    --_--resizer-handle-color: var(--g-color-line-generic);
+    --_--resizer-handle-color-hover: var(--g-color-line-generic-hover);
+    --_--resizer-z-index: 100;
+
     &__item {
         position: absolute;
         left: 0;
@@ -11,6 +18,7 @@ $block: '.#{variables.$ns}drawer';
         height: 100%;
         will-change: transform;
         background-color: var(--g-color-base-background);
+        pointer-events: initial;
 
         &_direction_right {
             left: auto;
@@ -64,8 +72,13 @@ $block: '.#{variables.$ns}drawer';
 
     &__veil {
         position: absolute;
+        pointer-events: initial;
         inset: 0;
         background-color: var(--g-color-sfx-veil);
+
+        &_hidden {
+            display: none;
+        }
     }
 
     &__veil-transition-enter {
@@ -89,4 +102,45 @@ $block: '.#{variables.$ns}drawer';
     &__veil-transition-exit-done {
         visibility: hidden;
     }
+
+    &__resizer-handle {
+        width: 2px;
+        height: 28px;
+
+        background: var(--gn-drawer-item-resizer-handle-color, var(--_--resizer-handle-color));
+        border-radius: 2px;
+    }
+
+    &__resizer {
+        position: absolute;
+        top: 0;
+        z-index: var(--gn-drawer-item-resizer-z-index, var(--_--resizer-z-index));
+
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+
+        width: 8px;
+        height: 100%;
+
+        cursor: col-resize;
+
+        background: var(--gn-drawer-item-resizer-color, var(--_--resizer-color));
+
+        &_direction_right {
+            left: 0;
+        }
+
+        &_direction_left {
+            right: 0;
+        }
+    }
+
+    &__resizer:hover &__resizer-handle {
+        background: var(
+            --gn-drawer-item-resizer-handle-color-hover,
+            var(--_--resizer-handle-color-hover)
+        );
+    }
 }
diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx
index 6d3c979..2be3543 100644
--- a/src/components/Drawer/Drawer.tsx
+++ b/src/components/Drawer/Drawer.tsx
@@ -1,17 +1,17 @@
 import React from 'react';
 
-import {useBodyScrollLock} from '@gravity-ui/uikit';
+import {Portal, useBodyScrollLock, useForkRef} from '@gravity-ui/uikit';
 import {CSSTransition, Transition} from 'react-transition-group';
 
 import {block} from '../utils/cn';
 
+import {type DrawerDirection, useResizableDrawerItem} from './utils';
+
 import './Drawer.scss';
 
 const b = block('drawer');
 const TIMEOUT = 300;
 
-export type DrawerDirection = 'right' | 'left';
-
 export interface DrawerItemProps {
     /** Unique identifier for the drawer item. */
     id: string;
@@ -35,32 +35,82 @@ export interface DrawerItemProps {
 
     /** Additional custom class name applied to the drawer item. */
     className?: string;
-}
 
-export const DrawerItem: React.FC<DrawerItemProps> = ({
-    visible,
-    content,
-    children,
-    direction = 'left',
-    className,
-}) => {
-    const itemRef = React.useRef<HTMLDivElement>(null);
-    const cssDirection = direction === 'left' ? undefined : direction;
+    /** Determines whether the drawer item can be resized */
+    resizable?: boolean;
 
-    return (
-        <CSSTransition
-            in={visible}
-            timeout={TIMEOUT}
-            unmountOnExit
-            classNames={b('item-transition', {direction: cssDirection})}
-            nodeRef={itemRef}
-        >
-            <div ref={itemRef} className={b('item', {direction: cssDirection}, className)}>
-                {children ?? content}
+    /**
+     * The width of the resizable drawer item.
+     * If not provided, the width will be stored internally.
+     */
+    width?: number;
+
+    /**
+     * Called at the end of resizing. Can be used to save the new width.
+     * @param width The new width of the drawer item
+     */
+    onResize?: (width: number) => void;
+
+    /** The minimum width of the resizable drawer item */
+    minResizeWidth?: number;
+
+    /** The maximum width of the resizable drawer item */
+    maxResizeWidth?: number;
+}
+
+export const DrawerItem = React.forwardRef<HTMLDivElement, DrawerItemProps>(
+    function DrawerItem(props, ref) {
+        const {
+            visible,
+            content,
+            children,
+            direction = 'left',
+            className,
+            resizable,
+            width,
+            minResizeWidth,
+            maxResizeWidth,
+            onResize,
+        } = props;
+
+        const itemRef = React.useRef<HTMLDivElement>(null);
+        const handleRef = useForkRef(ref, itemRef);
+        const cssDirection = direction === 'left' ? undefined : direction;
+
+        const {resizedWidth, resizerHandlers} = useResizableDrawerItem({
+            direction,
+            width,
+            minResizeWidth,
+            maxResizeWidth,
+            onResize,
+        });
+
+        const resizerElement = resizable ? (
+            <div className={b('resizer', {direction})} {...resizerHandlers}>
+                <div className={b('resizer-handle')} />
             </div>
-        </CSSTransition>
-    );
-};
+        ) : null;
+
+        return (
+            <CSSTransition
+                in={visible}
+                timeout={TIMEOUT}
+                unmountOnExit
+                classNames={b('item-transition', {direction: cssDirection})}
+                nodeRef={itemRef}
+            >
+                <div
+                    ref={handleRef}
+                    className={b('item', {direction: cssDirection}, className)}
+                    style={{width: resizable ? `${resizedWidth}px` : undefined}}
+                >
+                    {resizerElement}
+                    {children ?? content}
+                </div>
+            </CSSTransition>
+        );
+    },
+);
 
 type DrawerChild = React.ReactElement<DrawerItemProps>;
 
@@ -85,6 +135,12 @@ export interface DrawerProps {
 
     /** Optional callback function that is called when the escape key is pressed, if the drawer is open. */
     onEscape?: () => void;
+
+    /** Optional flag to hide the background darkening */
+    hideVeil?: boolean;
+
+    /** Optional flag to not use `Portal` for drawer */
+    disablePortal?: boolean;
 }
 
 export const Drawer: React.FC<DrawerProps> = ({
@@ -94,6 +150,8 @@ export const Drawer: React.FC<DrawerProps> = ({
     onVeilClick,
     onEscape,
     preventScrollBody = true,
+    hideVeil,
+    disablePortal,
 }) => {
     let someItemVisible = false;
     React.Children.forEach(children, (child) => {
@@ -124,7 +182,7 @@ export const Drawer: React.FC<DrawerProps> = ({
     const containerRef = React.useRef<HTMLDivElement>(null);
     const veilRef = React.useRef<HTMLDivElement>(null);
 
-    return (
+    const drawer = (
         <Transition
             in={someItemVisible}
             timeout={{enter: 0, exit: TIMEOUT}}
@@ -143,7 +201,11 @@ export const Drawer: React.FC<DrawerProps> = ({
                             classNames={b('veil-transition')}
                             nodeRef={veilRef}
                         >
-                            <div ref={veilRef} className={b('veil')} onClick={onVeilClick} />
+                            <div
+                                ref={veilRef}
+                                className={b('veil', {hidden: hideVeil})}
+                                onClick={onVeilClick}
+                            />
                         </CSSTransition>
                         {React.Children.map(children, (child) => {
                             if (
@@ -163,4 +225,12 @@ export const Drawer: React.FC<DrawerProps> = ({
             }}
         </Transition>
     );
+
+    if (disablePortal) {
+        return drawer;
+    }
+
+    return <Portal>{drawer}</Portal>;
 };
+
+export type {DrawerDirection} from './utils';
diff --git a/src/components/Drawer/README.md b/src/components/Drawer/README.md
index d1a5be4..269c32f 100644
--- a/src/components/Drawer/README.md
+++ b/src/components/Drawer/README.md
@@ -32,20 +32,50 @@ const App = () => {
 export default App;
 ```
 
+An example of a resizable drawer item
+
+```tsx
+import React from 'react';
+import {Drawer, DrawerItem} from '@gravity-ui/navigation';
+
+const App = () => {
+  const [isVisible, setVisible] = React.useState(false);
+  const [width, setWidth] = React.useState(400);
+
+  return (
+    <div>
+      <button onClick={() => setVisible(true)}>Open Drawer</button>
+      <Drawer onEscape={() => setVisible(false)} onVeilClick={() => setVisible(false)}>
+        <DrawerItem id="item1" visible={isVisible} resizable width={width} onResize={setWidth}>
+          <p>Content of the drawer</p>
+        </DrawerItem>
+      </Drawer>
+    </div>
+  );
+};
+
+export default App;
+```
+
 ## Components
 
 The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.
 
 ### `DrawerItem` Props
 
-| Name      | Description                                                                             |       Type        | Default |
-| :-------- | :-------------------------------------------------------------------------------------- | :---------------: | :-----: |
-| id        | Unique identifier for the drawer item.                                                  |     `string`      |         |
-| children  | Content to be displayed within the drawer item, preferable over the deprecated content. | `React.ReactNode` |         |
-| content   | (deprecated) use children. Content to be displayed within the drawer item.              | `React.ReactNode` |         |
-| visible   | Determines whether the drawer item is visible or hidden.                                |     `boolean`     |         |
-| direction | Specifies the direction from which the drawer should slide in (left or right).          | `DrawerDirection` | `left`  |
-| className | HTML `class` attribute                                                                  |     `string`      |         |
+| Name           | Description                                                                             |           Type            | Default |
+| :------------- | :-------------------------------------------------------------------------------------- | :-----------------------: | :-----: |
+| id             | Unique identifier for the drawer item.                                                  |         `string`          |         |
+| children       | Content to be displayed within the drawer item, preferable over the deprecated content. |     `React.ReactNode`     |         |
+| content        | (deprecated) use children. Content to be displayed within the drawer item.              |     `React.ReactNode`     |         |
+| visible        | Determines whether the drawer item is visible or hidden.                                |         `boolean`         |         |
+| direction      | Specifies the direction from which the drawer should slide in (left or right).          |     `DrawerDirection`     | `left`  |
+| className      | HTML `class` attribute                                                                  |         `string`          |         |
+| resizable      | Determines whether the drawer item can be resized                                       |         `boolean`         |         |
+| width          | The width of the resizable drawer item                                                  |         `number`          |         |
+| onResize       | Callback function called at the end of resizing. Can be used to save the new width.     | `(width: number) => void` |         |
+| minResizeWidth | The minimum width of the resizable drawer item                                          |         `number`          |         |
+| maxResizeWidth | The maximum width of the resizable drawer item                                          |         `number`          |         |
 
 ### `Drawer` Props
 
@@ -57,3 +87,5 @@ The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.
 | style             | Optional inline styles to be applied to the drawer component.                                    |        `React.CSSProperties`        |         |
 | onVeilClick       | Optional callback function that is called when the veil (overlay) is clicked.                    | `(event: React.MouseEvent) => void` |         |
 | onEscape          | Optional callback function that is called when the escape key is pressed, if the drawer is open. |            `() => void`             |         |
+| hideVeil          | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
+| disablePortal     | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
diff --git a/src/components/Drawer/__stories__/DisablePortal.scss b/src/components/Drawer/__stories__/DisablePortal.scss
new file mode 100644
index 0000000..602f896
--- /dev/null
+++ b/src/components/Drawer/__stories__/DisablePortal.scss
@@ -0,0 +1,43 @@
+.disable-portal {
+    width: 100dvw;
+    height: 100dvh;
+
+    display: flex;
+    flex-direction: column;
+
+    &__header {
+        width: 100%;
+        padding: 20px;
+        border-bottom: 1px solid var(--g-color-line-generic);
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    &__container {
+        position: relative;
+        padding: 16px;
+        width: 600px;
+        height: 400px;
+        border: 1px solid var(--g-color-line-brand);
+    }
+
+    &__drawer {
+        position: absolute;
+        inset: 0;
+        overflow: hidden;
+    }
+
+    &__item {
+        width: 400px;
+        height: 100%;
+        overflow-x: hidden;
+    }
+
+    &__item-content {
+        box-sizing: border-box;
+        padding: 8px 16px;
+        height: 100%;
+        overflow-y: scroll;
+    }
+}
diff --git a/src/components/Drawer/__stories__/DisablePortal.tsx b/src/components/Drawer/__stories__/DisablePortal.tsx
new file mode 100644
index 0000000..7aca669
--- /dev/null
+++ b/src/components/Drawer/__stories__/DisablePortal.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+
+import {Button, Checkbox, RadioButton} from '@gravity-ui/uikit';
+
+import {cn} from '../../utils/cn';
+import {Drawer, DrawerItem} from '../Drawer';
+
+import {PlaceholderText} from './moc';
+
+import './DisablePortal.scss';
+
+const b = cn('disable-portal');
+
+export function DisablePortalShowcase() {
+    const [visible, setVisible] = React.useState(true);
+    const [direction, setDirection] = React.useState<'left' | 'right'>('right');
+    const [disablePortal, setDisablePortal] = React.useState(true);
+
+    return (
+        <div className={b()}>
+            <div className={b('header')}>
+                <Button view="action" onClick={() => setVisible((v) => !v)}>
+                    {visible ? 'Hide' : 'Show'}
+                </Button>
+                <RadioButton
+                    value={direction}
+                    options={[
+                        {value: 'left', content: 'Left'},
+                        {value: 'right', content: 'Right'},
+                    ]}
+                    onUpdate={setDirection}
+                />
+                <Checkbox
+                    content="Disable protal"
+                    checked={disablePortal}
+                    onUpdate={(v) => setDisablePortal(v)}
+                />
+            </div>
+            <div className={b('container')}>
+                <p>Container area for drawer with disablePortal</p>
+                <Drawer
+                    className={b('drawer')}
+                    disablePortal={disablePortal}
+                    onVeilClick={() => setVisible(false)}
+                >
+                    <DrawerItem
+                        id="item"
+                        direction={direction}
+                        className={b('item')}
+                        visible={visible}
+                    >
+                        <div className={b('item-content')} tabIndex={0}>
+                            <PlaceholderText />
+                        </div>
+                    </DrawerItem>
+                </Drawer>
+            </div>
+        </div>
+    );
+}
diff --git a/src/components/Drawer/__stories__/Drawer.stories.tsx b/src/components/Drawer/__stories__/Drawer.stories.tsx
index 64286dd..d912524 100644
--- a/src/components/Drawer/__stories__/Drawer.stories.tsx
+++ b/src/components/Drawer/__stories__/Drawer.stories.tsx
@@ -2,7 +2,10 @@ import React from 'react';
 
 import type {Meta, StoryFn} from '@storybook/react';
 
+import {DisablePortalShowcase} from './DisablePortal';
 import {DrawerShowcase} from './DrawerShowcase';
+import {HideVeilShowcase} from './HideVeil';
+import {ResizableItemShowcase} from './ResizableItem';
 
 export default {
     title: 'components/Drawer',
@@ -11,3 +14,12 @@ export default {
 
 const ShowcaseTemplate: StoryFn = () => <DrawerShowcase />;
 export const Showcase = ShowcaseTemplate.bind({});
+
+const ResizableItemTemplate: StoryFn = () => <ResizableItemShowcase />;
+export const ResizableItem = ResizableItemTemplate.bind({});
+
+const DisablePortalTemplate: StoryFn = () => <DisablePortalShowcase />;
+export const DisablePortal = DisablePortalTemplate.bind({});
+
+const HideVeilTemplate: StoryFn = () => <HideVeilShowcase />;
+export const HideVeil = HideVeilTemplate.bind({});
diff --git a/src/components/Drawer/__stories__/DrawerShowcase.scss b/src/components/Drawer/__stories__/DrawerShowcase.scss
index 5f36c89..c9e9f0e 100644
--- a/src/components/Drawer/__stories__/DrawerShowcase.scss
+++ b/src/components/Drawer/__stories__/DrawerShowcase.scss
@@ -4,7 +4,6 @@
     position: relative;
 
     &__header {
-        width: 100%;
         padding: 20px;
         border-bottom: 1px solid var(--g-color-line-generic);
     }
diff --git a/src/components/Drawer/__stories__/DrawerShowcase.tsx b/src/components/Drawer/__stories__/DrawerShowcase.tsx
index 64ecac8..2fad0a8 100644
--- a/src/components/Drawer/__stories__/DrawerShowcase.tsx
+++ b/src/components/Drawer/__stories__/DrawerShowcase.tsx
@@ -14,6 +14,7 @@ export function DrawerShowcase() {
     const [visible2, setVisible2] = React.useState<boolean>(false);
 
     const [direction, setDirection] = React.useState<string>('left');
+    const [direction2, setDirection2] = React.useState<string>('left');
 
     const hideAll = React.useCallback(() => {
         setVisible1(false);
@@ -35,6 +36,11 @@ export function DrawerShowcase() {
                     <RadioButton.Option value="left">left</RadioButton.Option>
                     <RadioButton.Option value="right">right</RadioButton.Option>
                 </RadioButton>
+                &nbsp;&nbsp; Direction2: &nbsp;
+                <RadioButton value={direction2} onUpdate={setDirection2}>
+                    <RadioButton.Option value="left">left</RadioButton.Option>
+                    <RadioButton.Option value="right">right</RadioButton.Option>
+                </RadioButton>
             </div>
             <Drawer className={b('drawer')} onVeilClick={hideAll} onEscape={hideAll}>
                 <DrawerItem
@@ -49,7 +55,7 @@ export function DrawerShowcase() {
                     id="item-2"
                     className={b('item-2')}
                     content=""
-                    direction={direction as DrawerItemProps['direction']}
+                    direction={direction2 as DrawerItemProps['direction']}
                 />
             </Drawer>
         </div>
diff --git a/src/components/Drawer/__stories__/HideVeil.scss b/src/components/Drawer/__stories__/HideVeil.scss
new file mode 100644
index 0000000..b4095f4
--- /dev/null
+++ b/src/components/Drawer/__stories__/HideVeil.scss
@@ -0,0 +1,41 @@
+.hide-veil {
+    width: 100dvw;
+    height: 100dvh;
+
+    display: flex;
+    flex-direction: column;
+
+    &__header {
+        padding: 20px;
+        border-bottom: 1px solid var(--g-color-line-generic);
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    &__container {
+        position: relative;
+        flex-grow: 1;
+        padding: 16px;
+    }
+
+    &__drawer {
+        position: absolute;
+        inset: 0;
+        overflow: hidden;
+    }
+
+    &__item {
+        width: 400px;
+        height: 100%;
+        overflow-x: hidden;
+    }
+
+    &__item-content {
+        box-sizing: border-box;
+        padding: 8px 16px;
+        border-left: 1px solid var(--g-color-line-generic);
+        height: 100%;
+        overflow-y: scroll;
+    }
+}
diff --git a/src/components/Drawer/__stories__/HideVeil.tsx b/src/components/Drawer/__stories__/HideVeil.tsx
new file mode 100644
index 0000000..aa1fca2
--- /dev/null
+++ b/src/components/Drawer/__stories__/HideVeil.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import {Button, Checkbox} from '@gravity-ui/uikit';
+
+import {cn} from '../../utils/cn';
+import {Drawer, DrawerItem} from '../Drawer';
+
+import {PlaceholderText} from './moc';
+
+import './HideVeil.scss';
+
+const b = cn('hide-veil');
+
+export function HideVeilShowcase() {
+    const [visible, setVisible] = React.useState(true);
+    const [hideVeil, setHideVeil] = React.useState(true);
+
+    return (
+        <div className={b()}>
+            <div className={b('header')}>
+                <Button view="action" onClick={() => setVisible((v) => !v)}>
+                    {visible ? 'Hide' : 'Show'}
+                </Button>
+                <Checkbox content="Hide veil" checked={hideVeil} onUpdate={setHideVeil} />
+            </div>
+            <div className={b('container')}>
+                <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, quos!</p>
+                <Drawer
+                    className={b('drawer')}
+                    hideVeil={hideVeil}
+                    disablePortal
+                    onVeilClick={() => setVisible(false)}
+                >
+                    <DrawerItem id="item" className={b('item')} direction="right" visible={visible}>
+                        <div className={b('item-content')} tabIndex={0}>
+                            <PlaceholderText />
+                        </div>
+                    </DrawerItem>
+                </Drawer>
+            </div>
+        </div>
+    );
+}
diff --git a/src/components/Drawer/__stories__/ResizableItem.scss b/src/components/Drawer/__stories__/ResizableItem.scss
new file mode 100644
index 0000000..f1cb476
--- /dev/null
+++ b/src/components/Drawer/__stories__/ResizableItem.scss
@@ -0,0 +1,40 @@
+.resizable-item {
+    width: 100dvw;
+    height: 100dvh;
+
+    display: flex;
+    flex-direction: column;
+
+    &__header {
+        padding: 20px;
+        border-bottom: 1px solid var(--g-color-line-generic);
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    &__container {
+        position: relative;
+        padding: 16px;
+        flex-grow: 1;
+    }
+
+    &__drawer {
+        position: absolute;
+        inset: 0;
+        overflow: hidden;
+    }
+
+    &__item {
+        width: 400px;
+        height: 100%;
+        overflow-x: hidden;
+    }
+
+    &__item-content {
+        box-sizing: border-box;
+        padding: 8px 16px;
+        height: 100%;
+        overflow-y: scroll;
+    }
+}
diff --git a/src/components/Drawer/__stories__/ResizableItem.tsx b/src/components/Drawer/__stories__/ResizableItem.tsx
new file mode 100644
index 0000000..e2aae31
--- /dev/null
+++ b/src/components/Drawer/__stories__/ResizableItem.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+
+import {Button, Checkbox, RadioButton} from '@gravity-ui/uikit';
+
+import {cn} from '../../utils/cn';
+import {Drawer, DrawerItem} from '../Drawer';
+
+import {PlaceholderText} from './moc';
+
+import './ResizableItem.scss';
+
+const b = cn('resizable-item');
+
+export function ResizableItemShowcase() {
+    const [visible, setVisible] = React.useState(true);
+    const [direction, setDirection] = React.useState<'left' | 'right'>('right');
+    const [resizable, setResizable] = React.useState(true);
+    const [width, setWidth] = React.useState(400);
+
+    return (
+        <div className={b()}>
+            <div className={b('header')}>
+                <Button view="action" onClick={() => setVisible((v) => !v)}>
+                    {visible ? 'Hide' : 'Show'}
+                </Button>
+                <RadioButton
+                    value={direction}
+                    options={[
+                        {value: 'left', content: 'Left'},
+                        {value: 'right', content: 'Right'},
+                    ]}
+                    onUpdate={setDirection}
+                />
+                <Checkbox content="Resizable" checked={resizable} onUpdate={setResizable} />
+            </div>
+            <div className={b('container')}>
+                <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, quidem.</p>
+                <Drawer className={b('drawer')} disablePortal onVeilClick={() => setVisible(false)}>
+                    <DrawerItem
+                        id="item"
+                        direction={direction}
+                        className={b('item')}
+                        visible={visible}
+                        resizable={resizable}
+                        width={width}
+                        onResize={setWidth}
+                        minResizeWidth={300}
+                        maxResizeWidth={800}
+                    >
+                        <div className={b('item-content')} tabIndex={0}>
+                            <PlaceholderText />
+                        </div>
+                    </DrawerItem>
+                </Drawer>
+            </div>
+        </div>
+    );
+}
diff --git a/src/components/Drawer/__stories__/moc.tsx b/src/components/Drawer/__stories__/moc.tsx
new file mode 100644
index 0000000..f757704
--- /dev/null
+++ b/src/components/Drawer/__stories__/moc.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+export function PlaceholderText() {
+    return (
+        <>
+            Lorem ipsum dolor sit amet consectetur, adipisicing elit. Deserunt iste dolores tenetur
+            perspiciatis nihil, dolorem corrupti veritatis quia odit dignissimos itaque quisquam ad
+            consequuntur voluptas odio totam similique quibusdam? Esse temporibus omnis pariatur
+            quas in? Iusto nesciunt dolor, voluptas placeat sed iure molestias repellendus id
+            officiis aliquam! Vero ad corporis distinctio, explicabo tempore reiciendis obcaecati
+            quaerat debitis inventore quidem fugit illum repellat deleniti soluta nihil iste commodi
+            labore at. Asperiores officiis accusamus accusantium, vitae nemo adipisci modi illum!
+            Exercitationem enim accusamus fuga totam quod minus itaque eius vitae modi aliquam
+            doloribus nostrum, nobis illo nisi inventore odio harum perspiciatis adipisci iusto.
+            Sapiente quo aliquam aut officiis quas odit iusto, quia accusantium voluptatibus qui
+            temporibus harum dicta? Dignissimos pariatur commodi, consectetur laborum, tempore porro
+            molestiae alias non dolores ab earum impedit. Placeat culpa quibusdam consequuntur
+            molestiae saepe nostrum laudantium delectus, doloremque provident ad corrupti mollitia,
+            expedita repellendus necessitatibus autem soluta aliquid.
+        </>
+    );
+}
diff --git a/src/components/Drawer/utils.ts b/src/components/Drawer/utils.ts
new file mode 100644
index 0000000..203a3a3
--- /dev/null
+++ b/src/components/Drawer/utils.ts
@@ -0,0 +1,150 @@
+import * as React from 'react';
+
+export const DRAWER_ITEM_MIN_RESIZE_WIDTH = 200;
+export const DRAWER_ITEM_MAX_RESIZE_WIDTH = 800;
+export const DRAWER_ITEM_INITIAL_RESIZE_WIDTH = 400;
+
+export type DrawerDirection = 'right' | 'left';
+export type OnResizeHandler = (width: number) => void;
+
+function getEventClientX(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) {
+    return 'touches' in e ? e.touches[0]?.clientX ?? 0 : e.clientX;
+}
+
+export interface UseResizeHandlersParams {
+    onStart: () => void;
+    onMove: (delta: number) => void;
+    onEnd: (delta: number) => void;
+}
+
+export function useResizeHandlers({onStart, onMove, onEnd}: UseResizeHandlersParams) {
+    const initialXPosition = React.useRef(0);
+    const currentXPosition = React.useRef(0);
+
+    const handleMove = React.useCallback(
+        (e: MouseEvent | TouchEvent) => {
+            const currentX = getEventClientX(e);
+
+            if (currentXPosition.current === currentX) {
+                return;
+            }
+
+            currentXPosition.current = currentX;
+
+            const delta = initialXPosition.current - currentX;
+
+            onMove(delta);
+        },
+        [onMove],
+    );
+
+    const handleEnd = React.useCallback(
+        (e: MouseEvent | TouchEvent) => {
+            window.removeEventListener('mousemove', handleMove);
+            window.removeEventListener('touchmove', handleMove);
+
+            document.body.style.removeProperty('user-select');
+            document.body.style.removeProperty('-webkit-user-select');
+            document.body.style.removeProperty('cursor');
+
+            const currentX = getEventClientX(e);
+            const delta = initialXPosition.current - currentX;
+
+            onEnd(delta);
+        },
+        [handleMove, onEnd],
+    );
+
+    const handleStart = React.useCallback(
+        (e: React.MouseEvent | React.TouchEvent) => {
+            const currentX = getEventClientX(e);
+
+            initialXPosition.current = currentX;
+            currentXPosition.current = currentX;
+
+            window.addEventListener('mouseup', handleEnd, {once: true});
+            window.addEventListener('touchend', handleEnd, {once: true});
+            window.addEventListener('touchcancel', handleEnd, {once: true});
+
+            window.addEventListener('mousemove', handleMove);
+            window.addEventListener('touchmove', handleMove);
+
+            document.body.style.setProperty('user-select', 'none');
+            document.body.style.setProperty('-webkit-user-select', 'none');
+            document.body.style.setProperty('cursor', 'col-resize');
+
+            onStart();
+        },
+        [handleEnd, handleMove, onStart],
+    );
+
+    return {
+        onMouseDown: handleStart,
+        onTouchStart: handleStart,
+    };
+}
+
+export interface UseResizableDrawerItemParams {
+    direction?: DrawerDirection;
+    width?: number;
+    minResizeWidth?: number;
+    maxResizeWidth?: number;
+    onResize?: OnResizeHandler;
+}
+
+export function useResizableDrawerItem(params: UseResizableDrawerItemParams) {
+    const {
+        direction,
+        width,
+        minResizeWidth = DRAWER_ITEM_MIN_RESIZE_WIDTH,
+        maxResizeWidth = DRAWER_ITEM_MAX_RESIZE_WIDTH,
+        onResize,
+    } = params;
+
+    const [isResizing, setIsResizing] = React.useState(false);
+    const [resizeDelta, setResizeDelta] = React.useState(0);
+    const [internalWidth, setInternalWidth] = React.useState(
+        width ?? DRAWER_ITEM_INITIAL_RESIZE_WIDTH,
+    );
+
+    const getClampedWidth = React.useCallback(
+        (width: number) => Math.min(Math.max(width, minResizeWidth), maxResizeWidth),
+        [minResizeWidth, maxResizeWidth],
+    );
+
+    const getResizedWidth = React.useCallback(
+        (delta: number) => {
+            const signedDelta = direction === 'right' ? delta : -delta;
+            const newWidth = (width ?? internalWidth) + signedDelta;
+            return getClampedWidth(newWidth);
+        },
+        [width, internalWidth, direction, getClampedWidth],
+    );
+
+    const onStart = React.useCallback(() => {
+        setIsResizing(true);
+        setResizeDelta(0);
+    }, [setIsResizing, setResizeDelta]);
+
+    const onMove = React.useCallback((delta: number) => {
+        setResizeDelta(delta);
+    }, []);
+
+    const onEnd = React.useCallback(
+        (delta: number) => {
+            const newWidth = getResizedWidth(delta);
+            setIsResizing(false);
+            setInternalWidth(newWidth);
+            onResize?.(newWidth);
+        },
+        [setIsResizing, setInternalWidth, getResizedWidth, onResize],
+    );
+
+    const displayWidth = isResizing
+        ? getResizedWidth(resizeDelta)
+        : getClampedWidth(width ?? internalWidth);
+
+    const handlers = useResizeHandlers({onStart, onMove, onEnd});
+
+    return {resizedWidth: displayWidth, resizerHandlers: handlers};
+}

From 3cee0f880cac41e17140a82529f581370a559362 Mon Sep 17 00:00:00 2001
From: StepanKirichenko <asv-dgr-2@yandex-team.ru>
Date: Thu, 18 Jul 2024 14:33:03 +0300
Subject: [PATCH 2/3] fix: code review

---
 src/components/Drawer/Drawer.scss |  3 ++-
 src/components/Drawer/Drawer.tsx  |  2 --
 src/components/Drawer/README.md   | 11 +++++++++++
 src/components/Drawer/index.ts    |  2 ++
 src/components/index.ts           |  2 +-
 5 files changed, 16 insertions(+), 4 deletions(-)
 create mode 100644 src/components/Drawer/index.ts

diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss
index 15280c0..92e4ef0 100644
--- a/src/components/Drawer/Drawer.scss
+++ b/src/components/Drawer/Drawer.scss
@@ -5,6 +5,7 @@ $block: '.#{variables.$ns}drawer';
 #{$block} {
     pointer-events: none;
 
+    --_--resizer-width: 8px;
     --_--resizer-color: var(--g-color-base-generic);
     --_--resizer-handle-color: var(--g-color-line-generic);
     --_--resizer-handle-color-hover: var(--g-color-line-generic-hover);
@@ -121,7 +122,7 @@ $block: '.#{variables.$ns}drawer';
         justify-content: center;
         align-items: center;
 
-        width: 8px;
+        width: var(--gn-drawer-item-resizer-width, var(--_--resizer-width));
         height: 100%;
 
         cursor: col-resize;
diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx
index 2be3543..30536b7 100644
--- a/src/components/Drawer/Drawer.tsx
+++ b/src/components/Drawer/Drawer.tsx
@@ -232,5 +232,3 @@ export const Drawer: React.FC<DrawerProps> = ({
 
     return <Portal>{drawer}</Portal>;
 };
-
-export type {DrawerDirection} from './utils';
diff --git a/src/components/Drawer/README.md b/src/components/Drawer/README.md
index 269c32f..4e63e08 100644
--- a/src/components/Drawer/README.md
+++ b/src/components/Drawer/README.md
@@ -89,3 +89,14 @@ The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.
 | onEscape          | Optional callback function that is called when the escape key is pressed, if the drawer is open. |            `() => void`             |         |
 | hideVeil          | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
 | disablePortal     | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
+
+## CSS API
+
+| Name                                          | Description                                                 |            Default             |
+| :-------------------------------------------- | :---------------------------------------------------------- | :----------------------------: |
+| Resizer                                       |                                                             |                                |
+| `--gn-drawer-item-resizer-width`              | The width of the resizer element                            |              8px               |
+| `--gn-drawer-item-resizer-color`              | The color of the resizer element                            |    `--g-color-base-generic`    |
+| `--gn-drawer-item-resizer-handle-color`       | The color of the resizer handle                             |    `--g-color-line-generic`    |
+| `--gn-drawer-item-resizer-handle-color-hover` | The color of the resizer handle when the resizer is hovered | `--g-color-line-generic-hover` |
+| `--gn-drawer-item-resizer-z-index`            | z-index of the resizer element                              |              100               |
diff --git a/src/components/Drawer/index.ts b/src/components/Drawer/index.ts
new file mode 100644
index 0000000..59f65bf
--- /dev/null
+++ b/src/components/Drawer/index.ts
@@ -0,0 +1,2 @@
+export * from './Drawer';
+export type {DrawerDirection} from './utils';
diff --git a/src/components/index.ts b/src/components/index.ts
index 8405b9e..ae85b73 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,11 +1,11 @@
 export {AsideHeader} from './AsideHeader/AsideHeader';
 export type {AsideHeaderProps} from './AsideHeader/types';
 export {AsideHeaderContextProvider, useAsideHeaderContext} from './AsideHeader/AsideHeaderContext';
-export {Drawer, DrawerProps, DrawerItemProps, DrawerItem} from './Drawer/Drawer';
 export {FooterItem, FooterItemProps} from './FooterItem/FooterItem';
 export {PageLayout, type PageLayoutProps} from './AsideHeader/components/PageLayout/PageLayout';
 export {PageLayoutAside} from './AsideHeader/components/PageLayout/PageLayoutAside';
 export {AsideFallback} from './AsideHeader/components/PageLayout/AsideFallback';
+export * from './Drawer';
 export * from './ActionBar';
 export * from './Title';
 export * from './HotkeysPanel';

From 6eaf889e74c3feeff4b6782b70c0959b161b7dfa Mon Sep 17 00:00:00 2001
From: StepanKirichenko <asv-dgr-2@yandex-team.ru>
Date: Fri, 19 Jul 2024 11:16:45 +0300
Subject: [PATCH 3/3] fix: readme

---
 src/components/Drawer/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Drawer/README.md b/src/components/Drawer/README.md
index 4e63e08..86d8afc 100644
--- a/src/components/Drawer/README.md
+++ b/src/components/Drawer/README.md
@@ -88,7 +88,7 @@ The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.
 | onVeilClick       | Optional callback function that is called when the veil (overlay) is clicked.                    | `(event: React.MouseEvent) => void` |         |
 | onEscape          | Optional callback function that is called when the escape key is pressed, if the drawer is open. |            `() => void`             |         |
 | hideVeil          | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
-| disablePortal     | Optional flag to hide the background darkening                                                   |              `boolean`              |         |
+| disablePortal     | Optional flag to not render drawer inside `Portal`                                               |              `boolean`              |         |
 
 ## CSS API