@samitouri / QOS-React / commits / ce04f531d4

Implemented new OwnerStack UI enhancement

Hristo Kanchev committed Apr 7, 2019 at 11:37 UTC ce04f531d41ca79640ded42e6d1f9736fd8f9e67
7 files changed +226 -39
.flowconfig
-1
@@ -4,7 +4,6 @@
4 .*node_modules/archiver-utils
5 .*node_modules/babel.*
6 .*node_modules/browserify-zlib/.*
7 -.*node_modules/classnames.*
7 .*node_modules/gh-pages/.*
8 .*node_modules/invariant/.*
9 .*node_modules/json-loader.*
.gitignore
+2 -1
@@ -7,4 +7,5 @@ npm-debug.log
7 yarn-error.log
8 .DS_Store
9 yarn-error.log
10 -.vscode
\ No newline at end of file
10 +.vscode
11 +.idea
src/backend/views/Overlay.js
+1 -18
@@ -1,6 +1,7 @@
1 // @flow
2
3 import assign from 'object-assign';
4 +import { getElementDimensions } from '../../utils';
5
6 type Rect = {
7 bottom: number,
@@ -215,24 +216,6 @@ function findTipPos(dims, win) {
216 return { top, left: dims.left + margin + 'px' };
217 }
218
218 -function getElementDimensions(domElement) {
219 - const calculatedStyle = window.getComputedStyle(domElement);
220 - return {
221 - borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0],
222 - borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0],
223 - borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0],
224 - borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0],
225 - marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0],
226 - marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0],
227 - marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0],
228 - marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0],
229 - paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0],
230 - paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0],
231 - paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0],
232 - paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0],
233 - };
234 -}
235 -
219 // Get the window object for the document that a node belongs to,
220 // or return null if it cannot be found (node not attached to DOM,
221 // etc).
src/devtools/views/ButtonIcon.js
+7
@@ -7,6 +7,7 @@ export type IconType =
7 | 'back'
8 | 'cancel'
9 | 'close'
10 + | 'colon'
11 | 'copy'
12 | 'down'
13 | 'export'
@@ -39,6 +40,9 @@ export default function ButtonIcon({ type }: Props) {
40 case 'close':
41 pathData = PATH_CLOSE;
42 break;
43 + case 'colon':
44 + pathData = PATH_COLON;
45 + break;
46 case 'copy':
47 pathData = PATH_COPY;
48 break;
@@ -117,6 +121,9 @@ const PATH_CANCEL = `
121 const PATH_CLOSE =
122 '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';
123
124 +const PATH_COLON =
125 + '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';
126 +
127 const PATH_COPY = `
128 M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3a2 2 0 0 0 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2
129 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
src/devtools/views/Components/OwnersStack.css
+42 -2
@@ -2,7 +2,6 @@
2 flex: 1;
3 display: flex;
4 align-items: center;
5 - overflow-x: auto;
5 }
6
7 .Component,
@@ -12,7 +11,6 @@
11 color: var(--color-component-name);
12 font-family: var(--font-family-monospace);
13 font-size: var(--font-size-monospace-normal);
15 - white-space: nowrap;
14 border-radius: 0.125rem;
15 border: none;
16 background: none;
@@ -38,7 +36,49 @@
36 outline: none;
37 }
38
39 +.ElementsBar {
40 + flex: 1 0 auto;
41 +}
42 +
43 +.ElementsBarSelectedOnly {
44 + margin-left: 0.25rem;
45 +}
46 +
47 +.ElementsBarSelectedOnly .Component {
48 + visibility: hidden;
49 +}
50 +.ElementsBarSelectedOnly .FocusedComponent {
51 + float: left;
52 +}
53 +
54 +.ElementsDropdown {
55 + position: relative;
56 +}
57 +
58 +.Dropdown {
59 + z-index: 1;
60 + position: absolute;
61 + top: calc(100% + 5px);
62 + left: 0;
63 + min-height: 200px;
64 + background-color: var(--color-background);
65 + border: 1px solid var(--color-selected-border);
66 + overflow-y: auto;
67 +}
68 +
69 +.Dropdown .Component,
70 +.Dropdown .FocusedComponent {
71 + display: block;
72 + margin: 0.25rem 0.75rem 0.35rem;
73 +}
74 +
75 +.DropdownButtonActive {
76 + background-color: var(--color-selected-background);
77 + color: var(--color-selected-foreground);
78 +}
79 +
80 .VRule {
81 + flex: 0 0 auto;
82 height: 20px;
83 width: 1px;
84 background-color: var(--color-border);
src/devtools/views/Components/OwnersStack.js
+156 -17
@@ -1,43 +1,84 @@
1 // @flow
2 -
3 -import React, { useCallback, useContext } from 'react';
2 +import React, {
3 + useCallback,
4 + useContext,
5 + useEffect,
6 + useState,
7 + createRef,
8 + forwardRef,
9 +} from 'react';
10 +import classNames from 'classnames';
11 import Button from '../Button';
12 import ButtonIcon from '../ButtonIcon';
13 import { TreeContext } from './TreeContext';
14 import { StoreContext } from '../context';
15 +import { getElementDimensions } from '../../../utils';
16
17 import type { Element } from './types';
18
19 import styles from './OwnersStack.css';
20
13 -export default function OwnerStack() {
14 - const { ownerStack, resetOwnerStack } = useContext(TreeContext);
21 +type ElementsDropdownProps = {
22 + selectedElementIndex: number | null,
23 + children: Array<any>,
24 +};
25 +function ElementsDropdown({
26 + selectedElementIndex,
27 + children,
28 +}: ElementsDropdownProps) {
29 + const [isDropdownVisible, setIsDropdownVisible] = useState(false);
30 + const handleClick = useCallback(() => {
31 + setIsDropdownVisible(!isDropdownVisible);
32 + }, [isDropdownVisible, setIsDropdownVisible]);
33
16 - const elements = ownerStack.map((id, index) => (
17 - <ElementView key={id} id={id} index={index} />
18 - ));
34 + useEffect(() => {
35 + setIsDropdownVisible(false);
36 + // eslint-disable-next-line react-hooks/exhaustive-deps
37 + }, [selectedElementIndex]);
38
39 return (
21 - <div className={styles.OwnerStack}>
40 + <div className={styles.ElementsDropdown}>
41 <Button
23 - className={styles.IconButton}
24 - onClick={resetOwnerStack}
25 - title="Back to tree view"
42 + className={classNames(styles.IconButton, {
43 + [styles.DropdownButtonActive]: isDropdownVisible,
44 + })}
45 + onClick={handleClick}
46 + title="Open elements dropdown"
47 >
27 - <ButtonIcon type="close" />
48 + <ButtonIcon type="colon" />
49 </Button>
29 - <div className={styles.VRule} />
30 - {elements}
50 + {isDropdownVisible && <div className={styles.Dropdown}>{children}</div>}
51 </div>
52 );
53 }
54
35 -type Props = {
55 +type ElementsBarProps = {
56 + elements: Array<any>,
57 + showSelectedOnly: boolean,
58 +};
59 +const ElementsBar = forwardRef(
60 + (
61 + { elements, showSelectedOnly }: ElementsBarProps,
62 + ref: Object
63 + ) => {
64 + return (
65 + <div
66 + className={classNames(styles.ElementsBar, {
67 + [styles.ElementsBarSelectedOnly]: showSelectedOnly,
68 + })}
69 + ref={ref}
70 + >
71 + {elements}
72 + </div>
73 + );
74 + }
75 +);
76 +
77 +type ElementViewProps = {
78 id: number,
79 index: number,
80 };
39 -
40 -function ElementView({ id, index }: Props) {
81 +function ElementView({ id, index }: ElementViewProps) {
82 const { ownerStackIndex, selectOwner } = useContext(TreeContext);
83 const store = useContext(StoreContext);
84 const { displayName } = ((store.getElementByID(id): any): Element);
@@ -59,3 +100,101 @@ function ElementView({ id, index }: Props) {
100 </button>
101 );
102 }
103 +
104 +export default function OwnerStack() {
105 + const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext(
106 + TreeContext
107 + );
108 + const [isElementsBarOverflowing, setIsElementsBarOverflowing] = useState(
109 + false
110 + );
111 + const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
112 + const elementsBarRef = createRef<HTMLDivElement | null>();
113 + const elements = ownerStack.map((id, index) => (
114 + <ElementView key={id} id={id} index={index} />
115 + ));
116 +
117 + useEffect(() => {
118 + if (elementsBarRef.current === null) {
119 + return () => {};
120 + }
121 + const elements = Array.from(elementsBarRef.current.children);
122 + const elementsTotalWidth = elements.reduce((acc, el) => {
123 + const { offsetWidth } = el;
124 + const { marginRight } = getElementDimensions(el);
125 + return acc + (offsetWidth + marginRight);
126 + }, 0);
127 +
128 + setElementsTotalWidth(elementsTotalWidth);
129 + // eslint-disable-next-line react-hooks/exhaustive-deps
130 + }, [ownerStackIndex, elementsBarRef]);
131 +
132 + useElementsBarOverflowing(
133 + elementsBarRef,
134 + elementsTotalWidth,
135 + isElementsBarOverflowing => {
136 + setIsElementsBarOverflowing(isElementsBarOverflowing);
137 + }
138 + );
139 +
140 + return (
141 + <div className={styles.OwnerStack}>
142 + <Button
143 + className={styles.IconButton}
144 + onClick={resetOwnerStack}
145 + title="Back to tree view"
146 + >
147 + <ButtonIcon type="close" />
148 + </Button>
149 + {isElementsBarOverflowing && (
150 + <ElementsDropdown selectedElementIndex={ownerStackIndex}>
151 + {elements}
152 + </ElementsDropdown>
153 + )}
154 + <div className={styles.VRule} />
155 + <ElementsBar
156 + elements={elements}
157 + showSelectedOnly={isElementsBarOverflowing}
158 + ref={elementsBarRef}
159 + />
160 + </div>
161 + );
162 +}
163 +
164 +function useElementsBarOverflowing(
165 + elementsBarRef: Object,
166 + elementsTotalWidth: number,
167 + callback: Function
168 +) {
169 + const isElementsBarOverflowing = useCallback(() => {
170 + if (elementsBarRef.current !== null) {
171 + const elementsBarWidth = elementsBarRef.current.clientWidth;
172 + return elementsBarWidth <= elementsTotalWidth;
173 + }
174 + return false;
175 + }, [elementsBarRef, elementsTotalWidth]);
176 +
177 + useEffect(() => {
178 + let timeoutID = null;
179 + const handleResize = () => {
180 + callback(isElementsBarOverflowing());
181 + };
182 + const debounceHandleResize = () => {
183 + clearTimeout(((timeoutID: any): TimeoutID));
184 + timeoutID = setTimeout(handleResize, 100);
185 + };
186 +
187 + handleResize();
188 + // It's important to listen to the ownerDocument.defaultView to support the browser extension.
189 + // Here we use portals to render individual tabs (e.g. Profiler),
190 + // and the root document might belong to a different window.
191 + const ownerWindow = elementsBarRef.current.ownerDocument.defaultView;
192 + ownerWindow.addEventListener('resize', debounceHandleResize);
193 + return () => {
194 + ownerWindow.removeEventListener('resize', debounceHandleResize);
195 + if (timeoutID !== null) {
196 + clearTimeout(timeoutID);
197 + }
198 + };
199 + }, [elementsBarRef, isElementsBarOverflowing, callback]);
200 +}
src/utils.js
+18
@@ -63,3 +63,21 @@ export function utfEncodeString(string: string): Uint32Array {
63 function toCodePoint(string: string) {
64 return string.codePointAt(0);
65 }
66 +
67 +export function getElementDimensions(domElement: Element) {
68 + const calculatedStyle = window.getComputedStyle(domElement);
69 + return {
70 + borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0],
71 + borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0],
72 + borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0],
73 + borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0],
74 + marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0],
75 + marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0],
76 + marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0],
77 + marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0],
78 + paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0],
79 + paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0],
80 + paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0],
81 + paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0],
82 + };
83 +}