@samitouri / QOS-React-2 / commits / d166319e86

[DevTools] Added resize support for Components panel. (#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>

Hristo Kanchev committed Feb 26, 2020 at 20:30 UTC d166319e864c0097c9e763180df4add375f2f1cd
4 files changed +280 -35
packages/react-devtools-shared/src/devtools/views/Components/Components.css
+25 -19
@@ -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 -
1 .TreeWrapper {
13 - flex: 0 0 65%;
2 + flex: 0 0 var(--horizontal-resize-percentage);
3 overflow: auto;
4 }
5
6 .SelectedElementWrapper {
18 - flex: 0 0 35%;
7 + flex: 1 1 35%;
8 overflow-x: hidden;
9 overflow-y: auto;
10 }
11
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 +}
24
25 +@media screen and (max-width: 600px) {
26 .TreeWrapper {
29 - flex: 0 0 50%;
27 + flex: 0 0 var(--vertical-resize-percentage);
28 }
29
30 .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;
40 }
41 }
42
packages/react-devtools-shared/src/devtools/views/Components/Components.js
+26 -16
@@ -8,12 +8,13 @@
8 */
9
10 import * as React from 'react';
11 -import {Suspense} from 'react';
11 +import {Suspense, Fragment} from 'react';
12 import Tree from './Tree';
13 import SelectedElement from './SelectedElement';
14 import {InspectedElementContextController} from './InspectedElementContext';
15 import {NativeStyleContextController} from './NativeStyleEditor/context';
16 import {OwnersListContextController} from './OwnersListContext';
17 +import ComponentsResizer from './ComponentsResizer';
18 import portaledContent from '../portaledContent';
19 import {ModalDialog} from '../ModalDialog';
20 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
@@ -22,25 +23,34 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
23 import styles from './Components.css';
24
25 function Components(_: {||}) {
25 - // TODO Flex wrappers below should be user resizable.
26 return (
27 <SettingsModalContextController>
28 <OwnersListContextController>
29 <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>
54 </InspectedElementContextController>
55 </OwnersListContextController>
56 </SettingsModalContextController>
packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.css new
+16
@@ -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 +}
packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js new
+213
@@ -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 +}