-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1296 from Bynder/feature/GC-1464-resizable-sidebar
💄 GC-1464 Allow users to resize the sidebar in the Content Hub
- Loading branch information
Showing
8 changed files
with
526 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { describe, expect, it, vi } 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", () => { | ||
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.` | ||
); | ||
}); | ||
}); | ||
|
||
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import type { PropsWithChildren } from "react"; | ||
import React, { useCallback, useEffect, useRef } from "react"; | ||
import { keepValueWithinRange, toPixels } from "../helpers"; | ||
|
||
export interface ResizableProps { | ||
containerWidth?: number | string; | ||
initialWidth?: number | string; | ||
minResizableWidth?: number | string; | ||
maxResizableWidth?: number | string; | ||
useGutterOffset?: boolean; | ||
} | ||
|
||
export function Resizable(props: PropsWithChildren<ResizableProps>) { | ||
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<HTMLDivElement>(null); | ||
const resizeWrapperRef = useRef<HTMLDivElement>(null); | ||
const handleRef = useRef<HTMLSpanElement>(null); | ||
|
||
const [state, setState] = React.useState({ | ||
startX: 0, | ||
startWidth: 0, | ||
}); | ||
|
||
const getWidth = () => toPixels(resizeWrapperRef.current?.style.width || 0); | ||
|
||
const setWidth = (value: number) => { | ||
if (resizeWrapperRef.current === null) return; | ||
|
||
resizeWrapperRef.current.style.width = `${ | ||
keepValueWithinRange(value, minWidth, maxWidth) - gutterSize | ||
}px`; | ||
}; | ||
|
||
const doDrag = (evt: MouseEvent) => { | ||
const newWidth = state.startWidth + evt.clientX - state.startX; | ||
setWidth(newWidth); | ||
}; | ||
|
||
const stopDrag = () => { | ||
document.removeEventListener("mousemove", doDragRef.current, false); | ||
document.removeEventListener("mouseup", stopDragRef.current, false); | ||
}; | ||
|
||
const initDrag = useCallback( | ||
(evt: React.DragEvent<HTMLDivElement>) => { | ||
const { clientX } = evt; | ||
|
||
setState({ | ||
startX: clientX, | ||
startWidth: getWidth(), | ||
}); | ||
|
||
doDragRef.current = doDrag; | ||
stopDragRef.current = stopDrag; | ||
document.addEventListener("mousemove", doDrag, false); | ||
document.addEventListener("mouseup", stopDrag, false); | ||
}, | ||
[setState] | ||
); | ||
|
||
const setGutterHandlePosition = (evt: React.MouseEvent<HTMLSpanElement>) => { | ||
const handle = handleRef.current; | ||
if (handle === null) return; | ||
|
||
const rect = evt.currentTarget.getBoundingClientRect(); | ||
const gutter = handle.parentElement as HTMLSpanElement; | ||
const handleOffset = handle.offsetHeight / 2; | ||
|
||
const yPosition = keepValueWithinRange( | ||
evt.clientY - rect.top, | ||
handleOffset, | ||
gutter.offsetHeight - handleOffset | ||
); | ||
|
||
handle.style.top = `${yPosition - handleOffset}px`; | ||
}; | ||
|
||
useEffect(() => { | ||
setWidth(toPixels(initialWidth)); | ||
|
||
// remember to remove global listeners on dismount | ||
return () => stopDrag(); | ||
}, []); | ||
|
||
return ( | ||
<div ref={resizeRef} className="resizable"> | ||
<div ref={resizeWrapperRef} className="resizable__wrapper"> | ||
{children} | ||
<span | ||
role="none" | ||
className="resizable__gutter" | ||
onMouseDown={initDrag} | ||
onMouseMove={setGutterHandlePosition} | ||
> | ||
<span ref={handleRef} className="resizable__gutter-handle" /> | ||
</span> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
$gutterSize: 16px; | ||
$gutterColour: #006aff; | ||
|
||
.resizable { | ||
position: relative; | ||
display: flex; | ||
|
||
&__wrapper { | ||
position: relative; | ||
display: flex; | ||
align-items: stretch; | ||
} | ||
|
||
&__wrapper > *:first-child { | ||
width: 100% !important; | ||
overflow-x: hidden; | ||
} | ||
} | ||
|
||
.resizable__gutter { | ||
position: absolute; | ||
user-select: none; | ||
height: 100%; | ||
width: $gutterSize; | ||
top: 0; | ||
right: -(calc($gutterSize / 2)); | ||
cursor: ew-resize; | ||
|
||
&::before { | ||
content: ""; | ||
width: 2px; | ||
margin: auto; | ||
background-color: $gutterColour; | ||
display: block; | ||
height: 100%; | ||
} | ||
} | ||
|
||
.resizable__gutter-handle { | ||
background-color: $gutterColour; | ||
border-radius: 4px; | ||
height: 30px; | ||
margin: auto; | ||
width: 8px; | ||
display: flex; | ||
position: absolute; | ||
transform: translateX(-50%); | ||
left: 50%; | ||
opacity: 0; | ||
transition: opacity 0.1s; | ||
|
||
.resizable__gutter:hover & { | ||
opacity: 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,49 @@ | ||
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 toPixels = (value: string | number, percentageOf = 100) => { | ||
if (typeof value === "number") return value; | ||
|
||
const integer = parseInt(value, 10); | ||
|
||
if (value.endsWith("px")) { | ||
return integer; | ||
} | ||
|
||
if (value.endsWith("%")) { | ||
return (percentageOf / 100) * integer; | ||
} | ||
|
||
console.warn( | ||
`Could not interpret a normalised value for: ${value}.\nParsing directly to integer: ${integer}.` | ||
); | ||
return integer; | ||
}; | ||
|
||
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.