diff --git a/src/devtools/views/Components/Components.js b/src/devtools/views/Components/Components.js index 3ed317a101..395087a21d 100644 --- a/src/devtools/views/Components/Components.js +++ b/src/devtools/views/Components/Components.js @@ -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 ( - -
-
- + + +
+
+ +
+
+ }> + + +
+
-
- }> - - -
- -
- + + ); } diff --git a/src/devtools/views/Components/Element.js b/src/devtools/views/Components/Element.js index 0f3bb34a7a..020de1d5c8 100644 --- a/src/devtools/views/Components/Element.js +++ b/src/devtools/views/Components/Element.js @@ -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); diff --git a/src/devtools/views/Components/Guidelines.css b/src/devtools/views/Components/Guidelines.css new file mode 100644 index 0000000000..7ea56a3f50 --- /dev/null +++ b/src/devtools/views/Components/Guidelines.css @@ -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); +} diff --git a/src/devtools/views/Components/Guidelines.js b/src/devtools/views/Components/Guidelines.js new file mode 100644 index 0000000000..f8397fd654 --- /dev/null +++ b/src/devtools/views/Components/Guidelines.js @@ -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 ( + + {hoveredElementID !== selectedElementID && ( + + )} + + + ); +} + +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(subscription); + + if (data === null) { + return null; + } + + const { depth, startIndex, stopIndex } = data; + + return ( +
+ ); +} diff --git a/src/devtools/views/Components/HoveredElementContext.js b/src/devtools/views/Components/HoveredElementContext.js new file mode 100644 index 0000000000..161fd1f620 --- /dev/null +++ b/src/devtools/views/Components/HoveredElementContext.js @@ -0,0 +1,34 @@ +// @flow + +import React, { createContext, useState } from 'react'; + +type ID = number | null; +type SetID = (id: ID) => void; + +const HoveredElementIDContext = createContext(null); +HoveredElementIDContext.displayName = 'HoveredElementIDContext'; + +const HoveredElementSetIDContext = createContext(((null: any): SetID)); +HoveredElementSetIDContext.displayName = 'HoveredElementSetIDContext'; + +type Props = {| + children: React$Node, +|}; + +function HoveredElementContextController({ children }: Props) { + const [hoveredElementID, setHoveredElementID] = useState(null); + + return ( + + + {children} + + + ); +} + +export { + HoveredElementIDContext, + HoveredElementSetIDContext, + HoveredElementContextController, +}; diff --git a/src/devtools/views/Components/Tree.js b/src/devtools/views/Components/Tree.js index 4687c79939..0a6ac0fa4b 100644 --- a/src/devtools/views/Components/Tree.js +++ b/src/devtools/views/Components/Tree.js @@ -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 ( - -
-
- -
- }> - {ownerID !== null ? : } - -
- + + +
+
+ +
+ }> + {ownerID !== null ? : } + +
+ +
+
+ + {({ height, width }) => ( + // $FlowFixMe https://github.com/facebook/flow/issues/7341 + + {ElementView} + + )} + +
+
-
- - {({ height, width }) => ( - // $FlowFixMe https://github.com/facebook/flow/issues/7341 - - {ElementView} - - )} - -
- -
- + + ); } -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 (
+ > + + {children} +
); } diff --git a/src/devtools/views/Components/TreeFocusedContext.js b/src/devtools/views/Components/TreeFocusedContext.js new file mode 100644 index 0000000000..7851db1316 --- /dev/null +++ b/src/devtools/views/Components/TreeFocusedContext.js @@ -0,0 +1,7 @@ +// @flow + +import { createContext } from 'react'; + +const TreeFocusedContext = createContext(false); + +export default TreeFocusedContext; diff --git a/src/devtools/views/Settings/SettingsContext.js b/src/devtools/views/Settings/SettingsContext.js index 17194aaa03..fa2be0f5be 100644 --- a/src/devtools/views/Settings/SettingsContext.js +++ b/src/devtools/views/Settings/SettingsContext.js @@ -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, diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css index 5dba64cc20..f0ec9588a0 100644 --- a/src/devtools/views/root.css +++ b/src/devtools/views/root.css @@ -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);