From 3e94231a93009cf16454094fb8a6bf501088b41b Mon Sep 17 00:00:00 2001 From: StepanKirichenko Date: Wed, 17 Jul 2024 12:30:31 +0300 Subject: [PATCH] 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 = ({ - visible, - content, - children, - direction = 'left', - className, -}) => { - const itemRef = React.useRef(null); - const cssDirection = direction === 'left' ? undefined : direction; + /** Determines whether the drawer item can be resized */ + resizable?: boolean; - return ( - -
- {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( + function DrawerItem(props, ref) { + const { + visible, + content, + children, + direction = 'left', + className, + resizable, + width, + minResizeWidth, + maxResizeWidth, + onResize, + } = props; + + const itemRef = React.useRef(null); + const handleRef = useForkRef(ref, itemRef); + const cssDirection = direction === 'left' ? undefined : direction; + + const {resizedWidth, resizerHandlers} = useResizableDrawerItem({ + direction, + width, + minResizeWidth, + maxResizeWidth, + onResize, + }); + + const resizerElement = resizable ? ( +
+
- - ); -}; + ) : null; + + return ( + +
+ {resizerElement} + {children ?? content} +
+
+ ); + }, +); type DrawerChild = React.ReactElement; @@ -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 = ({ @@ -94,6 +150,8 @@ export const Drawer: React.FC = ({ onVeilClick, onEscape, preventScrollBody = true, + hideVeil, + disablePortal, }) => { let someItemVisible = false; React.Children.forEach(children, (child) => { @@ -124,7 +182,7 @@ export const Drawer: React.FC = ({ const containerRef = React.useRef(null); const veilRef = React.useRef(null); - return ( + const drawer = ( = ({ classNames={b('veil-transition')} nodeRef={veilRef} > -
+
{React.Children.map(children, (child) => { if ( @@ -163,4 +225,12 @@ export const Drawer: React.FC = ({ }} ); + + if (disablePortal) { + return drawer; + } + + return {drawer}; }; + +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 ( +
+ + setVisible(false)} onVeilClick={() => setVisible(false)}> + +

Content of the drawer

+
+
+
+ ); +}; + +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 ( +
+
+ + + setDisablePortal(v)} + /> +
+
+

Container area for drawer with disablePortal

+ setVisible(false)} + > + +
+ +
+
+
+
+
+ ); +} 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 = () => ; export const Showcase = ShowcaseTemplate.bind({}); + +const ResizableItemTemplate: StoryFn = () => ; +export const ResizableItem = ResizableItemTemplate.bind({}); + +const DisablePortalTemplate: StoryFn = () => ; +export const DisablePortal = DisablePortalTemplate.bind({}); + +const HideVeilTemplate: StoryFn = () => ; +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(false); const [direction, setDirection] = React.useState('left'); + const [direction2, setDirection2] = React.useState('left'); const hideAll = React.useCallback(() => { setVisible1(false); @@ -35,6 +36,11 @@ export function DrawerShowcase() { left right +    Direction2:   + + left + right +
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 ( +
+
+ + +
+
+

Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, quos!

+ setVisible(false)} + > + +
+ +
+
+
+
+
+ ); +} 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 ( +
+
+ + + +
+
+

Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, quidem.

+ setVisible(false)}> + +
+ +
+
+
+
+
+ ); +} 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}; +}