@samitouri / QOS-React-2 / commits / 406df2a617

Added properties panel and selected element context

Brian Vaughn committed Feb 2, 2019 at 13:57 UTC 406df2a617c6a9565b1dc053129e07007ea94bae
15 files changed +343 -96
shells/dev/index.html
-1
@@ -8,7 +8,6 @@
8 #target {
9 flex: 1;
10 border: none;
11 - border-bottom: 1px solid #ccc;
11 }
12 #devtools {
13 display: flex;
src/devtools/store.js
+5 -3
@@ -105,14 +105,16 @@ export default class Store extends EventEmitter {
105 return ((currentElement: any): Element);
106 }
107
108 - getElementByID(id: number): Element {
108 + getElementByID(id: number): Element | null {
109 const element = this._idToElement.get(id);
110
111 if (element == null) {
112 - throw Error(`No element found with id "${id}`);
112 + console.warn(`No element found with id "${id}`);
113 +
114 + return null;
115 }
116
115 - return ((element: any): Element);
117 + return element;
118 }
119
120 onBridgeOperations = (operations: Uint32Array) => {
src/devtools/views/ButtonIcon.css renamed
+1 -1
@@ -1,4 +1,4 @@
1 -.Icon {
1 +.ButtonIcon {
2 width: 1rem;
3 height: 1rem;
4 margin: 0 0.25rem;
src/devtools/views/ButtonIcon.js new
+55
@@ -0,0 +1,55 @@
1 +// @flow
2 +
3 +import React from 'react';
4 +import styles from './ButtonIcon.css';
5 +
6 +type Props = {|
7 + type: 'search' | 'view-dom' | 'view-source',
8 +|};
9 +
10 +export default function ButtonIcon({ type }: Props) {
11 + let pathData = null;
12 + switch (type) {
13 + case 'search':
14 + pathData = PATH_SEARCH;
15 + break;
16 + case 'view-dom':
17 + pathData = PATH_VIEW_DOM;
18 + break;
19 + case 'view-source':
20 + pathData = PATH_VIEW_SOURCE;
21 + break;
22 + default:
23 + console.warn(`Unsupported type "${type}" specified for ButtonIcon`);
24 + break;
25 + }
26 +
27 + return (
28 + <svg
29 + xmlns="http://www.w3.org/2000/svg"
30 + className={styles.ButtonIcon}
31 + width="24"
32 + height="24"
33 + viewBox="0 0 24 24"
34 + >
35 + <path d="M0 0h24v24H0z" fill="none" />
36 + <path fill="currentColor" d={pathData} />
37 + </svg>
38 + );
39 +}
40 +
41 +const PATH_SEARCH = `
42 + M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46
43 + 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87
44 + 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z
45 +`;
46 +
47 +const PATH_VIEW_DOM = `
48 + M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
49 + 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
50 + 3-1.34 3-3-1.34-3-3-3z
51 +`;
52 +
53 +const PATH_VIEW_SOURCE = `
54 + M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
55 + `;
src/devtools/views/Element.css
+13 -1
@@ -1,14 +1,26 @@
1 -.Element {
1 +.Element,
2 +.SelectedElement {
3 width: 100%;
4 border-radius: 0.25em;
5 cursor: pointer;
6 position: relative;
7 white-space: nowrap;
8 + line-height: 20px;
9 }
10 .Element:hover {
11 background-color: var(--color-state03);
12 }
13
14 +.SelectedElement {
15 + background-color: var(--color-state00);
16 +
17 + /* Invert colors */
18 + --color-component: var(--color-base00);
19 + --color-tree-tag: rgba(255, 255, 255, 0.8);
20 + --color-tree-attr-name: #b3e5fc;
21 + --color-tree-attr-value: #fff;
22 +}
23 +
24 .ArrowClosed,
25 .ArrowOpen {
26 position: absolute;
src/devtools/views/Element.js
+16 -4
@@ -1,7 +1,8 @@
1 // @flow
2
3 -import React, { Fragment, useContext } from 'react';
3 +import React, { Fragment, useCallback, useContext } from 'react';
4 import { TreeContext } from './context';
5 +import { SelectedElementContext } from './SelectedElementContext';
6
7 import styles from './Element.css';
8
@@ -22,13 +23,24 @@ export default function Element({ index, style }: Props) {
23 return null;
24 }
25
25 - const { children, depth, displayName, key } = element;
26 + const { children, depth, displayName, id, key } = element;
27
27 - // TODO: Add state for toggling element open/close
28 + const selectedElement = useContext(SelectedElementContext);
29 + const handleClick = useCallback(
30 + ({ metaKey }) => {
31 + selectedElement.id = metaKey ? null : id;
32 + },
33 + [id]
34 + );
35 +
36 + // TODO: Add click and key handlers for toggling element open/close state.
37
38 return (
39 <div
31 - className={styles.Element}
40 + className={
41 + selectedElement.id === id ? styles.SelectedElement : styles.Element
42 + }
43 + onClick={handleClick}
44 style={{
45 ...style,
46 paddingLeft: `${1 + depth}rem`,
src/devtools/views/Elements.css
+12 -37
@@ -2,49 +2,24 @@
2 width: 100%;
3 height: 100%;
4 display: flex;
5 - flex-direction: column;
5 + flex-direction: row;
6
7 + font-family: var(--font-family-sans);
8 -webkit-font-smoothing: antialiased;
9 }
10
10 -.SearchRow {
11 - flex: 0 0 auto;
12 - display: flex;
13 - align-items: center;
14 - font-size: 16px;
15 - border-bottom: 1px solid var(--color-base02);
16 - padding: 0.5rem;
11 +.TreeWrapper {
12 + flex: 0 0 65%;
13 }
14
19 -.SearchInput {
20 - font-size: 1rem;
21 - flex: 1;
22 - outline: none;
23 - border: none;
24 - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
25 - background-repeat: no-repeat;
26 - background-position: left center;
27 - background-size: 16px 16px;
28 - padding-left: 1.5rem;
15 +.SelectedElementWrapper {
16 + flex: 0 0 35%;
17 + overflow-x: hidden;
18 + overflow-y: auto;
19 }
20
31 -.IconButton {
32 - border: none;
33 - background: none;
34 - display: inline-flex;
35 - align-items: center;
36 - padding: 0.25rem;
37 - cursor: pointer;
38 - color: var(--color-state00);
39 - flex: 0 0 auto;
40 -}
41 -.IconButton:hover {
42 - color: var(--color-special02);
43 -}
44 -
45 -.Tree {
46 - width: 100%;
47 - overflow: auto;
48 - flex: 1 0 auto;
49 - padding: 0.25rem;
21 +@media screen and (max-width: 600px) {
22 + .Elements {
23 + flex-direction: column;
24 + }
25 }
src/devtools/views/Elements.js
+12 -14
@@ -4,7 +4,8 @@ import React, { useLayoutEffect, useMemo, useState } from 'react';
4 import Store from '../store';
5 import Tree from './Tree';
6 import { StoreContext, TreeContext } from './context';
7 -import SearchIcon from './SearchIcon';
7 +import SelectedElement from './SelectedElement';
8 +import { SelectedElementController } from './SelectedElementContext';
9 import styles from './Elements.css';
10
11 import './root.css';
@@ -38,23 +39,20 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
39 return () => store.removeListener('rootCommitted', handler);
40 }, [store]);
41
42 + // TODO Flex wrappers below should be user resizable.
43 return (
44 <StoreContext.Provider value={store}>
45 <TreeContext.Provider value={treeContext}>
44 - <div className={styles.Elements}>
45 - <div className={styles.SearchRow}>
46 - <input
47 - className={styles.SearchInput}
48 - placeholder="Search (text or /regex/)"
49 - />
50 - <button className={styles.IconButton}>
51 - <SearchIcon /> Select
52 - </button>
46 + <SelectedElementController>
47 + <div className={styles.Elements}>
48 + <div className={styles.TreeWrapper}>
49 + <Tree />
50 + </div>
51 + <div className={styles.SelectedElementWrapper}>
52 + <SelectedElement />
53 + </div>
54 </div>
54 - <div className={styles.Tree}>
55 - <Tree />
56 - </div>
57 - </div>
55 + </SelectedElementController>
56 </TreeContext.Provider>
57 </StoreContext.Provider>
58 );
src/devtools/views/SearchIcon.js deleted
-22
@@ -1,22 +0,0 @@
1 -// @flow
2 -
3 -import React from 'react';
4 -import styles from './SearchIcon.css';
5 -
6 -export default function SearchIcon() {
7 - return (
8 - <svg
9 - xmlns="http://www.w3.org/2000/svg"
10 - className={styles.Icon}
11 - width="24"
12 - height="24"
13 - viewBox="0 0 24 24"
14 - >
15 - <path d="M0 0h24v24H0z" fill="none" />
16 - <path
17 - fill="currentColor"
18 - d="M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"
19 - />
20 - </svg>
21 - );
22 -}
src/devtools/views/SelectedElement.css new
+54
@@ -0,0 +1,54 @@
1 +.SelectedElement {
2 + display: flex;
3 + flex-direction: column;
4 + height: 100%;
5 + width: 100%;
6 + border-left: 1px solid var(--color-base02);
7 + border-top: 1px solid var(--color-base02);
8 +}
9 +
10 +.TitleRow {
11 + flex: 0 0 42px;
12 + display: flex;
13 + align-items: center;
14 + font-size: 16px;
15 + border-bottom: 1px solid var(--color-base02);
16 + padding: 0.5rem;
17 +}
18 +
19 +.IconButton {
20 + flex: 0 0 auto;
21 + border: none;
22 + background: none;
23 + display: inline-flex;
24 + align-items: center;
25 + padding: 0.25rem;
26 + cursor: pointer;
27 + color: var(--color-state00);
28 +}
29 +.IconButton:hover {
30 + color: var(--color-special02);
31 +}
32 +
33 +.SelectedComponentName {
34 + flex: 1 1 auto;
35 + overflow: hidden;
36 + text-overflow: ellipsis;
37 +}
38 +
39 +.Component {
40 + overflow: hidden;
41 + text-overflow: ellipsis;
42 + color: var(--color-component);
43 + flex: 1 1 auto;
44 +}
45 +.Component:before {
46 + white-space: nowrap;
47 + content: '<';
48 + color: var(--color-tree-tag);
49 +}
50 +.Component:after {
51 + white-space: nowrap;
52 + content: '>';
53 + color: var(--color-tree-tag);
54 +}
src/devtools/views/SelectedElement.js new
+50
@@ -0,0 +1,50 @@
1 +// @flow
2 +
3 +import React, { useContext } from 'react';
4 +import { SelectedElementContext } from './SelectedElementContext';
5 +import { StoreContext } from './context';
6 +import ButtonIcon from './ButtonIcon';
7 +import styles from './SelectedElement.css';
8 +
9 +export type Props = {||};
10 +
11 +export default function SelectedElement(props: Props) {
12 + const { id } = useContext(SelectedElementContext);
13 + const store = useContext(StoreContext);
14 + const element = id !== null ? store.getElementByID(id) : null;
15 +
16 + // TODO Show/hide source button based on whether debug "source" is available.
17 +
18 + if (element === null) {
19 + return (
20 + <div className={styles.SelectedElement}>
21 + <div className={styles.TitleRow} />
22 + </div>
23 + );
24 + }
25 +
26 + return (
27 + <div className={styles.SelectedElement}>
28 + <div className={styles.TitleRow}>
29 + <div className={styles.SelectedComponentName}>
30 + <div className={styles.Component} title={element.displayName}>
31 + {element.displayName}
32 + </div>
33 + </div>
34 +
35 + <button
36 + className={styles.IconButton}
37 + title="Highlight this element in the page"
38 + >
39 + <ButtonIcon type="view-dom" /> DOM
40 + </button>
41 + <button
42 + className={styles.IconButton}
43 + title="View source for this element"
44 + >
45 + <ButtonIcon type="view-source" /> Source
46 + </button>
47 + </div>
48 + </div>
49 + );
50 +}
src/devtools/views/SelectedElementContext.js new
+41
@@ -0,0 +1,41 @@
1 +// @flow
2 +
3 +import React, { createContext, useMemo, useState } from 'react';
4 +
5 +type SelectedElementContextValue = {|
6 + id: number | null,
7 +|};
8 +
9 +const SelectedElementContext = createContext<SelectedElementContextValue>(
10 + ((null: any): SelectedElementContextValue)
11 +);
12 +// $FlowFixMe displayName is a valid attribute of React$Context
13 +SelectedElementContext.displayName = 'SelectedElementContext';
14 +
15 +type Props = {|
16 + children: React$Node,
17 +|};
18 +
19 +// TODO Remove this wrapper element once global Context.write API exists.
20 +function SelectedElementController({ children }: Props) {
21 + const [id, setID] = useState<number | null>(null);
22 + const value = useMemo(
23 + () => ({
24 + get id() {
25 + return id;
26 + },
27 + set id(id: number | null) {
28 + setID(id);
29 + },
30 + }),
31 + [id]
32 + );
33 +
34 + return (
35 + <SelectedElementContext.Provider value={value}>
36 + {children}
37 + </SelectedElementContext.Provider>
38 + );
39 +}
40 +
41 +export { SelectedElementContext, SelectedElementController };
src/devtools/views/Tree.css
+50
@@ -1,4 +1,54 @@
1 .Tree {
2 + height: 100%;
3 + width: 100%;
4 + display: flex;
5 + flex-direction: column;
6 + border-top: 1px solid var(--color-base02);
7 +}
8 +
9 +.SearchRow {
10 + flex: 0 0 42px;
11 + display: flex;
12 + align-items: center;
13 + font-size: 16px;
14 + border-bottom: 1px solid var(--color-base02);
15 + padding: 0.5rem;
16 +}
17 +
18 +.SearchInput {
19 + font-size: 1rem;
20 + flex: 1;
21 + outline: none;
22 + border: none;
23 + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
24 + background-repeat: no-repeat;
25 + background-position: left center;
26 + background-size: 16px 16px;
27 + padding-left: 1.5rem;
28 +}
29 +
30 +.IconButton {
31 + border: none;
32 + background: none;
33 + display: inline-flex;
34 + align-items: center;
35 + padding: 0.25rem;
36 + cursor: pointer;
37 + color: var(--color-state00);
38 + flex: 0 0 auto;
39 +}
40 +.IconButton:hover {
41 + color: var(--color-special02);
42 +}
43 +
44 +.AutoSizerWrapper {
45 + width: 100%;
46 + overflow: auto;
47 + flex: 1 0 auto;
48 + padding: 0.25rem;
49 +}
50 +
51 +.List {
52 font-family: var(--font-family-monospace);
53 font-size: var(--font-size-tree-compact);
54 line-height: var(--line-height-tree-compact);
src/devtools/views/Tree.js
+32 -13
@@ -4,6 +4,7 @@ import React, { useContext } from 'react';
4 import AutoSizer from 'react-virtualized-auto-sizer';
5 import { FixedSizeList } from 'react-window';
6 import Element from './Element';
7 +import ButtonIcon from './ButtonIcon';
8 import { TreeContext } from './context';
9
10 import styles from './Tree.css';
@@ -13,20 +14,38 @@ type Props = {||};
14 export default function Tree(props: Props) {
15 const treeContext = useContext(TreeContext);
16
17 + // TODO Add key handlers for selecting previous/next element.
18 +
19 return (
17 - <AutoSizer>
18 - {({ height, width }) => (
19 - <FixedSizeList
20 - className={styles.Tree}
21 - height={height}
22 - itemCount={treeContext.size}
23 - itemData={treeContext}
24 - itemSize={20}
25 - width={width}
20 + <div className={styles.Tree}>
21 + <div className={styles.SearchRow}>
22 + <input
23 + className={styles.SearchInput}
24 + placeholder="Search (text or /regex/)"
25 + />
26 + <button
27 + className={styles.IconButton}
28 + title="Select an element in the page to inspect it"
29 >
27 - {Element}
28 - </FixedSizeList>
29 - )}
30 - </AutoSizer>
30 + <ButtonIcon type="search" /> Select
31 + </button>
32 + </div>
33 + <div className={styles.AutoSizerWrapper}>
34 + <AutoSizer>
35 + {({ height, width }) => (
36 + <FixedSizeList
37 + className={styles.List}
38 + height={height}
39 + itemCount={treeContext.size}
40 + itemData={treeContext}
41 + itemSize={20}
42 + width={width}
43 + >
44 + {Element}
45 + </FixedSizeList>
46 + )}
47 + </AutoSizer>
48 + </div>
49 + </div>
50 );
51 }
src/devtools/views/root.css
+2
@@ -33,6 +33,8 @@
33 /* GitHub.com system fonts */
34 --font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
35 Courier, monospace;
36 + --font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
37 + Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
38 --font-size-tree-compact: 12px;
39 --font-size-tree-normal: 14px;
40 }