mirror of
https://github.com/facebook/react.git
synced 2026-02-24 04:33:04 +00:00
[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>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<SettingsModalContextController>
|
||||
<OwnersListContextController>
|
||||
<InspectedElementContextController>
|
||||
<div className={styles.Components}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<NativeStyleContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</NativeStyleContextController>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
<SettingsModal />
|
||||
</div>
|
||||
<ComponentsResizer>
|
||||
{({resizeElementRef, onResizeStart}) => (
|
||||
<Fragment>
|
||||
<div ref={resizeElementRef} className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.ResizeBarWrapper}>
|
||||
<div
|
||||
onMouseDown={onResizeStart}
|
||||
className={styles.ResizeBar}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<NativeStyleContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</NativeStyleContextController>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
<SettingsModal />
|
||||
</Fragment>
|
||||
)}
|
||||
</ComponentsResizer>
|
||||
</InspectedElementContextController>
|
||||
</OwnersListContextController>
|
||||
</SettingsModalContextController>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
213
packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js
vendored
Normal file
213
packages/react-devtools-shared/src/devtools/views/Components/ComponentsResizer.js
vendored
Normal file
@@ -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<HTMLElement>,
|
||||
onResizeStart: () => void,
|
||||
}) => React$Node,
|
||||
|};
|
||||
|
||||
export default function ComponentsResizer({children}: Props) {
|
||||
const wrapperElementRef = useRef<HTMLElement>(null);
|
||||
const resizeElementRef = useRef<HTMLElement>(null);
|
||||
|
||||
const [state, dispatch] = useReducer<ResizeState, ResizeAction>(
|
||||
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 (
|
||||
<div
|
||||
ref={wrapperElementRef}
|
||||
className={styles.ComponentsWrapper}
|
||||
{...(isResizing && {
|
||||
onMouseMove: onResize,
|
||||
onMouseLeave: onResizeEnd,
|
||||
onMouseUp: onResizeEnd,
|
||||
})}>
|
||||
{children({resizeElementRef, onResizeStart})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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}%`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user