Implemented new OwnerStack UI enhancement

This commit is contained in:
Hristo Kanchev
2019-04-07 11:37:12 +02:00
parent 5334249bda
commit ce04f531d4
7 changed files with 226 additions and 39 deletions
-1
View File
@@ -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
View File
@@ -7,4 +7,5 @@ npm-debug.log
yarn-error.log
.DS_Store
yarn-error.log
.vscode
.vscode
.idea
+1 -18
View File
@@ -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
View File
@@ -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
+42 -2
View File
@@ -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);
+156 -17
View File
@@ -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]);
}
+18
View File
@@ -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],
};
}