-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(resizing): Add new performant useResizable hook #91548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
94ef302
19050cf
6117068
7cf13b3
6a5ec4f
d4ca136
42d12c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import type {RefObject} from 'react'; | ||
import {useCallback, useEffect, useRef, useState} from 'react'; | ||
|
||
interface UseResizableOptions { | ||
/** | ||
* The starting size of the container, and the size that is set in the onDoubleClick handler. | ||
* | ||
* If `sizeStorageKey` is provided and exists in local storage, | ||
* then this will be ignored in favor of the size stored in local storage. | ||
*/ | ||
initialSize: number; | ||
/** | ||
* The maximum width the container can be resized to. Defaults to Infinity. | ||
*/ | ||
maxWidth: number; | ||
/** | ||
* The minimum width the container can be resized to. Defaults to 100. | ||
*/ | ||
minWidth: number; | ||
/** | ||
* The ref to the element to be resized. | ||
*/ | ||
ref: RefObject<HTMLElement | null>; | ||
/** | ||
* Triggered when the user finishes dragging the resize handle. | ||
*/ | ||
onResizeEnd?: (newWidth: number) => void; | ||
/** | ||
* Triggered when the user starts dragging the resize handle. | ||
*/ | ||
onResizeStart?: () => void; | ||
/** | ||
* The local storage key used to persist the size of the container. If not provided, | ||
* the size will not be persisted and the defaultWidth will be used. | ||
*/ | ||
sizeStorageKey?: string; | ||
} | ||
|
||
interface UseResizableResult { | ||
/** | ||
* Whether the drag handle is held. | ||
*/ | ||
isHeld: boolean; | ||
/** | ||
* Apply this to the drag handle element to include 'reset' functionality. | ||
*/ | ||
onDoubleClick: () => void; | ||
/** | ||
* Attach this to the drag handle element's onMouseDown handler. | ||
*/ | ||
onMouseDown: (e: React.MouseEvent) => void; | ||
/** | ||
* The current size of the container. This is NOT updated during the drag | ||
* event, only after the user finishes dragging. | ||
*/ | ||
size: number; | ||
} | ||
|
||
/** | ||
* Performant hook to support draggable container resizing. | ||
* | ||
* Currently only supports resizing width and not height. | ||
*/ | ||
const useResizable = ({ | ||
ref, | ||
initialSize, | ||
maxWidth, | ||
minWidth, | ||
onResizeEnd, | ||
onResizeStart, | ||
sizeStorageKey, | ||
}: UseResizableOptions): UseResizableResult => { | ||
const [isHeld, setIsHeld] = useState(false); | ||
|
||
const isDraggingRef = useRef<boolean>(false); | ||
const startXRef = useRef<number>(0); | ||
const startWidthRef = useRef<number>(0); | ||
|
||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might make sense to be a useLayoutEffect so it doesn't appear as the smaller size then the bigger size |
||
if (ref.current) { | ||
const storedSize = sizeStorageKey | ||
? parseInt(localStorage.getItem(sizeStorageKey) ?? '', 10) | ||
: undefined; | ||
|
||
ref.current.style.width = `${storedSize ?? initialSize}px`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think whats difficult about using px values here is what if i resize it while i'm on my big monitor then open it again with a smaller window. could it be too large? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you might consider storing the localstorage value as a % and computing the px value There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might depend on what maxValue is for these usecases |
||
} | ||
}, [ref, initialSize, sizeStorageKey]); | ||
|
||
const handleMouseMove = useCallback( | ||
(e: MouseEvent) => { | ||
if (!isDraggingRef.current) return; | ||
|
||
window.requestAnimationFrame(() => { | ||
const deltaX = e.clientX - startXRef.current; | ||
const newWidth = Math.max( | ||
minWidth, | ||
Math.min(maxWidth, startWidthRef.current + deltaX) | ||
); | ||
|
||
if (ref.current) { | ||
ref.current.style.width = `${newWidth}px`; | ||
} | ||
}); | ||
}, | ||
[ref, minWidth, maxWidth] | ||
); | ||
|
||
const handleMouseUp = useCallback(() => { | ||
setIsHeld(false); | ||
const newSize = ref.current?.offsetWidth ?? initialSize; | ||
isDraggingRef.current = false; | ||
document.body.style.cursor = ''; | ||
document.body.style.userSelect = ''; | ||
onResizeEnd?.(newSize); | ||
document.removeEventListener('mousemove', handleMouseMove); | ||
document.removeEventListener('mouseup', handleMouseUp); | ||
if (sizeStorageKey) { | ||
localStorage.setItem(sizeStorageKey, newSize.toString()); | ||
} | ||
}, [handleMouseMove, onResizeEnd, ref, sizeStorageKey, initialSize]); | ||
|
||
const handleMouseDown = useCallback( | ||
(e: React.MouseEvent) => { | ||
setIsHeld(true); | ||
e.preventDefault(); | ||
|
||
const currentWidth = ref.current | ||
? parseInt(getComputedStyle(ref.current).width, 10) | ||
: 0; | ||
|
||
isDraggingRef.current = true; | ||
startXRef.current = e.clientX; | ||
startWidthRef.current = currentWidth; | ||
|
||
document.body.style.cursor = 'ew-resize'; | ||
document.body.style.userSelect = 'none'; | ||
document.addEventListener('mousemove', handleMouseMove); | ||
document.addEventListener('mouseup', handleMouseUp); | ||
onResizeStart?.(); | ||
}, | ||
[ref, onResizeStart, handleMouseMove, handleMouseUp] | ||
); | ||
|
||
const onDoubleClick = useCallback(() => { | ||
if (ref.current) { | ||
ref.current.style.width = `${initialSize}px`; | ||
} | ||
}, [ref, initialSize]); | ||
|
||
return { | ||
isHeld, | ||
size: ref.current?.offsetWidth ?? initialSize, | ||
onMouseDown: handleMouseDown, | ||
onDoubleClick, | ||
}; | ||
}; | ||
|
||
export default useResizable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react aria kinda does these
buttonProps
it might make sense to have ahandleProps
if we think they should be applied in a uniform way