@samitouri / QOS-React-1 / commits / b1cbb482d5

[DevTools] More robust resize handling (#34036)

Sebastian "Sebbie" Silbermann committed Jul 29, 2025 at 17:45 UTC b1cbb482d59d5b756294c3409fded141e809f080
4 files changed +67 -68
.eslintrc.js
+1
@@ -608,6 +608,7 @@ module.exports = {
608 symbol: 'readonly',
609 SyntheticEvent: 'readonly',
610 SyntheticMouseEvent: 'readonly',
611 + SyntheticPointerEvent: 'readonly',
612 Thenable: 'readonly',
613 TimeoutID: 'readonly',
614 WheelEventHandler: 'readonly',
packages/react-devtools-shared/src/devtools/views/Components/Components.css
+9 -3
@@ -16,7 +16,6 @@
16
17 .TreeWrapper {
18 flex: 0 0 var(--horizontal-resize-percentage);
19 - overflow: auto;
19 }
20
21 .InspectedElementWrapper {
@@ -32,7 +31,14 @@
31
32 .ResizeBar {
33 position: absolute;
35 - left: -2px;
34 + /*
35 + * moving the bar out of its bounding box might cause its hitbox to overlap
36 + * with another scrollbar creating disorienting UX where you both resize and scroll
37 + * at the same time.
38 + * If you adjust this value, double check that starting resize right on this edge
39 + * doesn't also cause scroll
40 + */
41 + left: 1px;
42 width: 5px;
43 height: 100%;
44 cursor: ew-resize;
@@ -52,7 +58,7 @@
58 }
59
60 .ResizeBar {
55 - top: -2px;
61 + top: 1px;
62 left: 0;
63 width: 100%;
64 height: 5px;
packages/react-devtools-shared/src/devtools/views/Components/Components.js
+56 -65
@@ -29,7 +29,6 @@ type Orientation = 'horizontal' | 'vertical';
29
30 type ResizeActionType =
31 | 'ACTION_SET_DID_MOUNT'
32 - | 'ACTION_SET_IS_RESIZING'
32 | 'ACTION_SET_HORIZONTAL_PERCENTAGE'
33 | 'ACTION_SET_VERTICAL_PERCENTAGE';
34
@@ -40,7 +39,6 @@ type ResizeAction = {
39
40 type ResizeState = {
41 horizontalPercentage: number,
43 - isResizing: boolean,
42 verticalPercentage: number,
43 };
44
@@ -81,82 +79,81 @@ function Components(_: {}) {
79 return () => clearTimeout(timeoutID);
80 }, [horizontalPercentage, verticalPercentage]);
81
84 - const {isResizing} = state;
82 + const onResizeStart = (event: SyntheticPointerEvent<HTMLElement>) => {
83 + const element = event.currentTarget;
84 + element.setPointerCapture(event.pointerId);
85 + };
86 +
87 + const onResizeEnd = (event: SyntheticPointerEvent<HTMLElement>) => {
88 + const element = event.currentTarget;
89 + element.releasePointerCapture(event.pointerId);
90 + };
91 +
92 + const onResize = (event: SyntheticPointerEvent<HTMLElement>) => {
93 + const element = event.currentTarget;
94 + const isResizing = element.hasPointerCapture(event.pointerId);
95 + if (!isResizing) {
96 + return;
97 + }
98
86 - const onResizeStart = () =>
87 - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true});
99 + const resizeElement = resizeElementRef.current;
100 + const wrapperElement = wrapperElementRef.current;
101
89 - let onResize;
90 - let onResizeEnd;
91 - if (isResizing) {
92 - onResizeEnd = () =>
93 - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false});
102 + if (wrapperElement === null || resizeElement === null) {
103 + return;
104 + }
105
95 - // $FlowFixMe[missing-local-annot]
96 - onResize = event => {
97 - const resizeElement = resizeElementRef.current;
98 - const wrapperElement = wrapperElementRef.current;
106 + event.preventDefault();
107
100 - if (!isResizing || wrapperElement === null || resizeElement === null) {
101 - return;
102 - }
108 + const orientation = getOrientation(wrapperElement);
109
104 - event.preventDefault();
110 + const {height, width, left, top} = wrapperElement.getBoundingClientRect();
111
106 - const orientation = getOrientation(wrapperElement);
112 + const currentMousePosition =
113 + orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
114
108 - const {height, width, left, top} = wrapperElement.getBoundingClientRect();
115 + const boundaryMin = MINIMUM_SIZE;
116 + const boundaryMax =
117 + orientation === 'horizontal'
118 + ? width - MINIMUM_SIZE
119 + : height - MINIMUM_SIZE;
120
110 - const currentMousePosition =
111 - orientation === 'horizontal'
112 - ? event.clientX - left
113 - : event.clientY - top;
121 + const isMousePositionInBounds =
122 + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
123
115 - const boundaryMin = MINIMUM_SIZE;
116 - const boundaryMax =
124 + if (isMousePositionInBounds) {
125 + const resizedElementDimension =
126 + orientation === 'horizontal' ? width : height;
127 + const actionType =
128 orientation === 'horizontal'
118 - ? width - MINIMUM_SIZE
119 - : height - MINIMUM_SIZE;
120 -
121 - const isMousePositionInBounds =
122 - currentMousePosition > boundaryMin &&
123 - currentMousePosition < boundaryMax;
124 -
125 - if (isMousePositionInBounds) {
126 - const resizedElementDimension =
127 - orientation === 'horizontal' ? width : height;
128 - const actionType =
129 - orientation === 'horizontal'
130 - ? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
131 - : 'ACTION_SET_VERTICAL_PERCENTAGE';
132 - const percentage =
133 - (currentMousePosition / resizedElementDimension) * 100;
134 -
135 - setResizeCSSVariable(resizeElement, orientation, percentage);
136 -
137 - dispatch({
138 - type: actionType,
139 - payload: currentMousePosition / resizedElementDimension,
140 - });
141 - }
142 - };
143 - }
129 + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
130 + : 'ACTION_SET_VERTICAL_PERCENTAGE';
131 + const percentage = (currentMousePosition / resizedElementDimension) * 100;
132 +
133 + setResizeCSSVariable(resizeElement, orientation, percentage);
134 +
135 + dispatch({
136 + type: actionType,
137 + payload: currentMousePosition / resizedElementDimension,
138 + });
139 + }
140 + };
141
142 return (
143 <SettingsModalContextController>
144 <OwnersListContextController>
148 - <div
149 - ref={wrapperElementRef}
150 - className={styles.Components}
151 - onMouseMove={onResize}
152 - onMouseLeave={onResizeEnd}
153 - onMouseUp={onResizeEnd}>
145 + <div ref={wrapperElementRef} className={styles.Components}>
146 <Fragment>
147 <div ref={resizeElementRef} className={styles.TreeWrapper}>
148 <Tree />
149 </div>
150 <div className={styles.ResizeBarWrapper}>
159 - <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
151 + <div
152 + onPointerDown={onResizeStart}
153 + onPointerMove={onResize}
154 + onPointerUp={onResizeEnd}
155 + className={styles.ResizeBar}
156 + />
157 </div>
158 <div className={styles.InspectedElementWrapper}>
159 <NativeStyleContextController>
@@ -193,18 +190,12 @@ function initResizeState(): ResizeState {
190
191 return {
192 horizontalPercentage,
196 - isResizing: false,
193 verticalPercentage,
194 };
195 }
196
197 function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
198 switch (action.type) {
203 - case 'ACTION_SET_IS_RESIZING':
204 - return {
205 - ...state,
206 - isResizing: action.payload,
207 - };
199 case 'ACTION_SET_HORIZONTAL_PERCENTAGE':
200 return {
201 ...state,
packages/react-devtools-shared/src/devtools/views/Components/Tree.css
+1
@@ -38,6 +38,7 @@
38 font-family: var(--font-family-monospace);
39 font-size: var(--font-size-monospace-normal);
40 line-height: var(--line-height-data);
41 + user-select: none;
42 }
43
44 .VRule {