diff --git a/.changeset/proud-kangaroos-switch.md b/.changeset/proud-kangaroos-switch.md new file mode 100644 index 000000000..354c79223 --- /dev/null +++ b/.changeset/proud-kangaroos-switch.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Smoother transition for ResizablePanel.' diff --git a/.changeset/soft-months-beam.md b/.changeset/soft-months-beam.md new file mode 100644 index 000000000..c578dc5f0 --- /dev/null +++ b/.changeset/soft-months-beam.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Round the output size style in ResizablePanel. diff --git a/src/components/layout/ResizablePanel.stories.tsx b/src/components/layout/ResizablePanel.stories.tsx index 2f7d30b37..ad3851b67 100644 --- a/src/components/layout/ResizablePanel.stories.tsx +++ b/src/components/layout/ResizablePanel.stories.tsx @@ -13,30 +13,30 @@ export default { const TemplateRight: StoryFn = (args) => ( - + ); const TemplateLeft: StoryFn = (args) => { return ( - + ); }; const TemplateBottom: StoryFn = (args) => ( - + - + ); const TemplateTop: StoryFn = (args) => { return ( - - + + ); @@ -60,7 +60,14 @@ const TemplateControllable: StoryFn = (args) => { const GridTemplate: StoryFn = (args) => ( - + + +); + +const TemplateDisabled: StoryFn = (args) => ( + + + ); @@ -87,9 +94,10 @@ ResizeTop.args = { export const Controllable = TemplateControllable.bind({}); Controllable.args = { direction: 'right', + maxSize: 500, }; -export const Disabled = TemplateRight.bind({}); +export const Disabled = TemplateDisabled.bind({}); Disabled.args = { isDisabled: true, }; diff --git a/src/components/layout/ResizablePanel.tsx b/src/components/layout/ResizablePanel.tsx index e48f3452a..239f54ab2 100644 --- a/src/components/layout/ResizablePanel.tsx +++ b/src/components/layout/ResizablePanel.tsx @@ -1,4 +1,4 @@ -import { ForwardedRef, forwardRef, useEffect, useState } from 'react'; +import { ForwardedRef, forwardRef, useEffect, useMemo, useState } from 'react'; import { useHover, useMove } from 'react-aria'; import { BasePropsWithoutChildren, Styles, tasty } from '../../tasty/index'; @@ -19,35 +19,45 @@ export interface CubeResizablePanelProps extends CubePanelProps { } const HandlerElement = tasty({ + qa: 'ResizeHandler', styles: { // The real size is slightly bigger than the visual one. width: { - '': 'auto', + '': 'initial', horizontal: '9px', + 'disabled & horizontal': '1bw', }, height: { '': '9px', - horizontal: 'auto', + horizontal: 'initial', + 'disabled & !horizontal': '1bw', }, top: { - '': 'initial', - horizontal: 0, - '[data-direction="top"]': -2, + '': 0, + '[data-direction="top"]': 'initial', }, bottom: { - '': 'initial', - horizontal: 0, - '[data-direction="bottom"]': -2, + '': 0, + '[data-direction="bottom"]': 'initial', }, right: { '': 0, - horizontal: 'initial', - '[data-direction="right"]': -2, + '[data-direction="right"]': 'initial', }, left: { '': 0, - horizontal: 'initial', - '[data-direction="left"]': -2, + '[data-direction="left"]': 'initial', + }, + // Transform requires a separate visual size property to respect size boundaries + transform: { + '[data-direction="top"]': + 'translate(0, (@size-compensation - @visual-size))', + '[data-direction="right"]': + 'translate((@visual-size - @size-compensation), 0)', + '[data-direction="bottom"]': + 'translate(0, (@visual-size - @size-compensation))', + '[data-direction="left"]': + 'translate((@size-compensation - @visual-size), 0)', }, position: 'absolute', zIndex: 1, @@ -62,20 +72,27 @@ const HandlerElement = tasty({ padding: 0, boxSizing: 'border-box', transition: 'theme', + '--size-compensation': { + '': '7px', + disabled: '1bw', + }, Track: { width: { '': 'initial', horizontal: '5px', + 'disabled & horizontal': '1px', }, height: { '': '5px', horizontal: 'initial', + 'disabled & !horizontal': '1px', }, position: 'absolute', inset: { '': '2px 0', horizontal: '0 2px', + disabled: '0 0', }, fill: { '': '#border-opaque', @@ -85,6 +102,10 @@ const HandlerElement = tasty({ }, Drag: { + hide: { + '': false, + disabled: true, + }, width: { '': '3x', horizontal: '3px', @@ -152,6 +173,11 @@ const PanelElement = tasty(Panel, { }, placeSelf: 'stretch', touchAction: 'none', + + '--indent-compensation': { + '': '5px', + disabled: '1bw', + }, }, }); @@ -166,7 +192,7 @@ function ResizablePanel( size: providedSize, onSizeChange, minSize = 200, - maxSize = isControllable ? undefined : 400, + maxSize = isControllable ? undefined : 'min(50%, 400px)', } = props; const [isDragging, setIsDragging] = useState(false); @@ -189,6 +215,20 @@ function ResizablePanel( let [size, setSize] = useState( providedSize != null ? clamp(providedSize) : 200, ); + let [visualSize, setVisualSize] = useState(null); + + useEffect(() => { + if (ref.current) { + const offsetProp = isHorizontal ? 'offsetWidth' : 'offsetHeight'; + const containerSize = ref.current[offsetProp]; + + if (Math.abs(containerSize - size) < 1 && !isDisabled) { + setVisualSize(size); + } else { + setVisualSize(containerSize); + } + } + }, [size, isDisabled]); let { moveProps } = useMove({ onMoveStart(e) { @@ -200,8 +240,8 @@ function ResizablePanel( const offsetProp = isHorizontal ? 'offsetWidth' : 'offsetHeight'; - if (ref.current && Math.abs(ref.current[offsetProp] - size) > 1) { - setSize(ref.current[offsetProp]); + if (ref.current && Math.abs(ref.current[offsetProp] - size) >= 1) { + setSize(Math.round(ref.current[offsetProp])); } }, onMove(e) { @@ -229,7 +269,7 @@ function ResizablePanel( useEffect(() => { if (providedSize == null || Math.abs(providedSize - size) > 0.5) { - onSizeChange?.(size); + onSizeChange?.(Math.round(size)); } }, [size]); @@ -239,30 +279,39 @@ function ResizablePanel( } }, [providedSize]); + const mods = useMemo(() => { + return { + drag: isDragging, + horizontal: isHorizontal, + disabled: isDisabled, + }; + }, [isDragging, isHorizontal, isDisabled]); + return ( } {...mergeProps(props, { style: { + // We set a current size further via width/min-width/max-width styles to respect size boundaries '--size': `${size}px`, + // We use a separate visual size to paint the handler for smoother experience + '--visual-size': `${visualSize}px`, '--min-size': typeof minSize === 'number' ? `${minSize}px` : minSize, '--max-size': typeof maxSize === 'number' ? `${maxSize}px` : maxSize, }, innerStyles: { - margin: `5px ${direction}`, + // The panel inner space compensation for the handler + margin: `@indent-compensation ${direction}`, }, })} />