[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 {