Added selected and hovered guidelines to Components tree

This commit is contained in:
Brian Vaughn
2019-05-30 08:22:06 -07:00
parent d5be489d5b
commit ec421355bd
9 changed files with 265 additions and 57 deletions
+15 -12
View File
@@ -3,6 +3,7 @@
import React, { Suspense } from 'react';
import Tree from './Tree';
import SelectedElement from './SelectedElement';
import { HoveredElementContextController } from './HoveredElementContext';
import { InspectedElementContextController } from './InspectedElementContext';
import { OwnersListContextController } from './OwnersListContext';
import portaledContent from '../portaledContent';
@@ -14,19 +15,21 @@ function Components(_: {||}) {
// TODO Flex wrappers below should be user resizable.
return (
<OwnersListContextController>
<InspectedElementContextController>
<div className={styles.Components}>
<div className={styles.TreeWrapper}>
<Tree />
<HoveredElementContextController>
<InspectedElementContextController>
<div className={styles.Components}>
<div className={styles.TreeWrapper}>
<Tree />
</div>
<div className={styles.SelectedElementWrapper}>
<Suspense fallback={<Loading />}>
<SelectedElement />
</Suspense>
</div>
<ModalDialog />
</div>
<div className={styles.SelectedElementWrapper}>
<Suspense fallback={<Loading />}>
<SelectedElement />
</Suspense>
</div>
<ModalDialog />
</div>
</InspectedElementContextController>
</InspectedElementContextController>
</HoveredElementContextController>
</OwnersListContextController>
);
}
+4 -1
View File
@@ -19,6 +19,7 @@ import Store from 'src/devtools/store';
import ButtonIcon from '../ButtonIcon';
import { createRegExp } from '../utils';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { HoveredElementSetIDContext } from './HoveredElementContext';
import { StoreContext } from '../context';
import type { ItemData } from './Tree';
@@ -38,6 +39,7 @@ export default function ElementView({ data, index, style }: Props) {
TreeStateContext
);
const dispatch = useContext(TreeDispatcherContext);
const setHoveredElementID = useContext(HoveredElementSetIDContext);
const element =
ownerFlatTree !== null
@@ -119,9 +121,10 @@ export default function ElementView({ data, index, style }: Props) {
const handleMouseEnter = useCallback(() => {
setIsHovered(true);
if (id !== null) {
setHoveredElementID(id);
onElementMouseEnter(id);
}
}, [onElementMouseEnter, id]);
}, [id, onElementMouseEnter, setHoveredElementID]);
const handleMouseLeave = useCallback(() => {
setIsHovered(false);
@@ -0,0 +1,18 @@
.GuidelineHovered,
.GuidelineSelectedActive,
.GuidelineSelectedInactive {
width: 1px;
z-index: 2;
}
.GuidelineHovered {
background-color: var(--color-guideline-hovered);
}
.GuidelineSelectedActive {
background-color: var(--color-background-selected);
}
.GuidelineSelectedInactive {
background-color: var(--color-background-inactive);
}
+126
View File
@@ -0,0 +1,126 @@
// @flow
import React, { Fragment, useContext, useMemo } from 'react';
import { HoveredElementIDContext } from './HoveredElementContext';
import { TreeStateContext } from './TreeContext';
import TreeFocusedContext from './TreeFocusedContext';
import { SettingsContext } from '../Settings/SettingsContext';
import { StoreContext } from '../context';
import { useSubscription } from '../hooks';
import Store from '../../store';
import styles from './Guidelines.css';
export default function Guidelines(_: {||}) {
const hoveredElementID = useContext(HoveredElementIDContext);
const { selectedElementID } = useContext(TreeStateContext);
const treeFocused = useContext(TreeFocusedContext);
return (
<Fragment>
{hoveredElementID !== selectedElementID && (
<Guideline
className={styles.GuidelineHovered}
elementID={hoveredElementID}
/>
)}
<Guideline
className={
treeFocused
? styles.GuidelineSelectedActive
: styles.GuidelineSelectedInactive
}
elementID={selectedElementID}
/>
</Fragment>
);
}
type Data = {|
depth: number,
startIndex: number,
stopIndex: number,
|};
type Props = {|
className: string,
elementID: number | null,
|};
function Guideline({ className, elementID }: Props) {
const store = useContext(StoreContext);
const { lineHeight } = useContext(SettingsContext);
const subscription = useMemo(
() => ({
getCurrentValue: () => {
if (elementID === null) {
return null;
}
const element = store.getElementByID(elementID);
if (
element === null ||
element.isCollapsed ||
element.children.length === 0
) {
return null;
}
const startIndex = store.getIndexOfElementID(element.children[0]);
if (startIndex === null) {
return null;
}
let stopIndex = null;
let current = element;
while (current !== null) {
if (current.isCollapsed || current.children.length === 0) {
// We've found the last/deepest descendant.
stopIndex = store.getIndexOfElementID(current.id);
current = null;
} else {
const lastChildID = current.children[current.children.length - 1];
current = store.getElementByID(lastChildID);
}
}
if (stopIndex === null) {
return null;
}
return {
depth: element.depth,
startIndex,
stopIndex,
};
},
subscribe: (callback: Function) => {
store.addListener('mutated', callback);
return () => {
store.removeListener('mutated', callback);
};
},
}),
[elementID, store]
);
const data = useSubscription<Data | null, Store>(subscription);
if (data === null) {
return null;
}
const { depth, startIndex, stopIndex } = data;
return (
<div
className={className}
style={{
position: 'absolute',
top: `${startIndex * lineHeight}px`,
left: `${depth * 0.75 + 0.75}rem`,
height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
}}
/>
);
}
@@ -0,0 +1,34 @@
// @flow
import React, { createContext, useState } from 'react';
type ID = number | null;
type SetID = (id: ID) => void;
const HoveredElementIDContext = createContext<ID>(null);
HoveredElementIDContext.displayName = 'HoveredElementIDContext';
const HoveredElementSetIDContext = createContext<SetID>(((null: any): SetID));
HoveredElementSetIDContext.displayName = 'HoveredElementSetIDContext';
type Props = {|
children: React$Node,
|};
function HoveredElementContextController({ children }: Props) {
const [hoveredElementID, setHoveredElementID] = useState<ID>(null);
return (
<HoveredElementIDContext.Provider value={hoveredElementID}>
<HoveredElementSetIDContext.Provider value={setHoveredElementID}>
{children}
</HoveredElementSetIDContext.Provider>
</HoveredElementIDContext.Provider>
);
}
export {
HoveredElementIDContext,
HoveredElementSetIDContext,
HoveredElementContextController,
};
+58 -44
View File
@@ -22,6 +22,9 @@ import SearchInput from './SearchInput';
import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
import ComponentFiltersModal from './ComponentFiltersModal';
import { HoveredElementSetIDContext } from './HoveredElementContext';
import Guidelines from './Guidelines';
import TreeFocusedContext from './TreeFocusedContext';
import styles from './Tree.css';
@@ -273,54 +276,57 @@ export default function Tree(props: Props) {
);
return (
<ComponentFiltersModalContextController>
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
<InspectHostNodesToggle />
<div className={styles.VRule} />
<Suspense fallback={<Loading />}>
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
</Suspense>
<div className={styles.VRule} />
<ToggleComponentFiltersModalButton />
<TreeFocusedContext.Provider value={treeFocused}>
<ComponentFiltersModalContextController>
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
<InspectHostNodesToggle />
<div className={styles.VRule} />
<Suspense fallback={<Loading />}>
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
</Suspense>
<div className={styles.VRule} />
<ToggleComponentFiltersModalButton />
</div>
<div
className={styles.AutoSizerWrapper}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyPress={handleKeyPress}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={focusTargetRef}
tabIndex={0}
>
<AutoSizer>
{({ height, width }) => (
// $FlowFixMe https://github.com/facebook/flow/issues/7341
<FixedSizeList
className={styles.List}
height={height}
innerElementType={InnerElementType}
itemCount={numElements}
itemData={itemData}
itemSize={lineHeight}
overscanCount={3}
ref={listRef}
width={width}
>
{ElementView}
</FixedSizeList>
)}
</AutoSizer>
</div>
<ComponentFiltersModal />
</div>
<div
className={styles.AutoSizerWrapper}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyPress={handleKeyPress}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={focusTargetRef}
tabIndex={0}
>
<AutoSizer>
{({ height, width }) => (
// $FlowFixMe https://github.com/facebook/flow/issues/7341
<FixedSizeList
className={styles.List}
height={height}
innerElementType={InnerElementType}
itemCount={numElements}
itemData={itemData}
itemSize={lineHeight}
overscanCount={3}
ref={listRef}
width={width}
>
{ElementView}
</FixedSizeList>
)}
</AutoSizer>
</div>
<ComponentFiltersModal />
</div>
</ComponentFiltersModalContextController>
</ComponentFiltersModalContextController>
</TreeFocusedContext.Provider>
);
}
function InnerElementType({ style, ...rest }) {
function InnerElementType({ children, style, ...rest }) {
const { ownerID } = useContext(TreeStateContext);
const setHoveredElementID = useContext(HoveredElementSetIDContext);
// The list may need to scroll horizontally due to deeply nested elements.
// We don't know the maximum scroll width up front, because we're windowing.
@@ -355,6 +361,10 @@ function InnerElementType({ style, ...rest }) {
setMinWidth(null);
}
const handleMouseLeave = useCallback(() => {
setHoveredElementID(null);
}, [setHoveredElementID]);
// This style override enables the background color to fill the full visible width,
// when combined with the CSS tweaks in Element.
// A lot of options were considered; this seemed the one that requires the least code.
@@ -362,6 +372,7 @@ function InnerElementType({ style, ...rest }) {
return (
<div
className={styles.InnerElementType}
onMouseLeave={handleMouseLeave}
style={{
...style,
display: 'inline-block',
@@ -370,7 +381,10 @@ function InnerElementType({ style, ...rest }) {
}}
ref={divRef}
{...rest}
/>
>
<Guidelines />
{children}
</div>
);
}
@@ -0,0 +1,7 @@
// @flow
import { createContext } from 'react';
const TreeFocusedContext = createContext<boolean>(false);
export default TreeFocusedContext;
@@ -261,6 +261,7 @@ function updateThemeVariables(
updateStyleHelper(theme, 'color-dimmer', documentElements);
updateStyleHelper(theme, 'color-dimmest', documentElements);
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
updateStyleHelper(theme, 'color-guideline-hovered', documentElements);
updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements);
updateStyleHelper(
theme,
+2
View File
@@ -42,6 +42,7 @@
--light-color-dimmer: #cfd1d5;
--light-color-dimmest: #eff0f1;
--light-color-expand-collapse-toggle: #777d88;
--light-color-guideline-hovered: rgba(0, 0, 0, 0.1);
--light-color-jsx-arrow-brackets: #333333;
--light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
--light-color-modal-background: rgba(255, 255, 255, 0.75);
@@ -99,6 +100,7 @@
--dark-color-dimmer: #777d88;
--dark-color-dimmest: #4f5766;
--dark-color-expand-collapse-toggle: #8f949d;
--dark-color-guideline-hovered: rgba(255, 255, 255, 0.1);
--dark-color-jsx-arrow-brackets: #777d88;
--dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
--dark-color-modal-background: rgba(0, 0, 0, 0.75);