main
js 242 lines 6.93 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {Fragment, useEffect, useLayoutEffect, useReducer, useRef} from 'react';
12 import Tree from './Tree';
13 import {OwnersListContextController} from './OwnersListContext';
14 import portaledContent from '../portaledContent';
15 import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
16 import {
17 localStorageGetItem,
18 localStorageSetItem,
19 } from 'react-devtools-shared/src/storage';
20 import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
21 import InspectedElement from './InspectedElement';
22 import {ModalDialog} from '../ModalDialog';
23 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
24 import {NativeStyleContextController} from './NativeStyleEditor/context';
25
26 import styles from './Components.css';
27 import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
28
29 type Orientation = 'horizontal' | 'vertical';
30
31 type ResizeActionType =
32 | 'ACTION_SET_DID_MOUNT'
33 | 'ACTION_SET_HORIZONTAL_PERCENTAGE'
34 | 'ACTION_SET_VERTICAL_PERCENTAGE';
35
36 type ResizeAction = {
37 type: ResizeActionType,
38 payload: any,
39 };
40
41 type ResizeState = {
42 horizontalPercentage: number,
43 verticalPercentage: number,
44 };
45
46 function Components(_: {}) {
47 const wrapperElementRef = useRef<null | HTMLElement>(null);
48 const resizeElementRef = useRef<null | HTMLElement>(null);
49
50 const [state, dispatch] = useReducer<ResizeState, any, ResizeAction>(
51 resizeReducer,
52 null,
53 initResizeState,
54 );
55
56 const {horizontalPercentage, verticalPercentage} = state;
57
58 useLayoutEffect(() => {
59 const resizeElement = resizeElementRef.current;
60
61 setResizeCSSVariable(
62 resizeElement,
63 'horizontal',
64 horizontalPercentage * 100,
65 );
66 setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100);
67 }, []);
68
69 useEffect(() => {
70 const timeoutID = setTimeout(() => {
71 localStorageSetItem(
72 LOCAL_STORAGE_KEY,
73 JSON.stringify({
74 horizontalPercentage,
75 verticalPercentage,
76 }),
77 );
78 }, 500);
79
80 return () => clearTimeout(timeoutID);
81 }, [horizontalPercentage, verticalPercentage]);
82
83 const onResizeStart = (event: SyntheticPointerEvent) => {
84 const element = event.currentTarget;
85 element.setPointerCapture(event.pointerId);
86 };
87
88 const onResizeEnd = (event: SyntheticPointerEvent) => {
89 const element = event.currentTarget;
90 element.releasePointerCapture(event.pointerId);
91 };
92
93 const onResize = (event: SyntheticPointerEvent) => {
94 const element = event.currentTarget;
95 const isResizing = element.hasPointerCapture(event.pointerId);
96 if (!isResizing) {
97 return;
98 }
99
100 const resizeElement = resizeElementRef.current;
101 const wrapperElement = wrapperElementRef.current;
102
103 if (wrapperElement === null || resizeElement === null) {
104 return;
105 }
106
107 event.preventDefault();
108
109 const orientation = getOrientation(wrapperElement);
110
111 const {height, width, left, top} = wrapperElement.getBoundingClientRect();
112
113 const currentMousePosition =
114 orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
115
116 const boundaryMin = MINIMUM_SIZE;
117 const boundaryMax =
118 orientation === 'horizontal'
119 ? width - MINIMUM_SIZE
120 : height - MINIMUM_SIZE;
121
122 const isMousePositionInBounds =
123 currentMousePosition > boundaryMin && 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 = (currentMousePosition / resizedElementDimension) * 100;
133
134 setResizeCSSVariable(resizeElement, orientation, percentage);
135
136 dispatch({
137 type: actionType,
138 payload: currentMousePosition / resizedElementDimension,
139 });
140 }
141 };
142
143 return (
144 <SettingsModalContextController>
145 <OwnersListContextController>
146 <div ref={wrapperElementRef} className={styles.Components}>
147 <Fragment>
148 <div ref={resizeElementRef} className={styles.TreeWrapper}>
149 <Tree />
150 </div>
151 <div className={styles.ResizeBarWrapper}>
152 <div
153 onPointerDown={onResizeStart}
154 onPointerMove={onResize}
155 onPointerUp={onResizeEnd}
156 className={styles.ResizeBar}
157 />
158 </div>
159 <div className={styles.InspectedElementWrapper}>
160 <NativeStyleContextController>
161 <InspectedElementErrorBoundary>
162 <InspectedElement
163 fallbackEmpty={
164 'No React element selected. Select an element in the tree to inspect.'
165 }
166 />
167 </InspectedElementErrorBoundary>
168 </NativeStyleContextController>
169 </div>
170 <ModalDialog />
171 <SettingsModal />
172 </Fragment>
173 </div>
174 </OwnersListContextController>
175 </SettingsModalContextController>
176 );
177 }
178
179 const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
180 const VERTICAL_MODE_MAX_WIDTH = 600;
181 const MINIMUM_SIZE = 100;
182
183 function initResizeState(): ResizeState {
184 let horizontalPercentage = 0.65;
185 let verticalPercentage = 0.5;
186
187 try {
188 let data = localStorageGetItem(LOCAL_STORAGE_KEY);
189 if (data != null) {
190 data = JSON.parse(data);
191 horizontalPercentage = data.horizontalPercentage;
192 verticalPercentage = data.verticalPercentage;
193 }
194 } catch (error) {}
195
196 return {
197 horizontalPercentage,
198 verticalPercentage,
199 };
200 }
201
202 function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
203 switch (action.type) {
204 case 'ACTION_SET_HORIZONTAL_PERCENTAGE':
205 return {
206 ...state,
207 horizontalPercentage: action.payload,
208 };
209 case 'ACTION_SET_VERTICAL_PERCENTAGE':
210 return {
211 ...state,
212 verticalPercentage: action.payload,
213 };
214 default:
215 return state;
216 }
217 }
218
219 function getOrientation(
220 wrapperElement: null | HTMLElement,
221 ): null | Orientation {
222 if (wrapperElement != null) {
223 const {width} = wrapperElement.getBoundingClientRect();
224 return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical';
225 }
226 return null;
227 }
228
229 function setResizeCSSVariable(
230 resizeElement: null | HTMLElement,
231 orientation: null | Orientation,
232 percentage: number,
233 ): void {
234 if (resizeElement !== null && orientation !== null) {
235 resizeElement.style.setProperty(
236 `--${orientation}-resize-percentage`,
237 `${percentage}%`,
238 );
239 }
240 }
241
242 export default portaledContent(Components) as component();