diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 49930f9c7ddcf..7d94449fcc748 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -206,8 +206,12 @@ function createComponentsPanel() { } }); - // TODO: we should listen to createdPanel.onHidden to unmount some listeners - // and potentially stop highlighting + createdPanel.onShown.addListener(() => { + bridge.emit('extensionComponentsPanelShown'); + }); + createdPanel.onHidden.addListener(() => { + bridge.emit('extensionComponentsPanelHidden'); + }); }, ); } diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index cb494e1b3c1ba..3a12ae7415025 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -217,6 +217,8 @@ type FrontendEvents = { clearWarningsForElementID: [ElementAndRendererID], copyElementPath: [CopyElementPathParams], deletePath: [DeletePath], + extensionComponentsPanelShown: [], + extensionComponentsPanelHidden: [], getBackendVersion: [], getBridgeProtocol: [], getIfHasUnsupportedRendererVersion: [], diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 2a1569bb52e1e..d0fc0d924cd17 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -37,6 +37,7 @@ import styles from './Tree.css'; import ButtonIcon from '../ButtonIcon'; import Button from '../Button'; import {logEvent} from 'react-devtools-shared/src/Logger'; +import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility'; // Never indent more than this number of pixels (even if we have the room). const DEFAULT_INDENTATION_SIZE = 12; @@ -76,36 +77,28 @@ export default function Tree(): React.Node { const bridge = useContext(BridgeContext); const store = useContext(StoreContext); const {hideSettings} = useContext(OptionsContext); + const {lineHeight} = useContext(SettingsContext); + const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] = useState(false); const {highlightHostInstance, clearHighlightHostInstance} = useHighlightHostInstance(); + const [treeFocused, setTreeFocused] = useState(false); + const componentsPanelVisible = useExtensionComponentsPanelVisibility(bridge); + const treeRef = useRef(null); const focusTargetRef = useRef(null); + const listRef = useRef(null); - const [treeFocused, setTreeFocused] = useState(false); - - const {lineHeight} = useContext(SettingsContext); + useEffect(() => { + if (!componentsPanelVisible) { + return; + } - // Make sure a newly selected element is visible in the list. - // This is helpful for things like the owners list and search. - // - // TRICKY: - // It's important to use a callback ref for this, rather than a ref object and an effect. - // As an optimization, the AutoSizer component does not render children when their size would be 0. - // This means that in some cases (if the browser panel size is initially really small), - // the Tree component might render without rendering an inner List. - // In this case, the list ref would be null on mount (when the scroll effect runs), - // meaning the scroll action would be skipped (since ref updates don't re-run effects). - // Using a callback ref accounts for this case... - const listCallbackRef = useCallback( - (list: $FlowFixMe) => { - if (list != null && inspectedElementIndex !== null) { - list.scrollToItem(inspectedElementIndex, 'smart'); - } - }, - [inspectedElementIndex], - ); + if (listRef.current != null && inspectedElementIndex !== null) { + listRef.current.scrollToItem(inspectedElementIndex, 'smart'); + } + }, [inspectedElementIndex, componentsPanelVisible]); // Picking an element in the inspector should put focus into the tree. // If possible, navigation works right after picking a node. @@ -426,7 +419,7 @@ export default function Tree(): React.Node { itemData={itemData} itemKey={itemKey} itemSize={lineHeight} - ref={listCallbackRef} + ref={listRef} width={width}> {Element} diff --git a/packages/react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility.js b/packages/react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility.js new file mode 100644 index 0000000000000..f36e4a59c3b5e --- /dev/null +++ b/packages/react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {useState, useEffect} from 'react'; +import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; + +// Events that are prefixed with `extension` will only be emitted for the browser extension implementation. +// For other implementations, this hook will just return constant `true` value. +export function useExtensionComponentsPanelVisibility( + bridge: FrontendBridge, +): boolean { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + function onPanelShown() { + setIsVisible(true); + } + function onPanelHidden() { + setIsVisible(false); + } + + bridge.addListener('extensionComponentsPanelShown', onPanelShown); + bridge.addListener('extensionComponentsPanelHidden', onPanelHidden); + + return () => { + bridge.removeListener('extensionComponentsPanelShown', onPanelShown); + bridge.removeListener('extensionComponentsPanelHidden', onPanelHidden); + }; + }, [bridge]); + + return isVisible; +}