mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
## Summary Follow-up to https://github.com/facebook/react/pull/33517. With https://github.com/facebook/react/pull/33517, we now preserve at least some minimal indent. This actually doesn't work with the current setup, because we don't allow the container to overflow, so basically deeply nested elements will go off the screen. With these changes, we completely change the approach: - The layout will be static and it will have a constant indentation that will always be preserved. - The container will allow overflows, so users will be able to scroll horizontally and vertically. - We will implement automatic horizontal and vertical scrolls, if selected element is not in a viewport. - New: added vertical delimiter that can be used for simpler visual navigation. ## Demo ### Current public release https://github.com/user-attachments/assets/58645d42-c6b8-408b-b76f-95fb272f2e1e ### With https://github.com/facebook/react/pull/33517 https://github.com/user-attachments/assets/845285c8-5a01-4739-bcd7-ffc089e771bf ### This PR https://github.com/user-attachments/assets/72086b84-8d84-4626-94b3-e22e114e028e
247 lines
6.8 KiB
JavaScript
247 lines
6.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {Fragment, useEffect, useLayoutEffect, useReducer, useRef} from 'react';
|
|
import Tree from './Tree';
|
|
import {OwnersListContextController} from './OwnersListContext';
|
|
import portaledContent from '../portaledContent';
|
|
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
|
|
import {
|
|
localStorageGetItem,
|
|
localStorageSetItem,
|
|
} from 'react-devtools-shared/src/storage';
|
|
import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
|
|
import InspectedElement from './InspectedElement';
|
|
import {ModalDialog} from '../ModalDialog';
|
|
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
|
|
import {NativeStyleContextController} from './NativeStyleEditor/context';
|
|
|
|
import styles from './Components.css';
|
|
|
|
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 Components(_: {}) {
|
|
const wrapperElementRef = useRef<null | HTMLElement>(null);
|
|
const resizeElementRef = useRef<null | HTMLElement>(null);
|
|
|
|
const [state, dispatch] = useReducer<ResizeState, any, 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});
|
|
|
|
let onResize;
|
|
let onResizeEnd;
|
|
if (isResizing) {
|
|
onResizeEnd = () =>
|
|
dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false});
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
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 (
|
|
<SettingsModalContextController>
|
|
<OwnersListContextController>
|
|
<div
|
|
ref={wrapperElementRef}
|
|
className={styles.Components}
|
|
onMouseMove={onResize}
|
|
onMouseLeave={onResizeEnd}
|
|
onMouseUp={onResizeEnd}>
|
|
<Fragment>
|
|
<div ref={resizeElementRef} className={styles.TreeWrapper}>
|
|
<Tree />
|
|
</div>
|
|
<div className={styles.ResizeBarWrapper}>
|
|
<div onMouseDown={onResizeStart} className={styles.ResizeBar} />
|
|
</div>
|
|
<div className={styles.InspectedElementWrapper}>
|
|
<NativeStyleContextController>
|
|
<InspectedElementErrorBoundary>
|
|
<InspectedElement />
|
|
</InspectedElementErrorBoundary>
|
|
</NativeStyleContextController>
|
|
</div>
|
|
<ModalDialog />
|
|
<SettingsModal />
|
|
</Fragment>
|
|
</div>
|
|
</OwnersListContextController>
|
|
</SettingsModalContextController>
|
|
);
|
|
}
|
|
|
|
const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
|
|
const VERTICAL_MODE_MAX_WIDTH = 600;
|
|
const MINIMUM_SIZE = 100;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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}%`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export default (portaledContent(Components): React$ComponentType<{}>);
|