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
+}