From d166319e864c0097c9e763180df4add375f2f1cd Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Wed, 26 Feb 2020 20:30:42 +0100 Subject: [PATCH] [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 --- .../devtools/views/Components/Components.css | 44 ++-- .../devtools/views/Components/Components.js | 42 ++-- .../views/Components/ComponentsResizer.css | 16 ++ .../views/Components/ComponentsResizer.js | 213 ++++++++++++++++++ 4 files changed, 280 insertions(+), 35 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.css create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index f37b3eaa73d3f..5db5eaea2d667 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -1,36 +1,42 @@ -.Components { - position: relative; - width: 100%; - height: 100%; - display: flex; - flex-direction: row; - background-color: var(--color-background); - color: var(--color-text); - font-family: var(--font-family-sans); -} - .TreeWrapper { - flex: 0 0 65%; + flex: 0 0 var(--horizontal-resize-percentage); overflow: auto; } .SelectedElementWrapper { - flex: 0 0 35%; + flex: 1 1 35%; overflow-x: hidden; overflow-y: auto; } -@media screen and (max-width: 600px) { - .Components { - flex-direction: column; - } +.ResizeBarWrapper { + flex: 0 0 0px; + position: relative; +} + +.ResizeBar { + position: absolute; + left: -2px; + width: 5px; + height: 100%; + cursor: ew-resize; +} +@media screen and (max-width: 600px) { .TreeWrapper { - flex: 0 0 50%; + flex: 0 0 var(--vertical-resize-percentage); } .SelectedElementWrapper { - flex: 0 0 50%; + flex: 1 1 50%; + } + + .ResizeBar { + top: -2px; + left: 0; + width: 100%; + height: 5px; + cursor: ns-resize; } } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 6b65e0d2035f9..af1cf3ac95f7b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -8,12 +8,13 @@ */ import * as React from 'react'; -import {Suspense} from 'react'; +import {Suspense, Fragment} from 'react'; import Tree from './Tree'; import SelectedElement from './SelectedElement'; import {InspectedElementContextController} from './InspectedElementContext'; import {NativeStyleContextController} from './NativeStyleEditor/context'; import {OwnersListContextController} from './OwnersListContext'; +import ComponentsResizer from './ComponentsResizer'; import portaledContent from '../portaledContent'; import {ModalDialog} from '../ModalDialog'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; @@ -22,25 +23,34 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools import styles from './Components.css'; function Components(_: {||}) { - // TODO Flex wrappers below should be user resizable. return ( -
-
- -
-
- - }> - - - -
- - -
+ + {({resizeElementRef, onResizeStart}) => ( + +
+ +
+
+
+
+
+ + }> + + + +
+ + + + )} + diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.css b/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.css new file mode 100644 index 0000000000000..8a86aa1246a16 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.css @@ -0,0 +1,16 @@ +.ComponentsWrapper { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + background-color: var(--color-background); + color: var(--color-text); + font-family: var(--font-family-sans); +} + +@media screen and (max-width: 600px) { + .ComponentsWrapper { + flex-direction: column; + } +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js b/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js new file mode 100644 index 0000000000000..f97466843ea59 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js @@ -0,0 +1,213 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {useEffect, useLayoutEffect, useReducer, useRef} from 'react'; +import { + localStorageGetItem, + localStorageSetItem, +} from 'react-devtools-shared/src/storage'; +import styles from './ComponentsResizer.css'; + +const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; +const VERTICAL_MODE_MAX_WIDTH = 600; +const MINIMUM_SIZE = 50; + +type Orientation = 'horizontal' | 'vertical'; + +type ResizeActionType = + | 'ACTION_SET_DID_MOUNT' + | 'ACTION_SET_IS_RESIZING' + | 'ACTION_SET_HORIZONTAL_PERCENTAGE' + | 'ACTION_SET_VERTICAL_PERCENTAGE'; + +type ResizeAction = {| + type: ResizeActionType, + payload: any, +|}; + +type ResizeState = {| + horizontalPercentage: number, + isResizing: boolean, + verticalPercentage: number, +|}; + +function initResizeState(): ResizeState { + let horizontalPercentage = 0.65; + let verticalPercentage = 0.5; + + try { + let data = localStorageGetItem(LOCAL_STORAGE_KEY); + if (data != null) { + data = JSON.parse(data); + horizontalPercentage = data.horizontalPercentage; + verticalPercentage = data.verticalPercentage; + } + } catch (error) {} + + return { + horizontalPercentage, + isResizing: false, + verticalPercentage, + }; +} + +function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { + switch (action.type) { + case 'ACTION_SET_IS_RESIZING': + return { + ...state, + isResizing: action.payload, + }; + case 'ACTION_SET_HORIZONTAL_PERCENTAGE': + return { + ...state, + horizontalPercentage: action.payload, + }; + case 'ACTION_SET_VERTICAL_PERCENTAGE': + return { + ...state, + verticalPercentage: action.payload, + }; + default: + return state; + } +} + +type Props = {| + children: ({ + resizeElementRef: React$Ref, + onResizeStart: () => void, + }) => React$Node, +|}; + +export default function ComponentsResizer({children}: Props) { + const wrapperElementRef = useRef(null); + const resizeElementRef = useRef(null); + + const [state, dispatch] = useReducer( + resizeReducer, + null, + initResizeState, + ); + + const {horizontalPercentage, verticalPercentage} = state; + + useLayoutEffect(() => { + const resizeElement = resizeElementRef.current; + + setResizeCSSVariable( + resizeElement, + 'horizontal', + horizontalPercentage * 100, + ); + setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100); + }, []); + + useEffect(() => { + const timeoutID = setTimeout(() => { + localStorageSetItem( + LOCAL_STORAGE_KEY, + JSON.stringify({ + horizontalPercentage, + verticalPercentage, + }), + ); + }, 500); + + return () => clearTimeout(timeoutID); + }, [horizontalPercentage, verticalPercentage]); + + const {isResizing} = state; + + const onResizeStart = () => + dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true}); + const onResizeEnd = () => + dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false}); + + const onResize = event => { + const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperElementRef.current; + + if (!isResizing || wrapperElement === null || resizeElement === null) { + return; + } + + event.preventDefault(); + + const orientation = getOrientation(wrapperElement); + + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + + const currentMousePosition = + orientation === 'horizontal' ? event.clientX - left : event.clientY - top; + + const boundaryMin = MINIMUM_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_SIZE + : height - MINIMUM_SIZE; + + const isMousePositionInBounds = + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; + + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = + orientation === 'horizontal' + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' + : 'ACTION_SET_VERTICAL_PERCENTAGE'; + const percentage = (currentMousePosition / resizedElementDimension) * 100; + + setResizeCSSVariable(resizeElement, orientation, percentage); + + dispatch({ + type: actionType, + payload: currentMousePosition / resizedElementDimension, + }); + } + }; + + return ( +
+ {children({resizeElementRef, onResizeStart})} +
+ ); +} + +function getOrientation( + wrapperElement: null | HTMLElement, +): null | Orientation { + if (wrapperElement != null) { + const {width} = wrapperElement.getBoundingClientRect(); + return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; + } + return null; +} + +function setResizeCSSVariable( + resizeElement: null | HTMLElement, + orientation: null | Orientation, + percentage: number, +): void { + if (resizeElement !== null && orientation !== null) { + resizeElement.style.setProperty( + `--${orientation}-resize-percentage`, + `${percentage}%`, + ); + } +}