Skip to content

feat(nav, resizing): Make secondary nav resizible with new performant hook #91546

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

MichaelSun48
Copy link
Member

@MichaelSun48 MichaelSun48 commented May 13, 2025

Makes the secondary nav resizable via a new hook, useResizable.

useResizable consumes a container ref and updates its width directly, rather than storing and returning a size state like the previous useResizableDrawer, making it feel significantly more performant. It also ensures that the cursor is always attached to the drag handle while dragging, which was an issue with useResizableDrawer. Besides these changes, the API is similar to useResizableDrawer and supports params like onResizeStart, onResizeEnd, and a isHeld return param.

This hook only supports horizontal dragging, but it shouldn't be too hard to extend it to support vertical dragging as well.

Screen.Recording.2025-05-13.at.10.13.15.AM.mov

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label May 13, 2025
width: 8px;
border-radius: 8px;
z-index: ${p => p.theme.zIndex.drawer + 2};
cursor: ${p => (p.atMinWidth ? 'e-resize' : p.atMaxWidth ? 'w-resize' : 'ew-resize')};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have useResizable consume the resize handle as a ref to apply these cursor styles. If we do that, though, then we should also just remove the onMouseDown and onDoubleClick handlers and apply that to the ref directly. This would make the hook significantly more opinionated, which is why I opted not to not have the hook handle.

Comment on lines +56 to +58
initialSize = RESIZABLE_DEFAULT_WIDTH,
maxWidth = RESIZABLE_MAX_WIDTH,
minWidth = RESIZABLE_MIN_WIDTH,
Copy link
Member Author

@MichaelSun48 MichaelSun48 May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered making these non-optional parameters to force people to have an opinion on width config. Would love to get more thoughts on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's do this

@MichaelSun48 MichaelSun48 requested review from malwilley and a team May 13, 2025 17:11
@MichaelSun48 MichaelSun48 marked this pull request as ready for review May 13, 2025 17:12
Comment on lines +4 to +6
export const RESIZABLE_DEFAULT_WIDTH = 200;
export const RESIZABLE_MIN_WIDTH = 100;
export const RESIZABLE_MAX_WIDTH = Infinity;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a hook like this I wouldn't even have defaults. make the callers set these.

Comment on lines +62 to +80
}: UseResizableOptions): {
/**
* 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;
} => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to an interface

interface UseResizeResult { ... }

Comment on lines +56 to +58
initialSize = RESIZABLE_DEFAULT_WIDTH,
maxWidth = RESIZABLE_MAX_WIDTH,
minWidth = RESIZABLE_MIN_WIDTH,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's do this

* The ref to the element to be resized.
*/
ref: RefObject<HTMLElement | null>;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit: I usually don't put whitespace between properties. @TkDodo I actually wonder if there's a lint rule we can apply to this. Not sure if spacing is ever needed since we sort anyway.

* 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this shouldn't be something you set, the CSS should just imply this no?

Comment on lines +117 to +132
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDraggingRef.current) return;

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]
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use RequestAnimationFrame here. Otherwise this isn't performant

@MichaelSun48 MichaelSun48 marked this pull request as draft May 13, 2025 17:19
@MichaelSun48
Copy link
Member Author

Separated the hook into its own PR. Will rebase and undraft this after that is merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants