From e5b860085e13e8adffa6c098664690ea2cf90b3c Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 8 Sep 2023 14:51:28 +0100 Subject: [PATCH 01/33] :construction: PoC for a component that handles resizing with pure CSS --- lib/Resizeable/.specs/Button.spec.tsx | 57 ++++++++++++++++++++++++ lib/Resizeable/index.tsx | 37 +++++++++++++++ lib/Resizeable/styles.scss | 23 ++++++++++ lib/index.ts | 1 + stories/components/Resizable.stories.tsx | 53 ++++++++++++++++++++++ styles/components/_ui.scss | 1 + 6 files changed, 172 insertions(+) create mode 100644 lib/Resizeable/.specs/Button.spec.tsx create mode 100644 lib/Resizeable/index.tsx create mode 100644 lib/Resizeable/styles.scss create mode 100644 stories/components/Resizable.stories.tsx diff --git a/lib/Resizeable/.specs/Button.spec.tsx b/lib/Resizeable/.specs/Button.spec.tsx new file mode 100644 index 000000000..78f49cc2c --- /dev/null +++ b/lib/Resizeable/.specs/Button.spec.tsx @@ -0,0 +1,57 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import React from 'react'; + +import { render, screen, fireEvent } from '@testing-library/react'; + +import Button from "../index"; + +describe("Button", () => { + it("Can have different classnames depending on the type", () => { + const { rerender } = render( + + ) + + expect(screen.getByRole('button').classList.contains('button--primary')).toBeTruthy(); + + rerender( + + ); + + expect(screen.getByRole('button').classList.contains('button--collapsed')).toBeTruthy(); + }); + + it("Can be a regular button or a submit button", () => { + const { rerender } = render( + + ); + + expect(screen.getByRole('button').getAttribute('type')).toEqual('button'); + + rerender(); + + expect(screen.getByRole('button').getAttribute('type')).toEqual('submit'); + }); + + it("Should have the disabled attribute when clicked if props.disableOnClick is true", () => { + const { rerender } = render( + + ); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.getByRole('button').hasAttribute('disabled')).toBeFalsy(); + + rerender( + + ); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.getByRole('button').hasAttribute('disabled')).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx new file mode 100644 index 000000000..2de9a0e9b --- /dev/null +++ b/lib/Resizeable/index.tsx @@ -0,0 +1,37 @@ +import type { PropsWithChildren } from "react"; +import React from "react"; + +export interface ResizeableProps { + defaultWidth?: string; + gutterSize?: string; + minWidth?: string; + maxWidth?: string; +} + +export function Resizeable({ + children, + defaultWidth, + gutterSize = "16px", + maxWidth, + minWidth, +}: PropsWithChildren) { + return ( +
+ {children} + +
+ ); +} diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss new file mode 100644 index 000000000..fcd37f42b --- /dev/null +++ b/lib/Resizeable/styles.scss @@ -0,0 +1,23 @@ +.resizable { + resize: horizontal; + display: inline-block; + overflow: auto; + position: relative; + height: 100%; + + & > *:first-child { + width: 100% !important; + } + + &__gutter { + display: inline-block; + height: 100%; + position: absolute; + right: 0; + top: 0; + + &:hover { + @apply bg-red-500 bg-opacity-20; + } + } +} diff --git a/lib/index.ts b/lib/index.ts index bb54d7c14..9c7766f5e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -23,6 +23,7 @@ export { NotificationInlineWarning } from "./Notification/inline/NotificationInl export { Dropdown } from "./Dropdown"; export { DropdownMenu } from "./DropdownMenu"; export { ProgressButton } from "./ProgressButton"; +export { Resizable } from "./Resizeable"; export { Progress } from "./Progress"; export { SearchInput } from "./SearchInput"; export { Modal } from "./Modal"; diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx new file mode 100644 index 000000000..9e98c1241 --- /dev/null +++ b/stories/components/Resizable.stories.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Resizeable as ResizableComponent } from "../../lib/Resizeable"; + +import StoryItem from "../styleguide/StoryItem"; + +export default { + title: "GUI/Resizable", + component: ResizableComponent, +}; + +export function Resizable() { + return ( + <> + +
+ +

Hello

+
+

+ How are you? +

+
+
+ + +
+ Not resizable +
+ +
Hello
+
+
+ Not resizable +
+
+ + ); +} diff --git a/styles/components/_ui.scss b/styles/components/_ui.scss index 9c0f26889..a30ceaf88 100644 --- a/styles/components/_ui.scss +++ b/styles/components/_ui.scss @@ -11,6 +11,7 @@ @import "../../lib/DropdownMenu/styles"; @import "../../lib/Progress/styles"; @import "../../lib/ProgressButton/styles"; +@import "../../lib/Resizeable/styles"; @import "../../lib/Modal/styles"; @import "../../lib/FigurePlaceholder/styles"; @import "../../lib/Conversation/styles"; From 12633d66950df1054c526e872edd5a1adaf029fb Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 8 Sep 2023 15:47:50 +0100 Subject: [PATCH 02/33] :pencil2: use correct export name --- lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.ts b/lib/index.ts index 9c7766f5e..0137381b3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -23,7 +23,7 @@ export { NotificationInlineWarning } from "./Notification/inline/NotificationInl export { Dropdown } from "./Dropdown"; export { DropdownMenu } from "./DropdownMenu"; export { ProgressButton } from "./ProgressButton"; -export { Resizable } from "./Resizeable"; +export { Resizeable } from "./Resizeable"; export { Progress } from "./Progress"; export { SearchInput } from "./SearchInput"; export { Modal } from "./Modal"; From e7262ae227bdeaf07c21191062bedb6093097484 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 8 Sep 2023 16:22:03 +0100 Subject: [PATCH 03/33] =?UTF-8?q?:construction:=20looks=20like=20we=20do?= =?UTF-8?q?=20need=20js=20after=20all=20=F0=9F=98=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/Resizeable/index.tsx | 44 ++++++++++++++++++++++-- lib/Resizeable/styles.scss | 30 +++++++++++----- stories/components/Resizable.stories.tsx | 2 +- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 2de9a0e9b..7b8a39d1c 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from "react"; -import React from "react"; +import React, { useCallback, useEffect, useRef } from "react"; export interface ResizeableProps { defaultWidth?: string; @@ -15,8 +15,47 @@ export function Resizeable({ maxWidth, minWidth, }: PropsWithChildren) { + const resizableRef = useRef(null); + const [state, setState] = React.useState({ + startX: 0, + startWidth: 0, + }); + + const doDrag = (evt: MouseEvent) => { + if (resizableRef.current === null) return; + + resizableRef.current.style.width = `${ + state.startWidth + evt.clientX - state.startX + }px`; + }; + + const stopDrag = () => { + document.removeEventListener("mousemove", doDrag, false); + document.removeEventListener("mouseup", stopDrag, false); + }; + + const initDrag = useCallback( + (evt: React.DragEvent) => { + if (resizableRef.current === null) return; + + const { width } = window.getComputedStyle(resizableRef.current); + const startWidth = parseInt(width, 10); + const startX = evt.clientX; + + setState({ startX, startWidth }); + + document.addEventListener("mousemove", doDrag, false); + document.addEventListener("mouseup", stopDrag, false); + }, + [setState] + ); + + // remember to remove global listeners on dismount + useEffect(() => () => stopDrag(), []); + return (
{children}
); diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index fcd37f42b..4310cb14c 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -1,23 +1,35 @@ .resizable { - resize: horizontal; display: inline-block; - overflow: auto; + overflow: visible; position: relative; height: 100%; & > *:first-child { width: 100% !important; } +} + +.resizable__gutter { + position: absolute; + user-select: none; - &__gutter { - display: inline-block; + &:hover, &:focus, &:active { + @apply bg-red-500 bg-opacity-20; + } + + &--h { height: 100%; - position: absolute; - right: 0; + width: 16px; top: 0; + right: -8px; + cursor: col-resize; + } - &:hover { - @apply bg-red-500 bg-opacity-20; - } + &--v { + height: 16px; + width: 100%; + bottom: -8px; + left: 0; + cursor: row-resize; } } diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 9e98c1241..9a4f4a819 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -41,7 +41,7 @@ export function Resizable() {
Not resizable
- +
Hello
From 523fdfceeae4d1f6dafe29c4e359a0c936dbaf3e Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 8 Sep 2023 16:35:54 +0100 Subject: [PATCH 04/33] :fire: remove superfluous stuff from sloppy copy/paste job --- lib/Resizeable/.specs/Button.spec.tsx | 57 ------------------------ stories/components/Resizable.stories.tsx | 4 +- 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 lib/Resizeable/.specs/Button.spec.tsx diff --git a/lib/Resizeable/.specs/Button.spec.tsx b/lib/Resizeable/.specs/Button.spec.tsx deleted file mode 100644 index 78f49cc2c..000000000 --- a/lib/Resizeable/.specs/Button.spec.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { - describe, - expect, - it -} from 'vitest'; - -import React from 'react'; - -import { render, screen, fireEvent } from '@testing-library/react'; - -import Button from "../index"; - -describe("Button", () => { - it("Can have different classnames depending on the type", () => { - const { rerender } = render( - - ) - - expect(screen.getByRole('button').classList.contains('button--primary')).toBeTruthy(); - - rerender( - - ); - - expect(screen.getByRole('button').classList.contains('button--collapsed')).toBeTruthy(); - }); - - it("Can be a regular button or a submit button", () => { - const { rerender } = render( - - ); - - expect(screen.getByRole('button').getAttribute('type')).toEqual('button'); - - rerender(); - - expect(screen.getByRole('button').getAttribute('type')).toEqual('submit'); - }); - - it("Should have the disabled attribute when clicked if props.disableOnClick is true", () => { - const { rerender } = render( - - ); - - fireEvent.click(screen.getByRole('button')); - - expect(screen.getByRole('button').hasAttribute('disabled')).toBeFalsy(); - - rerender( - - ); - - fireEvent.click(screen.getByRole('button')); - - expect(screen.getByRole('button').hasAttribute('disabled')).toBeTruthy(); - }); -}); \ No newline at end of file diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 9a4f4a819..ccdc4bbfb 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -13,7 +13,7 @@ export function Resizable() { <>
Not resizable From 1904a4c969804e94190e1945234d8898bd671b03 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 8 Sep 2023 16:36:34 +0100 Subject: [PATCH 05/33] :bug: account for the width of the gutter --- lib/Resizeable/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 7b8a39d1c..3fbf3f374 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -16,16 +16,24 @@ export function Resizeable({ minWidth, }: PropsWithChildren) { const resizableRef = useRef(null); + const gutterRef = useRef(null); const [state, setState] = React.useState({ startX: 0, startWidth: 0, }); const doDrag = (evt: MouseEvent) => { - if (resizableRef.current === null) return; + if (resizableRef.current === null || gutterRef.current === null) return; + + // Using the computed style to get the px value + // Just in case the gutterSize prop is in anything other than px + const gutterOffset = parseInt( + window.getComputedStyle(gutterRef.current).width, + 10 + ); resizableRef.current.style.width = `${ - state.startWidth + evt.clientX - state.startX + state.startWidth + evt.clientX - state.startX - gutterOffset }px`; }; @@ -65,6 +73,7 @@ export function Resizeable({ > {children} Date: Wed, 13 Sep 2023 15:30:23 +0100 Subject: [PATCH 06/33] :pencil2: resizable type --- lib/Resizeable/index.tsx | 2 +- lib/Resizeable/styles.scss | 8 ++--- stories/components/Resizable.stories.tsx | 38 +++++++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 3fbf3f374..6b5aa5ac9 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -64,7 +64,7 @@ export function Resizeable({ return (
*:first-child { width: 100% !important; diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index ccdc4bbfb..2b797afd4 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -4,48 +4,64 @@ import { Resizeable as ResizableComponent } from "../../lib/Resizeable"; import StoryItem from "../styleguide/StoryItem"; export default { - title: "GUI/Resizable", + title: "GUI/Resizeable", component: ResizableComponent, }; -export function Resizable() { +export function Resizeable() { return ( <>
-

Hello

+
+ Hello +
-

How are you? -

+
- Not resizable + Not resizeable
Hello
- Not resizable + Not resizeable
From 79247db98b3b6563dba833ee45dee7fcfd91da56 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 13:28:18 +0100 Subject: [PATCH 07/33] :recycle: make width stateful --- lib/Resizeable/index.tsx | 49 +++++++++++++++--------- lib/Resizeable/styles.scss | 34 +++++++--------- stories/components/Resizable.stories.tsx | 6 ++- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 6b5aa5ac9..03f8f0a7e 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from "react"; -import React, { useCallback, useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; export interface ResizeableProps { defaultWidth?: string; @@ -21,6 +21,7 @@ export function Resizeable({ startX: 0, startWidth: 0, }); + const [width, setWidth] = useState(defaultWidth); const doDrag = (evt: MouseEvent) => { if (resizableRef.current === null || gutterRef.current === null) return; @@ -32,9 +33,12 @@ export function Resizeable({ 10 ); - resizableRef.current.style.width = `${ - state.startWidth + evt.clientX - state.startX - gutterOffset - }px`; + const newWidth = + state.startWidth + evt.clientX - state.startX - gutterOffset; + + console.log({ newWidth, maxWidth, minWidth }); + + setWidth(`${newWidth}px`); }; const stopDrag = () => { @@ -46,8 +50,10 @@ export function Resizeable({ (evt: React.DragEvent) => { if (resizableRef.current === null) return; - const { width } = window.getComputedStyle(resizableRef.current); - const startWidth = parseInt(width, 10); + const startWidth = parseInt( + window.getComputedStyle(resizableRef.current).width, + 10 + ); const startX = evt.clientX; setState({ startX, startWidth }); @@ -63,24 +69,31 @@ export function Resizeable({ return (
- {children} - + > + {children} + +
); } diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 35a806185..fdcb08e5b 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -1,10 +1,11 @@ .resizeable { - //display: inline-block; - //overflow: visible; position: relative; - //height: 100%; - & > *:first-child { + &__wrapper { + position: relative; + } + + &__wrapper > *:first-child { width: 100% !important; } } @@ -12,24 +13,17 @@ .resizable__gutter { position: absolute; user-select: none; + background: orange; + height: 100%; + width: 16px; + top: 0; + right: -8px; + cursor: col-resize; - &:hover, &:focus, &:active { + &:hover, + &:focus, + &:active { @apply bg-red-500 bg-opacity-20; } - &--h { - height: 100%; - width: 16px; - top: 0; - right: -8px; - cursor: col-resize; - } - - &--v { - height: 16px; - width: 100%; - bottom: -8px; - left: 0; - cursor: row-resize; - } } diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 2b797afd4..81329972c 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -23,7 +23,11 @@ export function Resizeable() { border: "1px solid red", }} > - +
Date: Thu, 14 Sep 2023 14:49:26 +0100 Subject: [PATCH 08/33] :recycle: general cleanup of component post-testing --- lib/Resizeable/index.tsx | 111 +++++++++++++++-------- lib/Resizeable/styles.scss | 5 +- stories/components/Resizable.stories.tsx | 7 +- 3 files changed, 77 insertions(+), 46 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 03f8f0a7e..f4c7c3b88 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,44 +1,83 @@ import type { PropsWithChildren } from "react"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect } from "react"; + +/** + * Try to normalise all units to a standard pixel value to keep maths simple elsewhere + */ +function normaliseUnitsToPixelValue( + value: string | number, + container?: HTMLElement +) { + if (typeof value === "number") { + return value; + } + + if (value.endsWith("px")) { + return parseInt(value, 10); + } + + if (value.endsWith("%")) { + const containerWidthAsPercent = + (container || document.body).offsetWidth / 100; + return containerWidthAsPercent * parseInt(value, 10); + } + + console.warn( + `Could not interpret a normalised value for "${value}. Parsing directly to integer.` + ); + return parseInt(value, 10); +} + +/** + * Ensure the default width is between the specified max and min widths + */ +const calculateStartingWidth = ( + defaultWidth: number, + min: number, + max: number +) => { + let value = normaliseUnitsToPixelValue(defaultWidth); + value = Math.min(value, max); + value = Math.max(value, min); + return value; +}; export interface ResizeableProps { defaultWidth?: string; - gutterSize?: string; minWidth?: string; maxWidth?: string; } export function Resizeable({ children, - defaultWidth, - gutterSize = "16px", + defaultWidth = "50%", maxWidth, minWidth, }: PropsWithChildren) { - const resizableRef = useRef(null); - const gutterRef = useRef(null); + const min = normaliseUnitsToPixelValue(minWidth ?? 0); + const max = normaliseUnitsToPixelValue(maxWidth ?? "100%"); + const gutterSize = 16; + const [state, setState] = React.useState({ startX: 0, startWidth: 0, + width: calculateStartingWidth( + normaliseUnitsToPixelValue(defaultWidth), + min, + max + ), }); - const [width, setWidth] = useState(defaultWidth); const doDrag = (evt: MouseEvent) => { - if (resizableRef.current === null || gutterRef.current === null) return; - - // Using the computed style to get the px value - // Just in case the gutterSize prop is in anything other than px - const gutterOffset = parseInt( - window.getComputedStyle(gutterRef.current).width, - 10 - ); - - const newWidth = - state.startWidth + evt.clientX - state.startX - gutterOffset; + const newWidth = state.startWidth + evt.clientX - state.startX; + const hasExceededMaxWidth = newWidth > max; + const hasExceededMinWidth = newWidth < min; - console.log({ newWidth, maxWidth, minWidth }); + if (hasExceededMaxWidth || hasExceededMinWidth) { + return; + } - setWidth(`${newWidth}px`); + setState((prev) => ({ ...prev, width: newWidth })); }; const stopDrag = () => { @@ -48,48 +87,40 @@ export function Resizeable({ const initDrag = useCallback( (evt: React.DragEvent) => { - if (resizableRef.current === null) return; + const { clientX } = evt; - const startWidth = parseInt( - window.getComputedStyle(resizableRef.current).width, - 10 - ); - const startX = evt.clientX; + const newState = { + ...state, + startX: clientX, + startWidth: state.width, + }; - setState({ startX, startWidth }); + setState(newState); document.addEventListener("mousemove", doDrag, false); document.addEventListener("mouseup", stopDrag, false); }, - [setState] + [state, setState] ); // remember to remove global listeners on dismount useEffect(() => () => stopDrag(), []); return ( -
+
{children} diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index fdcb08e5b..766de3201 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -1,8 +1,11 @@ .resizeable { position: relative; + display: flex; &__wrapper { position: relative; + display: flex; + align-items: stretch; } &__wrapper > *:first-child { @@ -13,7 +16,6 @@ .resizable__gutter { position: absolute; user-select: none; - background: orange; height: 100%; width: 16px; top: 0; @@ -25,5 +27,4 @@ &:active { @apply bg-red-500 bg-opacity-20; } - } diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 81329972c..be706487b 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -24,15 +24,14 @@ export function Resizeable() { }} >
From a589064fdb95a97f46f1868f160d01b114cc8268 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 14:54:55 +0100 Subject: [PATCH 09/33] :recycle: general cleanup of component post-testing --- lib/Resizeable/index.tsx | 4 ---- lib/Resizeable/styles.scss | 6 ++++-- stories/components/Resizable.stories.tsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index f4c7c3b88..d704d62a1 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -118,10 +118,6 @@ export function Resizeable({
diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 766de3201..29acbaf9b 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -1,3 +1,5 @@ +$gutterSize: 16px; + .resizeable { position: relative; display: flex; @@ -17,9 +19,9 @@ position: absolute; user-select: none; height: 100%; - width: 16px; + width: $gutterSize; top: 0; - right: -8px; + right: -(calc($gutterSize / 2)); cursor: col-resize; &:hover, diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index be706487b..103d6d94a 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -32,7 +32,7 @@ export function Resizeable() { style={{ width: "400px", flexShrink: "0", - border: "1px solid blue", + border: "1px solid orange", }} > Hello From 35faca1a30e9c6cac91a797b58e7b7dc4e0e2293 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 15:12:16 +0100 Subject: [PATCH 10/33] :lipstick: resize bar to have blue line in middle --- lib/Resizeable/styles.scss | 11 +++++++++++ stories/components/Resizable.stories.tsx | 2 -- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 29acbaf9b..e28cc933f 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -24,6 +24,17 @@ $gutterSize: 16px; right: -(calc($gutterSize / 2)); cursor: col-resize; + &::before { + content: ''; + width: 2px; + margin: auto; + background-color: #006AFF; + display: block; + height: 100%; + } + + + &:hover, &:focus, &:active { diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 103d6d94a..b7a178c5d 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -32,7 +32,6 @@ export function Resizeable() { style={{ width: "400px", flexShrink: "0", - border: "1px solid orange", }} > Hello @@ -45,7 +44,6 @@ export function Resizeable() { minWidth: "0", flexDirection: "column", display: " flex !important", - border: "1px solid green", }} > How are you? From 7b7de8b3c02d1e2789e40dd53685db509fc41c41 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 15:23:24 +0100 Subject: [PATCH 11/33] :lipstick: add gutter --- lib/Resizeable/index.tsx | 8 +++----- lib/Resizeable/styles.scss | 28 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index d704d62a1..d2ee42c12 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -115,11 +115,9 @@ export function Resizeable({ }} > {children} - + + +
); diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index e28cc933f..8d4f90ba4 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -1,4 +1,5 @@ $gutterSize: 16px; +$gutterColour: #006aff; .resizeable { position: relative; @@ -22,22 +23,39 @@ $gutterSize: 16px; width: $gutterSize; top: 0; right: -(calc($gutterSize / 2)); - cursor: col-resize; + cursor: ew-resize; &::before { - content: ''; + content: ""; width: 2px; margin: auto; - background-color: #006AFF; + background-color: $gutterColour; display: block; height: 100%; } - - &:hover, &:focus, &:active { @apply bg-red-500 bg-opacity-20; } } + +.resizable__gutter-handle { + background-color: $gutterColour; + border-radius: 4px; + height: 30px; + margin: auto; + width: 8px; + display: flex; + position: absolute; + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + opacity: 0; + transition: opacity 0.1s; + + .resizable__gutter:hover & { + opacity: 1; + } +} From 34a8230be66f87b68a200d04fec31983aea0692e Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 16:03:54 +0100 Subject: [PATCH 12/33] :sparkles: gutter should follow cursor --- lib/Resizeable/index.tsx | 24 ++++++++++++++++++++---- lib/Resizeable/styles.scss | 3 +-- stories/components/Resizable.stories.tsx | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index d2ee42c12..7bef3c625 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from "react"; -import React, { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; /** * Try to normalise all units to a standard pixel value to keep maths simple elsewhere @@ -54,6 +54,7 @@ export function Resizeable({ maxWidth, minWidth, }: PropsWithChildren) { + const handleRef = useRef(null); const min = normaliseUnitsToPixelValue(minWidth ?? 0); const max = normaliseUnitsToPixelValue(maxWidth ?? "100%"); const gutterSize = 16; @@ -100,9 +101,19 @@ export function Resizeable({ document.addEventListener("mousemove", doDrag, false); document.addEventListener("mouseup", stopDrag, false); }, - [state, setState] + [setState] ); + const setGutterHandlePosition = (evt: React.MouseEvent) => { + const handle = handleRef.current; + if (handle === null) return; + + const rect = evt.currentTarget.getBoundingClientRect(); + const y = evt.clientY - rect.top; + + handle.style.top = `${y - handle.offsetHeight / 2}px`; + }; + // remember to remove global listeners on dismount useEffect(() => () => stopDrag(), []); @@ -115,8 +126,13 @@ export function Resizeable({ }} > {children} - - + +
diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 8d4f90ba4..b2dec45b4 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -49,8 +49,7 @@ $gutterColour: #006aff; width: 8px; display: flex; position: absolute; - transform: translate(-50%, -50%); - top: 50%; + transform: translateX(-50%); left: 50%; opacity: 0; transition: opacity 0.1s; diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index b7a178c5d..0144f302d 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -21,6 +21,7 @@ export function Resizeable() { flex: "1", minHeight: "0", border: "1px solid red", + height: 600, }} > Date: Thu, 14 Sep 2023 16:12:50 +0100 Subject: [PATCH 13/33] :lipstick: remove debugging styles --- lib/Resizeable/styles.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index b2dec45b4..7984a93db 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -33,12 +33,6 @@ $gutterColour: #006aff; display: block; height: 100%; } - - &:hover, - &:focus, - &:active { - @apply bg-red-500 bg-opacity-20; - } } .resizable__gutter-handle { From e6e899da81e017d1c5792551bbc9c2af8168fddb Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 16:27:11 +0100 Subject: [PATCH 14/33] :recycle: move helpers --- lib/Resizeable/index.tsx | 55 +++++---------------------------------- lib/helpers.ts | 56 +++++++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 7bef3c625..d22ab974c 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,46 +1,6 @@ import type { PropsWithChildren } from "react"; import React, { useCallback, useEffect, useRef } from "react"; - -/** - * Try to normalise all units to a standard pixel value to keep maths simple elsewhere - */ -function normaliseUnitsToPixelValue( - value: string | number, - container?: HTMLElement -) { - if (typeof value === "number") { - return value; - } - - if (value.endsWith("px")) { - return parseInt(value, 10); - } - - if (value.endsWith("%")) { - const containerWidthAsPercent = - (container || document.body).offsetWidth / 100; - return containerWidthAsPercent * parseInt(value, 10); - } - - console.warn( - `Could not interpret a normalised value for "${value}. Parsing directly to integer.` - ); - return parseInt(value, 10); -} - -/** - * Ensure the default width is between the specified max and min widths - */ -const calculateStartingWidth = ( - defaultWidth: number, - min: number, - max: number -) => { - let value = normaliseUnitsToPixelValue(defaultWidth); - value = Math.min(value, max); - value = Math.max(value, min); - return value; -}; +import { keepValueWithinRange, normaliseUnitsToPixelValue } from "../helpers"; export interface ResizeableProps { defaultWidth?: string; @@ -62,7 +22,7 @@ export function Resizeable({ const [state, setState] = React.useState({ startX: 0, startWidth: 0, - width: calculateStartingWidth( + width: keepValueWithinRange( normaliseUnitsToPixelValue(defaultWidth), min, max @@ -71,14 +31,11 @@ export function Resizeable({ const doDrag = (evt: MouseEvent) => { const newWidth = state.startWidth + evt.clientX - state.startX; - const hasExceededMaxWidth = newWidth > max; - const hasExceededMinWidth = newWidth < min; - - if (hasExceededMaxWidth || hasExceededMinWidth) { - return; - } - setState((prev) => ({ ...prev, width: newWidth })); + setState((prev) => ({ + ...prev, + width: keepValueWithinRange(newWidth, min, max), + })); }; const stopDrag = () => { diff --git a/lib/helpers.ts b/lib/helpers.ts index e48076403..645cb619a 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,16 +1,54 @@ -export const pluralisePerson = (count: any) => count === 1 ? `${count} person` : `${count} people`; +export const pluralisePerson = (count: any) => + count === 1 ? `${count} person` : `${count} people`; -export const pluraliseHas = (count: any) => count === 1 ? 'has' : 'have'; +export const pluraliseHas = (count: any) => (count === 1 ? "has" : "have"); export const filterUsers = (users: any, term: any, searchByEmail = false) => { const safeTerm = term.toLowerCase(); return users.filter( - (user: any) => user.name - .toLowerCase() - .split(' ') - .filter((subStr: any) => subStr.lastIndexOf(safeTerm, 0) === 0).length > 0 || - user.name.toLowerCase().lastIndexOf(safeTerm, 0) === 0 || - user.display.toLowerCase().lastIndexOf(safeTerm, 0) === 0 || - (searchByEmail && user.email.toLowerCase().lastIndexOf(safeTerm, 0) === 0) + (user: any) => + user.name + .toLowerCase() + .split(" ") + .filter((subStr: any) => subStr.lastIndexOf(safeTerm, 0) === 0).length > + 0 || + user.name.toLowerCase().lastIndexOf(safeTerm, 0) === 0 || + user.display.toLowerCase().lastIndexOf(safeTerm, 0) === 0 || + (searchByEmail && user.email.toLowerCase().lastIndexOf(safeTerm, 0) === 0) ); }; + +export const normaliseUnitsToPixelValue = ( + value: string | number, + container: HTMLElement = document.body +) => { + if (typeof value === "number") { + // assume value is already in pixels + return value; + } + + if (value.endsWith("px")) { + return parseInt(value, 10); + } + + if (value.endsWith("%")) { + const containerWidthAsPercent = container.offsetWidth / 100; + return containerWidthAsPercent * parseInt(value, 10); + } + + console.warn( + `Could not interpret a normalised value for "${value}. Parsing directly to integer.` + ); + return parseInt(value, 10); +}; + +export const keepValueWithinRange = ( + start: number, + min?: number, + max?: number +) => { + let value = start; + if (typeof max === "number") value = Math.min(value, max); + if (typeof min === "number") value = Math.max(value, min); + return value; +}; From 42d08cfa0cccb3043bcbd2395733afbe69cf21f3 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Thu, 14 Sep 2023 16:38:41 +0100 Subject: [PATCH 15/33] :bug: prevent gutter handle from going out-out-of-bounds --- lib/Resizeable/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index d22ab974c..db55ed39b 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -66,9 +66,16 @@ export function Resizeable({ if (handle === null) return; const rect = evt.currentTarget.getBoundingClientRect(); - const y = evt.clientY - rect.top; + const gutter = handle.parentElement as HTMLSpanElement; + const handleOffset = handle.offsetHeight / 2; - handle.style.top = `${y - handle.offsetHeight / 2}px`; + const y = keepValueWithinRange( + evt.clientY - rect.top, + handleOffset, + gutter.offsetHeight - handleOffset + ); + + handle.style.top = `${y - handleOffset}px`; }; // remember to remove global listeners on dismount From 761e3ee69abe93b818bd15c7aa0b31c41b6279c4 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 09:36:38 +0100 Subject: [PATCH 16/33] :label: correct prop types --- lib/Resizeable/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index db55ed39b..4c92619cd 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -3,9 +3,9 @@ import React, { useCallback, useEffect, useRef } from "react"; import { keepValueWithinRange, normaliseUnitsToPixelValue } from "../helpers"; export interface ResizeableProps { - defaultWidth?: string; - minWidth?: string; - maxWidth?: string; + defaultWidth?: number | string; + minWidth?: number | string; + maxWidth?: number | string; } export function Resizeable({ From 2d0d3456169fbd7fcd529ebf6fcb770a850328bb Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 10:10:45 +0100 Subject: [PATCH 17/33] :recycle: use ref over state for setting dom element width --- lib/Resizeable/index.tsx | 42 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 4c92619cd..50b804094 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -14,6 +14,7 @@ export function Resizeable({ maxWidth, minWidth, }: PropsWithChildren) { + const resizeWrapperRef = useRef(null); const handleRef = useRef(null); const min = normaliseUnitsToPixelValue(minWidth ?? 0); const max = normaliseUnitsToPixelValue(maxWidth ?? "100%"); @@ -22,20 +23,22 @@ export function Resizeable({ const [state, setState] = React.useState({ startX: 0, startWidth: 0, - width: keepValueWithinRange( - normaliseUnitsToPixelValue(defaultWidth), - min, - max - ), }); + const getWidth = () => + normaliseUnitsToPixelValue(resizeWrapperRef.current?.style.width || 0); + + const setWidth = (value: number) => { + if (resizeWrapperRef.current === null) return; + + resizeWrapperRef.current.style.width = `${ + keepValueWithinRange(value, min, max) - gutterSize + }px`; + }; + const doDrag = (evt: MouseEvent) => { const newWidth = state.startWidth + evt.clientX - state.startX; - - setState((prev) => ({ - ...prev, - width: keepValueWithinRange(newWidth, min, max), - })); + setWidth(newWidth); }; const stopDrag = () => { @@ -50,7 +53,7 @@ export function Resizeable({ const newState = { ...state, startX: clientX, - startWidth: state.width, + startWidth: getWidth(), }; setState(newState); @@ -78,17 +81,18 @@ export function Resizeable({ handle.style.top = `${y - handleOffset}px`; }; - // remember to remove global listeners on dismount - useEffect(() => () => stopDrag(), []); + useEffect(() => { + setWidth( + keepValueWithinRange(normaliseUnitsToPixelValue(defaultWidth), min, max) + ); + + // remember to remove global listeners on dismount + return () => stopDrag(); + }, []); return (
-
+
{children} Date: Fri, 15 Sep 2023 10:37:03 +0100 Subject: [PATCH 18/33] :sparkles: gutter offset prop - helps when "position" or "flex" are applied to children prop --- lib/Resizeable/index.tsx | 12 +++++++++++- lib/Resizeable/styles.scss | 1 + stories/components/Resizable.stories.tsx | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 50b804094..f74effbfb 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -6,19 +6,29 @@ export interface ResizeableProps { defaultWidth?: number | string; minWidth?: number | string; maxWidth?: number | string; + useGutterOffset?: boolean; } +/** + * A wrapper for making a given child element resizable + * @param children The element to be resized + * @param defaultWidth The width the child should initially render to. If min and max widths are provided, then the provided width will be massaged to meet those constraints + * @param maxWidth The maximum width the element can be resized to + * @param minWidth The minimum width the element can be resized to + * @param useGutterOffset Toggle whether we should take the width of the gutter into account for calculating the width of the resized item. This can be handy depending on the CSS being applied to or around the child element + */ export function Resizeable({ children, defaultWidth = "50%", maxWidth, minWidth, + useGutterOffset = false, }: PropsWithChildren) { const resizeWrapperRef = useRef(null); const handleRef = useRef(null); const min = normaliseUnitsToPixelValue(minWidth ?? 0); const max = normaliseUnitsToPixelValue(maxWidth ?? "100%"); - const gutterSize = 16; + const gutterSize = useGutterOffset ? 16 : 0; const [state, setState] = React.useState({ startX: 0, diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 7984a93db..c6f308244 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -24,6 +24,7 @@ $gutterColour: #006aff; top: 0; right: -(calc($gutterSize / 2)); cursor: ew-resize; + z-index: 99; &::before { content: ""; diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 0144f302d..b2d815c2f 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -28,6 +28,7 @@ export function Resizeable() { defaultWidth="240px" maxWidth="33.33%" minWidth="240px" + useGutterOffset >
Not resizeable
- +
Hello
From 832a7c5bb43956bb5af863601d9f9c5738175f33 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 10:51:17 +0100 Subject: [PATCH 19/33] :coffin: z-index has no effect here --- lib/Resizeable/styles.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index c6f308244..7984a93db 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -24,7 +24,6 @@ $gutterColour: #006aff; top: 0; right: -(calc($gutterSize / 2)); cursor: ew-resize; - z-index: 99; &::before { content: ""; From 65677b2a13c57d7636a7e6a41f6a691a3740705f Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 10:56:14 +0100 Subject: [PATCH 20/33] :recycle: improve readability of normaliseUnitsToPixelValue --- lib/helpers.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/helpers.ts b/lib/helpers.ts index 645cb619a..ac554bf6f 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -22,24 +22,22 @@ export const normaliseUnitsToPixelValue = ( value: string | number, container: HTMLElement = document.body ) => { - if (typeof value === "number") { - // assume value is already in pixels - return value; - } + if (typeof value === "number") return value; + + const integer = parseInt(value, 10); if (value.endsWith("px")) { - return parseInt(value, 10); + return integer; } if (value.endsWith("%")) { - const containerWidthAsPercent = container.offsetWidth / 100; - return containerWidthAsPercent * parseInt(value, 10); + return (container.offsetWidth / 100) * integer; } console.warn( - `Could not interpret a normalised value for "${value}. Parsing directly to integer.` + `Could not interpret a normalised value for: ${value}.\nParsing directly to integer: ${integer}.` ); - return parseInt(value, 10); + return integer; }; export const keepValueWithinRange = ( From 72b83ddbcfa4b8bc294896266948acc3d34d94b0 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 10:58:20 +0100 Subject: [PATCH 21/33] :recycle: better name for `y` --- lib/Resizeable/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index f74effbfb..910aeb8ce 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -82,13 +82,13 @@ export function Resizeable({ const gutter = handle.parentElement as HTMLSpanElement; const handleOffset = handle.offsetHeight / 2; - const y = keepValueWithinRange( + const yPosition = keepValueWithinRange( evt.clientY - rect.top, handleOffset, gutter.offsetHeight - handleOffset ); - handle.style.top = `${y - handleOffset}px`; + handle.style.top = `${yPosition - handleOffset}px`; }; useEffect(() => { From c4b1adb1eaa92554bcd9cb48e705ce6a1de31b4b Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 13:04:59 +0100 Subject: [PATCH 22/33] :recycle: better helper name --- lib/Resizeable/index.tsx | 13 +++++-------- lib/helpers.ts | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 910aeb8ce..79a583b2d 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -1,6 +1,6 @@ import type { PropsWithChildren } from "react"; import React, { useCallback, useEffect, useRef } from "react"; -import { keepValueWithinRange, normaliseUnitsToPixelValue } from "../helpers"; +import { keepValueWithinRange, toPixels } from "../helpers"; export interface ResizeableProps { defaultWidth?: number | string; @@ -26,8 +26,8 @@ export function Resizeable({ }: PropsWithChildren) { const resizeWrapperRef = useRef(null); const handleRef = useRef(null); - const min = normaliseUnitsToPixelValue(minWidth ?? 0); - const max = normaliseUnitsToPixelValue(maxWidth ?? "100%"); + const min = toPixels(minWidth ?? 0); + const max = toPixels(maxWidth ?? "100%"); const gutterSize = useGutterOffset ? 16 : 0; const [state, setState] = React.useState({ @@ -35,8 +35,7 @@ export function Resizeable({ startWidth: 0, }); - const getWidth = () => - normaliseUnitsToPixelValue(resizeWrapperRef.current?.style.width || 0); + const getWidth = () => toPixels(resizeWrapperRef.current?.style.width || 0); const setWidth = (value: number) => { if (resizeWrapperRef.current === null) return; @@ -92,9 +91,7 @@ export function Resizeable({ }; useEffect(() => { - setWidth( - keepValueWithinRange(normaliseUnitsToPixelValue(defaultWidth), min, max) - ); + setWidth(keepValueWithinRange(toPixels(defaultWidth), min, max)); // remember to remove global listeners on dismount return () => stopDrag(); diff --git a/lib/helpers.ts b/lib/helpers.ts index ac554bf6f..ab90d37a0 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -18,7 +18,7 @@ export const filterUsers = (users: any, term: any, searchByEmail = false) => { ); }; -export const normaliseUnitsToPixelValue = ( +export const toPixels = ( value: string | number, container: HTMLElement = document.body ) => { From 3dce17b82402d2ff8d578c14f9683217bf6b3894 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:23:19 +0100 Subject: [PATCH 23/33] :white_check_mark: helper util tests --- lib/.specs/helpers.spec.ts | 42 ++++++++++++++++++++++++++++++++++++++ lib/helpers.ts | 7 ++----- 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 lib/.specs/helpers.spec.ts diff --git a/lib/.specs/helpers.spec.ts b/lib/.specs/helpers.spec.ts new file mode 100644 index 000000000..151e13f06 --- /dev/null +++ b/lib/.specs/helpers.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { keepValueWithinRange, toPixels } from "../helpers"; + +describe("helpers: toPixels", () => { + it("should not convert arguments of type number", () => { + expect(toPixels(100)).toBe(100); + }); + + it("should convert pixels to number", () => { + expect(toPixels(`100px`)).toBe(100); + }); + + it("should convert percentage to number", () => { + expect(toPixels(`100%`)).toBe(100); + }); + + it("should convert to a percentage value of the percentageOf arg", () => { + expect(toPixels("50%", 200)).toBe(100); + }); + + it("should return number and warning for unknown units", () => { + expect(toPixels("123rem")).toBe(123); + }); +}); + +describe("helpers: keepValueWithinRange", () => { + it("should return the value if there are no other arguments", () => { + expect(keepValueWithinRange(50)).toBe(50); + }); + + it("should return the value if it is within the range", () => { + expect(keepValueWithinRange(50, 33, 77)).toBe(50); + }); + + it("should return the max if the value is above the max", () => { + expect(keepValueWithinRange(11, 1, 10)).toBe(10); + }); + + it("should return the min if the value is below the min", () => { + expect(keepValueWithinRange(0, 1, 10)).toBe(1); + }); +}); diff --git a/lib/helpers.ts b/lib/helpers.ts index ab90d37a0..bb8f48908 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -18,10 +18,7 @@ export const filterUsers = (users: any, term: any, searchByEmail = false) => { ); }; -export const toPixels = ( - value: string | number, - container: HTMLElement = document.body -) => { +export const toPixels = (value: string | number, percentageOf = 100) => { if (typeof value === "number") return value; const integer = parseInt(value, 10); @@ -31,7 +28,7 @@ export const toPixels = ( } if (value.endsWith("%")) { - return (container.offsetWidth / 100) * integer; + return (percentageOf / 100) * integer; } console.warn( From 705cebbf5cf5e332a18296e16158890225fcc25a Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:25:28 +0100 Subject: [PATCH 24/33] :wrench: include ts in vitest --- vite.config.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 7361e2049..d1d276852 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,36 +1,36 @@ /// -import { defineConfig } from 'vite'; -import magicalSvg from 'vite-plugin-magical-svg' -import tsconfigPaths from 'vite-tsconfig-paths'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vite"; +import magicalSvg from "vite-plugin-magical-svg"; +import tsconfigPaths from "vite-tsconfig-paths"; +import react from "@vitejs/plugin-react"; import path from "path"; export default defineConfig({ test: { globals: true, - include: ['**/.specs/**/*.+(spec).tsx'], + include: ["**/.specs/**/*.+(spec).ts(x)?"], coverage: { - reporter: ['text', 'json', 'html'], + reporter: ["text", "json", "html"], }, - environment: 'jsdom', + environment: "jsdom", }, plugins: [ magicalSvg({ - target: 'react', + target: "react", }), react(), tsconfigPaths(), ], resolve: { alias: { - "lib": path.resolve(__dirname, "./lib"), - "tests": path.resolve(__dirname, "./tests"), - "stories": path.resolve(__dirname, "./stories"), - "src": path.resolve(__dirname, "./lib/src"), - "components": path.resolve(__dirname, "./lib/src/components"), - "modules": path.resolve(__dirname, "./lib/src/modules"), - "helpers": path.resolve(__dirname, "./lib/src/helpers"), - "assets": path.resolve(__dirname, "./assets"), + lib: path.resolve(__dirname, "./lib"), + tests: path.resolve(__dirname, "./tests"), + stories: path.resolve(__dirname, "./stories"), + src: path.resolve(__dirname, "./lib/src"), + components: path.resolve(__dirname, "./lib/src/components"), + modules: path.resolve(__dirname, "./lib/src/modules"), + helpers: path.resolve(__dirname, "./lib/src/helpers"), + assets: path.resolve(__dirname, "./assets"), }, }, }); From 6b5990c9d296f134d1a712bb12a85e9b334e4855 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:25:47 +0100 Subject: [PATCH 25/33] :lipstick: prevent child elements leaking over the resizer --- lib/Resizeable/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 7984a93db..1a009ae97 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -13,6 +13,7 @@ $gutterColour: #006aff; &__wrapper > *:first-child { width: 100% !important; + overflow-x: hidden; } } From 70aae46097e99c6a66452614a9a5fca75bfe05dc Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:26:57 +0100 Subject: [PATCH 26/33] :bug: fix storybook examples --- lib/Resizeable/index.tsx | 13 +- stories/components/Resizable.stories.tsx | 173 +++++++++++++++++++++-- 2 files changed, 174 insertions(+), 12 deletions(-) diff --git a/lib/Resizeable/index.tsx b/lib/Resizeable/index.tsx index 79a583b2d..a3c1e1a2f 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizeable/index.tsx @@ -24,11 +24,12 @@ export function Resizeable({ minWidth, useGutterOffset = false, }: PropsWithChildren) { + const resizeRef = useRef(null); const resizeWrapperRef = useRef(null); const handleRef = useRef(null); - const min = toPixels(minWidth ?? 0); - const max = toPixels(maxWidth ?? "100%"); const gutterSize = useGutterOffset ? 16 : 0; + let min: number; + let max: number; const [state, setState] = React.useState({ startX: 0, @@ -91,14 +92,18 @@ export function Resizeable({ }; useEffect(() => { - setWidth(keepValueWithinRange(toPixels(defaultWidth), min, max)); + // set initial values on mount as we need to know the resize container width before calculating percentages + min = toPixels(minWidth ?? 0, resizeRef.current?.offsetWidth); + max = toPixels(maxWidth ?? "100%", resizeRef.current?.offsetWidth); + const newWidth = keepValueWithinRange(toPixels(defaultWidth), min, max); + setWidth(newWidth); // remember to remove global listeners on dismount return () => stopDrag(); }, []); return ( -
+
{children}
@@ -29,6 +33,146 @@ export function Resizeable() { maxWidth="33.33%" minWidth="240px" useGutterOffset + > + + + + + + + + All items + + 200 + + + + + + + + + + Assigned items + + 40 + + + + + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + + + + + + Another Folder + + + + + + + + + Another Folder + + + + + + + + + Another Folder + + + + + + + +
+
+ + +
+
- Hello +

+ Hello, World! Lorem Ipsum is simply dummy text of the printing + and typesetting industry. Lorem Ipsum has been the industry's + standard dummy text ever since the 1500s, when an unknown + printer took a galley of type and scrambled it to make a type + specimen book. It has survived not only five centuries, but also + the leap into electronic typesetting, remaining essentially + unchanged. It was popularised in the 1960s with the release of + Letraset sheets containing Lorem Ipsum passages, and more + recently with desktop publishing software like Aldus PageMaker + including versions of Lorem Ipsum. +

- How are you? +

How are you?

@@ -58,7 +213,7 @@ export function Resizeable() { description="Could be used to reveal a before and after of two images" >
- Not resizeable +

Not resizeable

-
Hello
+
+

Hello

+
- Not resizeable +

Not resizeable

From 2290f68ccc44ecb299ce7bafaecc8066345b58c4 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:35:22 +0100 Subject: [PATCH 27/33] :white_check_mark: spy on console.warn --- lib/.specs/helpers.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/.specs/helpers.spec.ts b/lib/.specs/helpers.spec.ts index 151e13f06..4d4d23cce 100644 --- a/lib/.specs/helpers.spec.ts +++ b/lib/.specs/helpers.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { keepValueWithinRange, toPixels } from "../helpers"; describe("helpers: toPixels", () => { @@ -19,7 +19,13 @@ describe("helpers: toPixels", () => { }); it("should return number and warning for unknown units", () => { + const warn = vi.spyOn(global.console, "warn"); + expect(toPixels("123rem")).toBe(123); + expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith( + `Could not interpret a normalised value for: 123rem.\nParsing directly to integer: 123.` + ); }); }); From 0f5e73c242fb3c58d704cb1868091f80590607bb Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 14:57:58 +0100 Subject: [PATCH 28/33] :lipstick: make examples 100% page width --- lib/Resizeable/styles.scss | 1 + stories/components/Resizable.stories.tsx | 281 ++++++++++++----------- 2 files changed, 150 insertions(+), 132 deletions(-) diff --git a/lib/Resizeable/styles.scss b/lib/Resizeable/styles.scss index 1a009ae97..05fa73049 100644 --- a/lib/Resizeable/styles.scss +++ b/lib/Resizeable/styles.scss @@ -4,6 +4,7 @@ $gutterColour: #006aff; .resizeable { position: relative; display: flex; + width: 100%; &__wrapper { position: relative; diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 368f98c31..1cf26f9c5 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -26,131 +26,17 @@ export function Resizeable() { flex: "1", minHeight: "0", height: 600, + width: "100%", }} > - - - - - - - - All items - - 200 - - - - - - - - - - Assigned items - - 40 - - - - - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - Another Folder - - - - - - - - - - - - Another Folder - - - - - - - - - Another Folder - - - - - - - - - Another Folder - - - - - - + {/* eslint-disable-next-line no-use-before-define */} +
@@ -166,6 +52,7 @@ export function Resizeable() { minHeight: "0", border: "1px solid red", height: 200, + width: "100%", }} > -
-

Not resizeable

-
- -
-

Hello

+
+
+

Not resizeable

+
+ +
+

Hello

+
+
+
+

Not resizeable

- -
-

Not resizeable

); } + +function FolderMenu() { + return ( + + + + + + + + All items + + 200 + + + + + + + + + + Assigned items + + 40 + + + + + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + Another Folder + + + + + + + + + + + + Another Folder + + + + + + + + + Another Folder + + + + + + + + + Another Folder + + + + + + + ); +} From 69183c88e0d110f92f3327abea59c78144244679 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Fri, 15 Sep 2023 16:36:02 +0100 Subject: [PATCH 29/33] :pencil2: use resizable spelling, containerWidth prop --- lib/{Resizeable => Resizable}/index.tsx | 23 +++++++++++++---------- lib/{Resizeable => Resizable}/styles.scss | 3 +-- lib/index.ts | 2 +- stories/components/Resizable.stories.tsx | 17 ++++++++++------- styles/components/_ui.scss | 2 +- 5 files changed, 26 insertions(+), 21 deletions(-) rename lib/{Resizeable => Resizable}/index.tsx (85%) rename lib/{Resizeable => Resizable}/styles.scss (96%) diff --git a/lib/Resizeable/index.tsx b/lib/Resizable/index.tsx similarity index 85% rename from lib/Resizeable/index.tsx rename to lib/Resizable/index.tsx index a3c1e1a2f..63ef7a0ce 100644 --- a/lib/Resizeable/index.tsx +++ b/lib/Resizable/index.tsx @@ -2,11 +2,12 @@ import type { PropsWithChildren } from "react"; import React, { useCallback, useEffect, useRef } from "react"; import { keepValueWithinRange, toPixels } from "../helpers"; -export interface ResizeableProps { +export interface ResizableProps { defaultWidth?: number | string; minWidth?: number | string; maxWidth?: number | string; useGutterOffset?: boolean; + containerWidth?: number | string; } /** @@ -16,20 +17,25 @@ export interface ResizeableProps { * @param maxWidth The maximum width the element can be resized to * @param minWidth The minimum width the element can be resized to * @param useGutterOffset Toggle whether we should take the width of the gutter into account for calculating the width of the resized item. This can be handy depending on the CSS being applied to or around the child element + * @param rest Contains params that need converting such as the containerWidth */ -export function Resizeable({ +export function Resizable({ children, defaultWidth = "50%", maxWidth, minWidth, useGutterOffset = false, -}: PropsWithChildren) { + ...rest +}: PropsWithChildren) { + const containerWidth = toPixels( + rest.containerWidth ?? document.body.offsetWidth + ); const resizeRef = useRef(null); const resizeWrapperRef = useRef(null); const handleRef = useRef(null); const gutterSize = useGutterOffset ? 16 : 0; - let min: number; - let max: number; + const min = toPixels(minWidth ?? 0, containerWidth); + const max = toPixels(maxWidth ?? "100%", containerWidth); const [state, setState] = React.useState({ startX: 0, @@ -92,9 +98,6 @@ export function Resizeable({ }; useEffect(() => { - // set initial values on mount as we need to know the resize container width before calculating percentages - min = toPixels(minWidth ?? 0, resizeRef.current?.offsetWidth); - max = toPixels(maxWidth ?? "100%", resizeRef.current?.offsetWidth); const newWidth = keepValueWithinRange(toPixels(defaultWidth), min, max); setWidth(newWidth); @@ -103,8 +106,8 @@ export function Resizeable({ }, []); return ( -
-
+
+
{children} +
+

This content will appear next to the resizable component

+
-

Not resizeable

+

Not resizable

-

Not resizeable

+

Not resizable

diff --git a/styles/components/_ui.scss b/styles/components/_ui.scss index a30ceaf88..cf7bbb0b9 100644 --- a/styles/components/_ui.scss +++ b/styles/components/_ui.scss @@ -11,7 +11,7 @@ @import "../../lib/DropdownMenu/styles"; @import "../../lib/Progress/styles"; @import "../../lib/ProgressButton/styles"; -@import "../../lib/Resizeable/styles"; +@import "../../lib/Resizable/styles"; @import "../../lib/Modal/styles"; @import "../../lib/FigurePlaceholder/styles"; @import "../../lib/Conversation/styles"; From b1140f0074ab8c47c593d51584e53ad235c190de Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Tue, 26 Sep 2023 10:19:49 +0100 Subject: [PATCH 30/33] :zap: keep reference to event handlers for clean event listener removal --- lib/Resizable/index.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/Resizable/index.tsx b/lib/Resizable/index.tsx index 63ef7a0ce..4c4355a7c 100644 --- a/lib/Resizable/index.tsx +++ b/lib/Resizable/index.tsx @@ -30,6 +30,8 @@ export function Resizable({ const containerWidth = toPixels( rest.containerWidth ?? document.body.offsetWidth ); + const doDragRef = useRef<(evt: MouseEvent) => void>(() => {}); + const stopDragRef = useRef<(evt: MouseEvent) => void>(() => {}); const resizeRef = useRef(null); const resizeWrapperRef = useRef(null); const handleRef = useRef(null); @@ -58,22 +60,21 @@ export function Resizable({ }; const stopDrag = () => { - document.removeEventListener("mousemove", doDrag, false); - document.removeEventListener("mouseup", stopDrag, false); + document.removeEventListener("mousemove", doDragRef.current, false); + document.removeEventListener("mouseup", stopDragRef.current, false); }; const initDrag = useCallback( (evt: React.DragEvent) => { const { clientX } = evt; - const newState = { - ...state, + setState({ startX: clientX, startWidth: getWidth(), - }; - - setState(newState); + }); + doDragRef.current = doDrag; + stopDragRef.current = stopDrag; document.addEventListener("mousemove", doDrag, false); document.addEventListener("mouseup", stopDrag, false); }, From 6aa07e03c749730ef3d31421525323733aac45b3 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Tue, 26 Sep 2023 10:28:04 +0100 Subject: [PATCH 31/33] :label: swap jsdoc for better variable names --- lib/Resizable/index.tsx | 35 ++++++++++-------------- stories/components/Resizable.stories.tsx | 18 ++++++------ 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/Resizable/index.tsx b/lib/Resizable/index.tsx index 4c4355a7c..c781b61cb 100644 --- a/lib/Resizable/index.tsx +++ b/lib/Resizable/index.tsx @@ -3,27 +3,18 @@ import React, { useCallback, useEffect, useRef } from "react"; import { keepValueWithinRange, toPixels } from "../helpers"; export interface ResizableProps { - defaultWidth?: number | string; - minWidth?: number | string; - maxWidth?: number | string; - useGutterOffset?: boolean; containerWidth?: number | string; + initialWidth?: number | string; + minResizableWidth?: number | string; + maxResizableWidth?: number | string; + useGutterOffset?: boolean; } -/** - * A wrapper for making a given child element resizable - * @param children The element to be resized - * @param defaultWidth The width the child should initially render to. If min and max widths are provided, then the provided width will be massaged to meet those constraints - * @param maxWidth The maximum width the element can be resized to - * @param minWidth The minimum width the element can be resized to - * @param useGutterOffset Toggle whether we should take the width of the gutter into account for calculating the width of the resized item. This can be handy depending on the CSS being applied to or around the child element - * @param rest Contains params that need converting such as the containerWidth - */ export function Resizable({ children, - defaultWidth = "50%", - maxWidth, - minWidth, + initialWidth = "50%", + maxResizableWidth, + minResizableWidth, useGutterOffset = false, ...rest }: PropsWithChildren) { @@ -36,8 +27,8 @@ export function Resizable({ const resizeWrapperRef = useRef(null); const handleRef = useRef(null); const gutterSize = useGutterOffset ? 16 : 0; - const min = toPixels(minWidth ?? 0, containerWidth); - const max = toPixels(maxWidth ?? "100%", containerWidth); + const minWidth = toPixels(minResizableWidth ?? 0, containerWidth); + const maxWidth = toPixels(maxResizableWidth ?? "100%", containerWidth); const [state, setState] = React.useState({ startX: 0, @@ -50,7 +41,7 @@ export function Resizable({ if (resizeWrapperRef.current === null) return; resizeWrapperRef.current.style.width = `${ - keepValueWithinRange(value, min, max) - gutterSize + keepValueWithinRange(value, minWidth, maxWidth) - gutterSize }px`; }; @@ -99,7 +90,11 @@ export function Resizable({ }; useEffect(() => { - const newWidth = keepValueWithinRange(toPixels(defaultWidth), min, max); + const newWidth = keepValueWithinRange( + toPixels(initialWidth), + minWidth, + maxWidth + ); setWidth(newWidth); // remember to remove global listeners on dismount diff --git a/stories/components/Resizable.stories.tsx b/stories/components/Resizable.stories.tsx index 9213056db..6a871c3b4 100644 --- a/stories/components/Resizable.stories.tsx +++ b/stories/components/Resizable.stories.tsx @@ -30,9 +30,9 @@ export function Resizable() { }} > {/* eslint-disable-next-line no-use-before-define */} @@ -59,9 +59,9 @@ export function Resizable() { }} >
Not resizable

From ed8cb414dd0ac5b9abe7e9d0f1a07f1c60695433 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Tue, 26 Sep 2023 10:30:26 +0100 Subject: [PATCH 32/33] :recycle: remove duplicated keepValueWithinRange calculation --- lib/Resizable/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/Resizable/index.tsx b/lib/Resizable/index.tsx index c781b61cb..b6bfcfc67 100644 --- a/lib/Resizable/index.tsx +++ b/lib/Resizable/index.tsx @@ -90,12 +90,7 @@ export function Resizable({ }; useEffect(() => { - const newWidth = keepValueWithinRange( - toPixels(initialWidth), - minWidth, - maxWidth - ); - setWidth(newWidth); + setWidth(toPixels(initialWidth)); // remember to remove global listeners on dismount return () => stopDrag(); From 3ec581d1d7ec0b0c73309523625e5d9507677656 Mon Sep 17 00:00:00 2001 From: TimBryanDev Date: Tue, 26 Sep 2023 10:41:06 +0100 Subject: [PATCH 33/33] :recycle: replace rest prop in favor of prop destructuring --- lib/Resizable/index.tsx | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/Resizable/index.tsx b/lib/Resizable/index.tsx index b6bfcfc67..3d7e8815f 100644 --- a/lib/Resizable/index.tsx +++ b/lib/Resizable/index.tsx @@ -10,25 +10,20 @@ export interface ResizableProps { useGutterOffset?: boolean; } -export function Resizable({ - children, - initialWidth = "50%", - maxResizableWidth, - minResizableWidth, - useGutterOffset = false, - ...rest -}: PropsWithChildren) { - const containerWidth = toPixels( - rest.containerWidth ?? document.body.offsetWidth +export function Resizable(props: PropsWithChildren) { + const { children, initialWidth = "50%", useGutterOffset = false } = props; + const containerWidth: number = toPixels( + props.containerWidth ?? document.body.offsetWidth ); + const gutterSize = useGutterOffset ? 16 : 0; + const minWidth = toPixels(props.minResizableWidth ?? 0, containerWidth); + const maxWidth = toPixels(props.maxResizableWidth ?? "100%", containerWidth); + const doDragRef = useRef<(evt: MouseEvent) => void>(() => {}); const stopDragRef = useRef<(evt: MouseEvent) => void>(() => {}); const resizeRef = useRef(null); const resizeWrapperRef = useRef(null); const handleRef = useRef(null); - const gutterSize = useGutterOffset ? 16 : 0; - const minWidth = toPixels(minResizableWidth ?? 0, containerWidth); - const maxWidth = toPixels(maxResizableWidth ?? "100%", containerWidth); const [state, setState] = React.useState({ startX: 0,