mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Implemented new OwnerStack UI enhancement
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
.*node_modules/archiver-utils
|
||||
.*node_modules/babel.*
|
||||
.*node_modules/browserify-zlib/.*
|
||||
.*node_modules/classnames.*
|
||||
.*node_modules/gh-pages/.*
|
||||
.*node_modules/invariant/.*
|
||||
.*node_modules/json-loader.*
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
.vscode
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import assign from 'object-assign';
|
||||
import { getElementDimensions } from '../../utils';
|
||||
|
||||
type Rect = {
|
||||
bottom: number,
|
||||
@@ -215,24 +216,6 @@ function findTipPos(dims, win) {
|
||||
return { top, left: dims.left + margin + 'px' };
|
||||
}
|
||||
|
||||
function getElementDimensions(domElement) {
|
||||
const calculatedStyle = window.getComputedStyle(domElement);
|
||||
return {
|
||||
borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0],
|
||||
borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0],
|
||||
borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0],
|
||||
borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0],
|
||||
marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0],
|
||||
marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0],
|
||||
marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0],
|
||||
marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0],
|
||||
paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0],
|
||||
paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0],
|
||||
paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0],
|
||||
paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0],
|
||||
};
|
||||
}
|
||||
|
||||
// Get the window object for the document that a node belongs to,
|
||||
// or return null if it cannot be found (node not attached to DOM,
|
||||
// etc).
|
||||
|
||||
@@ -7,6 +7,7 @@ export type IconType =
|
||||
| 'back'
|
||||
| 'cancel'
|
||||
| 'close'
|
||||
| 'colon'
|
||||
| 'copy'
|
||||
| 'down'
|
||||
| 'export'
|
||||
@@ -39,6 +40,9 @@ export default function ButtonIcon({ type }: Props) {
|
||||
case 'close':
|
||||
pathData = PATH_CLOSE;
|
||||
break;
|
||||
case 'colon':
|
||||
pathData = PATH_COLON;
|
||||
break;
|
||||
case 'copy':
|
||||
pathData = PATH_COPY;
|
||||
break;
|
||||
@@ -117,6 +121,9 @@ const PATH_CANCEL = `
|
||||
const PATH_CLOSE =
|
||||
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
|
||||
|
||||
const PATH_COLON =
|
||||
'M10,9a2,2 0 1,0 4,0a2,2 0 1,0 -4,0 M10,19a2,2 0 1,0 4,0a2,2 0 1,0 -4,0';
|
||||
|
||||
const PATH_COPY = `
|
||||
M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3a2 2 0 0 0 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2
|
||||
2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.Component,
|
||||
@@ -12,7 +11,6 @@
|
||||
color: var(--color-component-name);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
white-space: nowrap;
|
||||
border-radius: 0.125rem;
|
||||
border: none;
|
||||
background: none;
|
||||
@@ -38,7 +36,49 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ElementsBar {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.ElementsBarSelectedOnly {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ElementsBarSelectedOnly .Component {
|
||||
visibility: hidden;
|
||||
}
|
||||
.ElementsBarSelectedOnly .FocusedComponent {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ElementsDropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Dropdown {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
left: 0;
|
||||
min-height: 200px;
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid var(--color-selected-border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Dropdown .Component,
|
||||
.Dropdown .FocusedComponent {
|
||||
display: block;
|
||||
margin: 0.25rem 0.75rem 0.35rem;
|
||||
}
|
||||
|
||||
.DropdownButtonActive {
|
||||
background-color: var(--color-selected-background);
|
||||
color: var(--color-selected-foreground);
|
||||
}
|
||||
|
||||
.VRule {
|
||||
flex: 0 0 auto;
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
|
||||
@@ -1,43 +1,84 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
createRef,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import { TreeContext } from './TreeContext';
|
||||
import { StoreContext } from '../context';
|
||||
import { getElementDimensions } from '../../../utils';
|
||||
|
||||
import type { Element } from './types';
|
||||
|
||||
import styles from './OwnersStack.css';
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { ownerStack, resetOwnerStack } = useContext(TreeContext);
|
||||
type ElementsDropdownProps = {
|
||||
selectedElementIndex: number | null,
|
||||
children: Array<any>,
|
||||
};
|
||||
function ElementsDropdown({
|
||||
selectedElementIndex,
|
||||
children,
|
||||
}: ElementsDropdownProps) {
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const handleClick = useCallback(() => {
|
||||
setIsDropdownVisible(!isDropdownVisible);
|
||||
}, [isDropdownVisible, setIsDropdownVisible]);
|
||||
|
||||
const elements = ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
));
|
||||
useEffect(() => {
|
||||
setIsDropdownVisible(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedElementIndex]);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
<div className={styles.ElementsDropdown}>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={resetOwnerStack}
|
||||
title="Back to tree view"
|
||||
className={classNames(styles.IconButton, {
|
||||
[styles.DropdownButtonActive]: isDropdownVisible,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
title="Open elements dropdown"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
<ButtonIcon type="colon" />
|
||||
</Button>
|
||||
<div className={styles.VRule} />
|
||||
{elements}
|
||||
{isDropdownVisible && <div className={styles.Dropdown}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type ElementsBarProps = {
|
||||
elements: Array<any>,
|
||||
showSelectedOnly: boolean,
|
||||
};
|
||||
const ElementsBar = forwardRef(
|
||||
(
|
||||
{ elements, showSelectedOnly }: ElementsBarProps,
|
||||
ref: Object
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.ElementsBar, {
|
||||
[styles.ElementsBarSelectedOnly]: showSelectedOnly,
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
{elements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type ElementViewProps = {
|
||||
id: number,
|
||||
index: number,
|
||||
};
|
||||
|
||||
function ElementView({ id, index }: Props) {
|
||||
function ElementView({ id, index }: ElementViewProps) {
|
||||
const { ownerStackIndex, selectOwner } = useContext(TreeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const { displayName } = ((store.getElementByID(id): any): Element);
|
||||
@@ -59,3 +100,101 @@ function ElementView({ id, index }: Props) {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext(
|
||||
TreeContext
|
||||
);
|
||||
const [isElementsBarOverflowing, setIsElementsBarOverflowing] = useState(
|
||||
false
|
||||
);
|
||||
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
|
||||
const elementsBarRef = createRef<HTMLDivElement | null>();
|
||||
const elements = ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
if (elementsBarRef.current === null) {
|
||||
return () => {};
|
||||
}
|
||||
const elements = Array.from(elementsBarRef.current.children);
|
||||
const elementsTotalWidth = elements.reduce((acc, el) => {
|
||||
const { offsetWidth } = el;
|
||||
const { marginRight } = getElementDimensions(el);
|
||||
return acc + (offsetWidth + marginRight);
|
||||
}, 0);
|
||||
|
||||
setElementsTotalWidth(elementsTotalWidth);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ownerStackIndex, elementsBarRef]);
|
||||
|
||||
useElementsBarOverflowing(
|
||||
elementsBarRef,
|
||||
elementsTotalWidth,
|
||||
isElementsBarOverflowing => {
|
||||
setIsElementsBarOverflowing(isElementsBarOverflowing);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={resetOwnerStack}
|
||||
title="Back to tree view"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
{isElementsBarOverflowing && (
|
||||
<ElementsDropdown selectedElementIndex={ownerStackIndex}>
|
||||
{elements}
|
||||
</ElementsDropdown>
|
||||
)}
|
||||
<div className={styles.VRule} />
|
||||
<ElementsBar
|
||||
elements={elements}
|
||||
showSelectedOnly={isElementsBarOverflowing}
|
||||
ref={elementsBarRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useElementsBarOverflowing(
|
||||
elementsBarRef: Object,
|
||||
elementsTotalWidth: number,
|
||||
callback: Function
|
||||
) {
|
||||
const isElementsBarOverflowing = useCallback(() => {
|
||||
if (elementsBarRef.current !== null) {
|
||||
const elementsBarWidth = elementsBarRef.current.clientWidth;
|
||||
return elementsBarWidth <= elementsTotalWidth;
|
||||
}
|
||||
return false;
|
||||
}, [elementsBarRef, elementsTotalWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutID = null;
|
||||
const handleResize = () => {
|
||||
callback(isElementsBarOverflowing());
|
||||
};
|
||||
const debounceHandleResize = () => {
|
||||
clearTimeout(((timeoutID: any): TimeoutID));
|
||||
timeoutID = setTimeout(handleResize, 100);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
// It's important to listen to the ownerDocument.defaultView to support the browser extension.
|
||||
// Here we use portals to render individual tabs (e.g. Profiler),
|
||||
// and the root document might belong to a different window.
|
||||
const ownerWindow = elementsBarRef.current.ownerDocument.defaultView;
|
||||
ownerWindow.addEventListener('resize', debounceHandleResize);
|
||||
return () => {
|
||||
ownerWindow.removeEventListener('resize', debounceHandleResize);
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
}, [elementsBarRef, isElementsBarOverflowing, callback]);
|
||||
}
|
||||
|
||||
@@ -63,3 +63,21 @@ export function utfEncodeString(string: string): Uint32Array {
|
||||
function toCodePoint(string: string) {
|
||||
return string.codePointAt(0);
|
||||
}
|
||||
|
||||
export function getElementDimensions(domElement: Element) {
|
||||
const calculatedStyle = window.getComputedStyle(domElement);
|
||||
return {
|
||||
borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0],
|
||||
borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0],
|
||||
borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0],
|
||||
borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0],
|
||||
marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0],
|
||||
marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0],
|
||||
marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0],
|
||||
marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0],
|
||||
paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0],
|
||||
paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0],
|
||||
paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0],
|
||||
paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user