mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added properties panel and selected element context
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
#target {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
#devtools {
|
||||
display: flex;
|
||||
|
||||
@@ -105,14 +105,16 @@ export default class Store extends EventEmitter {
|
||||
return ((currentElement: any): Element);
|
||||
}
|
||||
|
||||
getElementByID(id: number): Element {
|
||||
getElementByID(id: number): Element | null {
|
||||
const element = this._idToElement.get(id);
|
||||
|
||||
if (element == null) {
|
||||
throw Error(`No element found with id "${id}`);
|
||||
console.warn(`No element found with id "${id}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((element: any): Element);
|
||||
return element;
|
||||
}
|
||||
|
||||
onBridgeOperations = (operations: Uint32Array) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.Icon {
|
||||
.ButtonIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0 0.25rem;
|
||||
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import styles from './ButtonIcon.css';
|
||||
|
||||
type Props = {|
|
||||
type: 'search' | 'view-dom' | 'view-source',
|
||||
|};
|
||||
|
||||
export default function ButtonIcon({ type }: Props) {
|
||||
let pathData = null;
|
||||
switch (type) {
|
||||
case 'search':
|
||||
pathData = PATH_SEARCH;
|
||||
break;
|
||||
case 'view-dom':
|
||||
pathData = PATH_VIEW_DOM;
|
||||
break;
|
||||
case 'view-source':
|
||||
pathData = PATH_VIEW_SOURCE;
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unsupported type "${type}" specified for ButtonIcon`);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.ButtonIcon}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path fill="currentColor" d={pathData} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const PATH_SEARCH = `
|
||||
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
|
||||
`;
|
||||
|
||||
const PATH_VIEW_DOM = `
|
||||
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
|
||||
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
|
||||
3-1.34 3-3-1.34-3-3-3z
|
||||
`;
|
||||
|
||||
const PATH_VIEW_SOURCE = `
|
||||
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
|
||||
`;
|
||||
@@ -1,14 +1,26 @@
|
||||
.Element {
|
||||
.Element,
|
||||
.SelectedElement {
|
||||
width: 100%;
|
||||
border-radius: 0.25em;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
line-height: 20px;
|
||||
}
|
||||
.Element:hover {
|
||||
background-color: var(--color-state03);
|
||||
}
|
||||
|
||||
.SelectedElement {
|
||||
background-color: var(--color-state00);
|
||||
|
||||
/* Invert colors */
|
||||
--color-component: var(--color-base00);
|
||||
--color-tree-tag: rgba(255, 255, 255, 0.8);
|
||||
--color-tree-attr-name: #b3e5fc;
|
||||
--color-tree-attr-value: #fff;
|
||||
}
|
||||
|
||||
.ArrowClosed,
|
||||
.ArrowOpen {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useContext } from 'react';
|
||||
import React, { Fragment, useCallback, useContext } from 'react';
|
||||
import { TreeContext } from './context';
|
||||
import { SelectedElementContext } from './SelectedElementContext';
|
||||
|
||||
import styles from './Element.css';
|
||||
|
||||
@@ -22,13 +23,24 @@ export default function Element({ index, style }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { children, depth, displayName, key } = element;
|
||||
const { children, depth, displayName, id, key } = element;
|
||||
|
||||
// TODO: Add state for toggling element open/close
|
||||
const selectedElement = useContext(SelectedElementContext);
|
||||
const handleClick = useCallback(
|
||||
({ metaKey }) => {
|
||||
selectedElement.id = metaKey ? null : id;
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
// TODO: Add click and key handlers for toggling element open/close state.
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.Element}
|
||||
className={
|
||||
selectedElement.id === id ? styles.SelectedElement : styles.Element
|
||||
}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
...style,
|
||||
paddingLeft: `${1 + depth}rem`,
|
||||
|
||||
@@ -2,49 +2,24 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.SearchRow {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid var(--color-base02);
|
||||
padding: 0.5rem;
|
||||
.TreeWrapper {
|
||||
flex: 0 0 65%;
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
border: none;
|
||||
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");
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
background-size: 16px 16px;
|
||||
padding-left: 1.5rem;
|
||||
.SelectedElementWrapper {
|
||||
flex: 0 0 35%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
border: none;
|
||||
background: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-state00);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.IconButton:hover {
|
||||
color: var(--color-special02);
|
||||
}
|
||||
|
||||
.Tree {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
padding: 0.25rem;
|
||||
@media screen and (max-width: 600px) {
|
||||
.Elements {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import Store from '../store';
|
||||
import Tree from './Tree';
|
||||
import { StoreContext, TreeContext } from './context';
|
||||
import SearchIcon from './SearchIcon';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import { SelectedElementController } from './SelectedElementContext';
|
||||
import styles from './Elements.css';
|
||||
|
||||
import './root.css';
|
||||
@@ -38,23 +39,20 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
|
||||
return () => store.removeListener('rootCommitted', handler);
|
||||
}, [store]);
|
||||
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContext.Provider value={treeContext}>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.SearchRow}>
|
||||
<input
|
||||
className={styles.SearchInput}
|
||||
placeholder="Search (text or /regex/)"
|
||||
/>
|
||||
<button className={styles.IconButton}>
|
||||
<SearchIcon /> Select
|
||||
</button>
|
||||
<SelectedElementController>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.Tree}>
|
||||
<Tree />
|
||||
</div>
|
||||
</div>
|
||||
</SelectedElementController>
|
||||
</TreeContext.Provider>
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import styles from './SearchIcon.css';
|
||||
|
||||
export default function SearchIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.Icon}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.SelectedElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--color-base02);
|
||||
border-top: 1px solid var(--color-base02);
|
||||
}
|
||||
|
||||
.TitleRow {
|
||||
flex: 0 0 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid var(--color-base02);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
flex: 0 0 auto;
|
||||
border: none;
|
||||
background: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-state00);
|
||||
}
|
||||
.IconButton:hover {
|
||||
color: var(--color-special02);
|
||||
}
|
||||
|
||||
.SelectedComponentName {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Component {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-component);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.Component:before {
|
||||
white-space: nowrap;
|
||||
content: '<';
|
||||
color: var(--color-tree-tag);
|
||||
}
|
||||
.Component:after {
|
||||
white-space: nowrap;
|
||||
content: '>';
|
||||
color: var(--color-tree-tag);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { SelectedElementContext } from './SelectedElementContext';
|
||||
import { StoreContext } from './context';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import styles from './SelectedElement.css';
|
||||
|
||||
export type Props = {||};
|
||||
|
||||
export default function SelectedElement(props: Props) {
|
||||
const { id } = useContext(SelectedElementContext);
|
||||
const store = useContext(StoreContext);
|
||||
const element = id !== null ? store.getElementByID(id) : null;
|
||||
|
||||
// TODO Show/hide source button based on whether debug "source" is available.
|
||||
|
||||
if (element === null) {
|
||||
return (
|
||||
<div className={styles.SelectedElement}>
|
||||
<div className={styles.TitleRow} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.SelectedElement}>
|
||||
<div className={styles.TitleRow}>
|
||||
<div className={styles.SelectedComponentName}>
|
||||
<div className={styles.Component} title={element.displayName}>
|
||||
{element.displayName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
title="Highlight this element in the page"
|
||||
>
|
||||
<ButtonIcon type="view-dom" /> DOM
|
||||
</button>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
title="View source for this element"
|
||||
>
|
||||
<ButtonIcon type="view-source" /> Source
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
|
||||
import React, { createContext, useMemo, useState } from 'react';
|
||||
|
||||
type SelectedElementContextValue = {|
|
||||
id: number | null,
|
||||
|};
|
||||
|
||||
const SelectedElementContext = createContext<SelectedElementContextValue>(
|
||||
((null: any): SelectedElementContextValue)
|
||||
);
|
||||
// $FlowFixMe displayName is a valid attribute of React$Context
|
||||
SelectedElementContext.displayName = 'SelectedElementContext';
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
// TODO Remove this wrapper element once global Context.write API exists.
|
||||
function SelectedElementController({ children }: Props) {
|
||||
const [id, setID] = useState<number | null>(null);
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
get id() {
|
||||
return id;
|
||||
},
|
||||
set id(id: number | null) {
|
||||
setID(id);
|
||||
},
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectedElementContext.Provider value={value}>
|
||||
{children}
|
||||
</SelectedElementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { SelectedElementContext, SelectedElementController };
|
||||
@@ -1,4 +1,54 @@
|
||||
.Tree {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--color-base02);
|
||||
}
|
||||
|
||||
.SearchRow {
|
||||
flex: 0 0 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid var(--color-base02);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
border: none;
|
||||
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");
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
background-size: 16px 16px;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
border: none;
|
||||
background: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-state00);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.IconButton:hover {
|
||||
color: var(--color-special02);
|
||||
}
|
||||
|
||||
.AutoSizerWrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.List {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-tree-compact);
|
||||
line-height: var(--line-height-tree-compact);
|
||||
|
||||
+32
-13
@@ -4,6 +4,7 @@ import React, { useContext } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import Element from './Element';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import { TreeContext } from './context';
|
||||
|
||||
import styles from './Tree.css';
|
||||
@@ -13,20 +14,38 @@ type Props = {||};
|
||||
export default function Tree(props: Props) {
|
||||
const treeContext = useContext(TreeContext);
|
||||
|
||||
// TODO Add key handlers for selecting previous/next element.
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
className={styles.Tree}
|
||||
height={height}
|
||||
itemCount={treeContext.size}
|
||||
itemData={treeContext}
|
||||
itemSize={20}
|
||||
width={width}
|
||||
<div className={styles.Tree}>
|
||||
<div className={styles.SearchRow}>
|
||||
<input
|
||||
className={styles.SearchInput}
|
||||
placeholder="Search (text or /regex/)"
|
||||
/>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
title="Select an element in the page to inspect it"
|
||||
>
|
||||
{Element}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<ButtonIcon type="search" /> Select
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.AutoSizerWrapper}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
itemCount={treeContext.size}
|
||||
itemData={treeContext}
|
||||
itemSize={20}
|
||||
width={width}
|
||||
>
|
||||
{Element}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
/* GitHub.com system fonts */
|
||||
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
|
||||
Courier, monospace;
|
||||
--font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
|
||||
Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
--font-size-tree-compact: 12px;
|
||||
--font-size-tree-normal: 14px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user