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
+}