diff --git a/.flowconfig b/.flowconfig index 35306bd54a..c1209df273 100644 --- a/.flowconfig +++ b/.flowconfig @@ -4,7 +4,6 @@ .*node_modules/archiver-utils .*node_modules/babel.* .*node_modules/browserify-zlib/.* -.*node_modules/classnames.* .*node_modules/gh-pages/.* .*node_modules/invariant/.* .*node_modules/json-loader.* diff --git a/.gitignore b/.gitignore index 514ad873cd..ee4e445f06 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ npm-debug.log yarn-error.log .DS_Store yarn-error.log -.vscode \ No newline at end of file +.vscode +.idea diff --git a/src/backend/views/Overlay.js b/src/backend/views/Overlay.js index 24b0933a43..a54547e525 100644 --- a/src/backend/views/Overlay.js +++ b/src/backend/views/Overlay.js @@ -1,6 +1,7 @@ // @flow import assign from 'object-assign'; +import { getElementDimensions } from '../../utils'; type Rect = { bottom: number, @@ -215,24 +216,6 @@ function findTipPos(dims, win) { return { top, left: dims.left + margin + 'px' }; } -function getElementDimensions(domElement) { - const calculatedStyle = window.getComputedStyle(domElement); - return { - borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0], - borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0], - borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0], - borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0], - marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0], - marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0], - marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0], - marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0], - paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0], - paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0], - paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0], - paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0], - }; -} - // Get the window object for the document that a node belongs to, // or return null if it cannot be found (node not attached to DOM, // etc). diff --git a/src/devtools/views/ButtonIcon.js b/src/devtools/views/ButtonIcon.js index 6d423dca00..7558ddfec8 100644 --- a/src/devtools/views/ButtonIcon.js +++ b/src/devtools/views/ButtonIcon.js @@ -7,6 +7,7 @@ export type IconType = | 'back' | 'cancel' | 'close' + | 'colon' | 'copy' | 'down' | 'export' @@ -39,6 +40,9 @@ export default function ButtonIcon({ type }: Props) { case 'close': pathData = PATH_CLOSE; break; + case 'colon': + pathData = PATH_COLON; + break; case 'copy': pathData = PATH_COPY; break; @@ -117,6 +121,9 @@ const PATH_CANCEL = ` const PATH_CLOSE = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'; +const PATH_COLON = + 'M10,9a2,2 0 1,0 4,0a2,2 0 1,0 -4,0 M10,19a2,2 0 1,0 4,0a2,2 0 1,0 -4,0'; + const PATH_COPY = ` M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3a2 2 0 0 0 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z diff --git a/src/devtools/views/Components/OwnersStack.css b/src/devtools/views/Components/OwnersStack.css index 2f0ebbd2a8..2af09bc30e 100644 --- a/src/devtools/views/Components/OwnersStack.css +++ b/src/devtools/views/Components/OwnersStack.css @@ -2,7 +2,6 @@ flex: 1; display: flex; align-items: center; - overflow-x: auto; } .Component, @@ -12,7 +11,6 @@ color: var(--color-component-name); font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); - white-space: nowrap; border-radius: 0.125rem; border: none; background: none; @@ -38,7 +36,49 @@ outline: none; } +.ElementsBar { + flex: 1 0 auto; +} + +.ElementsBarSelectedOnly { + margin-left: 0.25rem; +} + +.ElementsBarSelectedOnly .Component { + visibility: hidden; +} +.ElementsBarSelectedOnly .FocusedComponent { + float: left; +} + +.ElementsDropdown { + position: relative; +} + +.Dropdown { + z-index: 1; + position: absolute; + top: calc(100% + 5px); + left: 0; + min-height: 200px; + background-color: var(--color-background); + border: 1px solid var(--color-selected-border); + overflow-y: auto; +} + +.Dropdown .Component, +.Dropdown .FocusedComponent { + display: block; + margin: 0.25rem 0.75rem 0.35rem; +} + +.DropdownButtonActive { + background-color: var(--color-selected-background); + color: var(--color-selected-foreground); +} + .VRule { + flex: 0 0 auto; height: 20px; width: 1px; background-color: var(--color-border); diff --git a/src/devtools/views/Components/OwnersStack.js b/src/devtools/views/Components/OwnersStack.js index 2049d4d5c1..55afea5de3 100644 --- a/src/devtools/views/Components/OwnersStack.js +++ b/src/devtools/views/Components/OwnersStack.js @@ -1,43 +1,84 @@ // @flow - -import React, { useCallback, useContext } from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useState, + createRef, + forwardRef, +} from 'react'; +import classNames from 'classnames'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import { TreeContext } from './TreeContext'; import { StoreContext } from '../context'; +import { getElementDimensions } from '../../../utils'; import type { Element } from './types'; import styles from './OwnersStack.css'; -export default function OwnerStack() { - const { ownerStack, resetOwnerStack } = useContext(TreeContext); +type ElementsDropdownProps = { + selectedElementIndex: number | null, + children: Array, +}; +function ElementsDropdown({ + selectedElementIndex, + children, +}: ElementsDropdownProps) { + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const handleClick = useCallback(() => { + setIsDropdownVisible(!isDropdownVisible); + }, [isDropdownVisible, setIsDropdownVisible]); - const elements = ownerStack.map((id, index) => ( - - )); + useEffect(() => { + setIsDropdownVisible(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedElementIndex]); return ( -
+
-
- {elements} + {isDropdownVisible &&
{children}
}
); } -type Props = { +type ElementsBarProps = { + elements: Array, + showSelectedOnly: boolean, +}; +const ElementsBar = forwardRef( + ( + { elements, showSelectedOnly }: ElementsBarProps, + ref: Object + ) => { + return ( +
+ {elements} +
+ ); + } +); + +type ElementViewProps = { id: number, index: number, }; - -function ElementView({ id, index }: Props) { +function ElementView({ id, index }: ElementViewProps) { const { ownerStackIndex, selectOwner } = useContext(TreeContext); const store = useContext(StoreContext); const { displayName } = ((store.getElementByID(id): any): Element); @@ -59,3 +100,101 @@ function ElementView({ id, index }: Props) { ); } + +export default function OwnerStack() { + const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext( + TreeContext + ); + const [isElementsBarOverflowing, setIsElementsBarOverflowing] = useState( + false + ); + const [elementsTotalWidth, setElementsTotalWidth] = useState(0); + const elementsBarRef = createRef(); + const elements = ownerStack.map((id, index) => ( + + )); + + useEffect(() => { + if (elementsBarRef.current === null) { + return () => {}; + } + const elements = Array.from(elementsBarRef.current.children); + const elementsTotalWidth = elements.reduce((acc, el) => { + const { offsetWidth } = el; + const { marginRight } = getElementDimensions(el); + return acc + (offsetWidth + marginRight); + }, 0); + + setElementsTotalWidth(elementsTotalWidth); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ownerStackIndex, elementsBarRef]); + + useElementsBarOverflowing( + elementsBarRef, + elementsTotalWidth, + isElementsBarOverflowing => { + setIsElementsBarOverflowing(isElementsBarOverflowing); + } + ); + + return ( +
+ + {isElementsBarOverflowing && ( + + {elements} + + )} +
+ +
+ ); +} + +function useElementsBarOverflowing( + elementsBarRef: Object, + elementsTotalWidth: number, + callback: Function +) { + const isElementsBarOverflowing = useCallback(() => { + if (elementsBarRef.current !== null) { + const elementsBarWidth = elementsBarRef.current.clientWidth; + return elementsBarWidth <= elementsTotalWidth; + } + return false; + }, [elementsBarRef, elementsTotalWidth]); + + useEffect(() => { + let timeoutID = null; + const handleResize = () => { + callback(isElementsBarOverflowing()); + }; + const debounceHandleResize = () => { + clearTimeout(((timeoutID: any): TimeoutID)); + timeoutID = setTimeout(handleResize, 100); + }; + + handleResize(); + // It's important to listen to the ownerDocument.defaultView to support the browser extension. + // Here we use portals to render individual tabs (e.g. Profiler), + // and the root document might belong to a different window. + const ownerWindow = elementsBarRef.current.ownerDocument.defaultView; + ownerWindow.addEventListener('resize', debounceHandleResize); + return () => { + ownerWindow.removeEventListener('resize', debounceHandleResize); + if (timeoutID !== null) { + clearTimeout(timeoutID); + } + }; + }, [elementsBarRef, isElementsBarOverflowing, callback]); +} diff --git a/src/utils.js b/src/utils.js index 9c2c9189b9..aa79351fc6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -63,3 +63,21 @@ export function utfEncodeString(string: string): Uint32Array { function toCodePoint(string: string) { return string.codePointAt(0); } + +export function getElementDimensions(domElement: Element) { + const calculatedStyle = window.getComputedStyle(domElement); + return { + borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0], + borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0], + borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0], + borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0], + marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0], + marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0], + marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0], + marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0], + paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0], + paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0], + paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0], + paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0], + }; +}