Skip to content

Commit d166319

Browse files
[DevTools] Added resize support for Components panel. (facebook#18046)
* feat: DevTools - Added Resize Support. * feat: Prettier. * feat: DevTools - Added debug comments. * feat: DevTools - Removed Use Memo. * feat: DevTools - Added types. * feat: DevTools - Extracted values to constants. * feat: DevTools - Removed useCallback. * feat: DevTools - Finished refactoring. * feat: DevTools - Merging fixup. * feat: DevTools - Prettier fix. * feat: DevTools - Extracted code from Components fil. * feat: DevTools - Fixed orientation change issue. * feat: DevTools - Added flow types for reducer and refs. * feat: DevTools - Fixed orientation change on initial load. * Update packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js * Removed unused `orientationRef` * Fix Flow ref issue Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
1 parent e1c7e65 commit d166319

File tree

4 files changed

+280
-35
lines changed

4 files changed

+280
-35
lines changed

packages/react-devtools-shared/src/devtools/views/Components/Components.css

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
1-
.Components {
2-
position: relative;
3-
width: 100%;
4-
height: 100%;
5-
display: flex;
6-
flex-direction: row;
7-
background-color: var(--color-background);
8-
color: var(--color-text);
9-
font-family: var(--font-family-sans);
10-
}
11-
121
.TreeWrapper {
13-
flex: 0 0 65%;
2+
flex: 0 0 var(--horizontal-resize-percentage);
143
overflow: auto;
154
}
165

176
.SelectedElementWrapper {
18-
flex: 0 0 35%;
7+
flex: 1 1 35%;
198
overflow-x: hidden;
209
overflow-y: auto;
2110
}
2211

23-
@media screen and (max-width: 600px) {
24-
.Components {
25-
flex-direction: column;
26-
}
12+
.ResizeBarWrapper {
13+
flex: 0 0 0px;
14+
position: relative;
15+
}
16+
17+
.ResizeBar {
18+
position: absolute;
19+
left: -2px;
20+
width: 5px;
21+
height: 100%;
22+
cursor: ew-resize;
23+
}
2724

25+
@media screen and (max-width: 600px) {
2826
.TreeWrapper {
29-
flex: 0 0 50%;
27+
flex: 0 0 var(--vertical-resize-percentage);
3028
}
3129

3230
.SelectedElementWrapper {
33-
flex: 0 0 50%;
31+
flex: 1 1 50%;
32+
}
33+
34+
.ResizeBar {
35+
top: -2px;
36+
left: 0;
37+
width: 100%;
38+
height: 5px;
39+
cursor: ns-resize;
3440
}
3541
}
3642

packages/react-devtools-shared/src/devtools/views/Components/Components.js

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
*/
99

1010
import * as React from 'react';
11-
import {Suspense} from 'react';
11+
import {Suspense, Fragment} from 'react';
1212
import Tree from './Tree';
1313
import SelectedElement from './SelectedElement';
1414
import {InspectedElementContextController} from './InspectedElementContext';
1515
import {NativeStyleContextController} from './NativeStyleEditor/context';
1616
import {OwnersListContextController} from './OwnersListContext';
17+
import ComponentsResizer from './ComponentsResizer';
1718
import portaledContent from '../portaledContent';
1819
import {ModalDialog} from '../ModalDialog';
1920
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
@@ -22,25 +23,34 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
2223
import styles from './Components.css';
2324

2425
function Components(_: {||}) {
25-
// TODO Flex wrappers below should be user resizable.
2626
return (
2727
<SettingsModalContextController>
2828
<OwnersListContextController>
2929
<InspectedElementContextController>
30-
<div className={styles.Components}>
31-
<div className={styles.TreeWrapper}>
32-
<Tree />
33-
</div>
34-
<div className={styles.SelectedElementWrapper}>
35-
<NativeStyleContextController>
36-
<Suspense fallback={<Loading />}>
37-
<SelectedElement />
38-
</Suspense>
39-
</NativeStyleContextController>
40-
</div>
41-
<ModalDialog />
42-
<SettingsModal />
43-
</div>
30+
<ComponentsResizer>
31+
{({resizeElementRef, onResizeStart}) => (
32+
<Fragment>
33+
<div ref={resizeElementRef} className={styles.TreeWrapper}>
34+
<Tree />
35+
</div>
36+
<div className={styles.ResizeBarWrapper}>
37+
<div
38+
onMouseDown={onResizeStart}
39+
className={styles.ResizeBar}
40+
/>
41+
</div>
42+
<div className={styles.SelectedElementWrapper}>
43+
<NativeStyleContextController>
44+
<Suspense fallback={<Loading />}>
45+
<SelectedElement />
46+
</Suspense>
47+
</NativeStyleContextController>
48+
</div>
49+
<ModalDialog />
50+
<SettingsModal />
51+
</Fragment>
52+
)}
53+
</ComponentsResizer>
4454
</InspectedElementContextController>
4555
</OwnersListContextController>
4656
</SettingsModalContextController>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.ComponentsWrapper {
2+
position: relative;
3+
width: 100%;
4+
height: 100%;
5+
display: flex;
6+
flex-direction: row;
7+
background-color: var(--color-background);
8+
color: var(--color-text);
9+
font-family: var(--font-family-sans);
10+
}
11+
12+
@media screen and (max-width: 600px) {
13+
.ComponentsWrapper {
14+
flex-direction: column;
15+
}
16+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import {useEffect, useLayoutEffect, useReducer, useRef} from 'react';
12+
import {
13+
localStorageGetItem,
14+
localStorageSetItem,
15+
} from 'react-devtools-shared/src/storage';
16+
import styles from './ComponentsResizer.css';
17+
18+
const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
19+
const VERTICAL_MODE_MAX_WIDTH = 600;
20+
const MINIMUM_SIZE = 50;
21+
22+
type Orientation = 'horizontal' | 'vertical';
23+
24+
type ResizeActionType =
25+
| 'ACTION_SET_DID_MOUNT'
26+
| 'ACTION_SET_IS_RESIZING'
27+
| 'ACTION_SET_HORIZONTAL_PERCENTAGE'
28+
| 'ACTION_SET_VERTICAL_PERCENTAGE';
29+
30+
type ResizeAction = {|
31+
type: ResizeActionType,
32+
payload: any,
33+
|};
34+
35+
type ResizeState = {|
36+
horizontalPercentage: number,
37+
isResizing: boolean,
38+
verticalPercentage: number,
39+
|};
40+
41+
function initResizeState(): ResizeState {
42+
let horizontalPercentage = 0.65;
43+
let verticalPercentage = 0.5;
44+
45+
try {
46+
let data = localStorageGetItem(LOCAL_STORAGE_KEY);
47+
if (data != null) {
48+
data = JSON.parse(data);
49+
horizontalPercentage = data.horizontalPercentage;
50+
verticalPercentage = data.verticalPercentage;
51+
}
52+
} catch (error) {}
53+
54+
return {
55+
horizontalPercentage,
56+
isResizing: false,
57+
verticalPercentage,
58+
};
59+
}
60+
61+
function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
62+
switch (action.type) {
63+
case 'ACTION_SET_IS_RESIZING':
64+
return {
65+
...state,
66+
isResizing: action.payload,
67+
};
68+
case 'ACTION_SET_HORIZONTAL_PERCENTAGE':
69+
return {
70+
...state,
71+
horizontalPercentage: action.payload,
72+
};
73+
case 'ACTION_SET_VERTICAL_PERCENTAGE':
74+
return {
75+
...state,
76+
verticalPercentage: action.payload,
77+
};
78+
default:
79+
return state;
80+
}
81+
}
82+
83+
type Props = {|
84+
children: ({
85+
resizeElementRef: React$Ref<HTMLElement>,
86+
onResizeStart: () => void,
87+
}) => React$Node,
88+
|};
89+
90+
export default function ComponentsResizer({children}: Props) {
91+
const wrapperElementRef = useRef<HTMLElement>(null);
92+
const resizeElementRef = useRef<HTMLElement>(null);
93+
94+
const [state, dispatch] = useReducer<ResizeState, ResizeAction>(
95+
resizeReducer,
96+
null,
97+
initResizeState,
98+
);
99+
100+
const {horizontalPercentage, verticalPercentage} = state;
101+
102+
useLayoutEffect(() => {
103+
const resizeElement = resizeElementRef.current;
104+
105+
setResizeCSSVariable(
106+
resizeElement,
107+
'horizontal',
108+
horizontalPercentage * 100,
109+
);
110+
setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100);
111+
}, []);
112+
113+
useEffect(() => {
114+
const timeoutID = setTimeout(() => {
115+
localStorageSetItem(
116+
LOCAL_STORAGE_KEY,
117+
JSON.stringify({
118+
horizontalPercentage,
119+
verticalPercentage,
120+
}),
121+
);
122+
}, 500);
123+
124+
return () => clearTimeout(timeoutID);
125+
}, [horizontalPercentage, verticalPercentage]);
126+
127+
const {isResizing} = state;
128+
129+
const onResizeStart = () =>
130+
dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true});
131+
const onResizeEnd = () =>
132+
dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false});
133+
134+
const onResize = event => {
135+
const resizeElement = resizeElementRef.current;
136+
const wrapperElement = wrapperElementRef.current;
137+
138+
if (!isResizing || wrapperElement === null || resizeElement === null) {
139+
return;
140+
}
141+
142+
event.preventDefault();
143+
144+
const orientation = getOrientation(wrapperElement);
145+
146+
const {height, width, left, top} = wrapperElement.getBoundingClientRect();
147+
148+
const currentMousePosition =
149+
orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
150+
151+
const boundaryMin = MINIMUM_SIZE;
152+
const boundaryMax =
153+
orientation === 'horizontal'
154+
? width - MINIMUM_SIZE
155+
: height - MINIMUM_SIZE;
156+
157+
const isMousePositionInBounds =
158+
currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
159+
160+
if (isMousePositionInBounds) {
161+
const resizedElementDimension =
162+
orientation === 'horizontal' ? width : height;
163+
const actionType =
164+
orientation === 'horizontal'
165+
? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
166+
: 'ACTION_SET_VERTICAL_PERCENTAGE';
167+
const percentage = (currentMousePosition / resizedElementDimension) * 100;
168+
169+
setResizeCSSVariable(resizeElement, orientation, percentage);
170+
171+
dispatch({
172+
type: actionType,
173+
payload: currentMousePosition / resizedElementDimension,
174+
});
175+
}
176+
};
177+
178+
return (
179+
<div
180+
ref={wrapperElementRef}
181+
className={styles.ComponentsWrapper}
182+
{...(isResizing && {
183+
onMouseMove: onResize,
184+
onMouseLeave: onResizeEnd,
185+
onMouseUp: onResizeEnd,
186+
})}>
187+
{children({resizeElementRef, onResizeStart})}
188+
</div>
189+
);
190+
}
191+
192+
function getOrientation(
193+
wrapperElement: null | HTMLElement,
194+
): null | Orientation {
195+
if (wrapperElement != null) {
196+
const {width} = wrapperElement.getBoundingClientRect();
197+
return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical';
198+
}
199+
return null;
200+
}
201+
202+
function setResizeCSSVariable(
203+
resizeElement: null | HTMLElement,
204+
orientation: null | Orientation,
205+
percentage: number,
206+
): void {
207+
if (resizeElement !== null && orientation !== null) {
208+
resizeElement.style.setProperty(
209+
`--${orientation}-resize-percentage`,
210+
`${percentage}%`,
211+
);
212+
}
213+
}

0 commit comments

Comments
 (0)