mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added selected and hovered guidelines to Components tree
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user