@samitouri / QOS-React-2 / commits / 38b7cf5693

Ignore hover when navigating with keyboard

Dan Abramov committed Apr 11, 2019 at 13:25 UTC 38b7cf56936b55a59d2e3dd405a60122dfc00af7
3 files changed +91 -26
src/devtools/views/Components/Element.css
+3 -2
@@ -1,6 +1,7 @@
1 .Element,
2 .InactiveSelectedElement,
3 -.SelectedElement {
3 +.SelectedElement,
4 +.HoveredElement {
5 border-radius: 0.25em;
6 white-space: nowrap;
7 line-height: var(--line-height-data);
@@ -11,7 +12,7 @@
12
13 --color-expand-collapse-toggle: var(--color-dim);
14 }
14 -.Element:hover {
15 +.HoveredElement {
16 background-color: var(--color-hover-background);
17 }
18 .InactiveSelectedElement {
src/devtools/views/Components/Element.js
+20 -16
@@ -7,13 +7,14 @@ import React, {
7 useLayoutEffect,
8 useMemo,
9 useRef,
10 + useState,
11 } from 'react';
12 import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
13 import Store from 'src/devtools/store';
14 import ButtonIcon from '../ButtonIcon';
15 import { createRegExp } from '../utils';
16 import { TreeContext } from './TreeContext';
16 -import { BridgeContext, StoreContext } from '../context';
17 +import { StoreContext } from '../context';
18
19 import type { ItemData } from './Tree';
20 import type { Element } from './types';
@@ -27,6 +28,7 @@ type Props = {
28 };
29
30 export default function ElementView({ data, index, style }: Props) {
31 + const [isHovered, setIsHovered] = useState(false);
32 const {
33 baseDepth,
34 getElementAtIndex,
@@ -35,15 +37,18 @@ export default function ElementView({ data, index, style }: Props) {
37 selectedElementID,
38 selectElementByID,
39 } = useContext(TreeContext);
38 - const bridge = useContext(BridgeContext);
40 const store = useContext(StoreContext);
41
42 const element = getElementAtIndex(index);
43
44 + const {
45 + lastScrolledIDRef,
46 + treeFocused,
47 + isNavigatingWithKeyboard,
48 + onElementMouseEnter,
49 + } = data;
50 const id = element === null ? null : element.id;
51 const isSelected = selectedElementID === id;
45 - const lastScrolledIDRef = data.lastScrolledIDRef;
46 - const treeFocused = data.treeFocused;
52
53 const handleDoubleClick = useCallback(() => {
54 if (id !== null) {
@@ -88,20 +93,16 @@ export default function ElementView({ data, index, style }: Props) {
93 [id, selectElementByID]
94 );
95
91 - const rendererID = id !== null ? store.getRendererIDForElement(id) : null;
92 - // Individual elements don't have a corresponding leave handler.
93 - // Instead, it's implemented on the tree level.
96 const handleMouseEnter = useCallback(() => {
95 - if (element !== null && id !== null && rendererID !== null) {
96 - bridge.send('highlightElementInDOM', {
97 - displayName: element.displayName,
98 - hideAfterTimeout: false,
99 - id,
100 - rendererID,
101 - scrollIntoView: false,
102 - });
97 + setIsHovered(true);
98 + if (id !== null) {
99 + onElementMouseEnter(id);
100 }
104 - }, [bridge, element, id, rendererID]);
101 + }, [onElementMouseEnter, id]);
102 +
103 + const handleMouseLeave = useCallback(() => {
104 + setIsHovered(false);
105 + }, []);
106
107 // Handle elements that are removed from the tree while an async render is in progress.
108 if (element == null) {
@@ -121,12 +122,15 @@ export default function ElementView({ data, index, style }: Props) {
122 className = treeFocused
123 ? styles.SelectedElement
124 : styles.InactiveSelectedElement;
125 + } else if (isHovered && !isNavigatingWithKeyboard) {
126 + className = styles.HoveredElement;
127 }
128
129 return (
130 <div
131 className={className}
132 onMouseEnter={handleMouseEnter}
133 + onMouseLeave={handleMouseLeave}
134 onMouseDown={handleMouseDown}
135 onDoubleClick={handleDoubleClick}
136 style={{
src/devtools/views/Components/Tree.js
+68 -8
@@ -27,7 +27,9 @@ export type ItemData = {|
27 baseDepth: number,
28 numElements: number,
29 getElementAtIndex: (index: number) => Element | null,
30 + isNavigatingWithKeyboard: boolean,
31 lastScrolledIDRef: { current: number | null },
32 + onElementMouseEnter: (id: number) => void,
33 treeFocused: boolean,
34 |};
35
@@ -49,6 +51,9 @@ export default function Tree(props: Props) {
51 } = useContext(TreeContext);
52 const bridge = useContext(BridgeContext);
53 const store = useContext(StoreContext);
54 + const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] = useState(
55 + false
56 + );
57 // $FlowFixMe https://github.com/facebook/flow/issues/7341
58 const listRef = useRef<FixedSizeList<ItemData> | null>(null);
59 const treeRef = useRef<HTMLDivElement | null>(null);
@@ -100,8 +105,6 @@ export default function Tree(props: Props) {
105 }
106
107 let element;
103 -
104 - // eslint-disable-next-line default-case
108 switch (event.key) {
109 case 'ArrowDown':
110 event.preventDefault();
@@ -143,7 +146,10 @@ export default function Tree(props: Props) {
146 event.preventDefault();
147 selectPreviousElementInTree();
148 break;
149 + default:
150 + return;
151 }
152 + setIsNavigatingWithKeyboard(true);
153 };
154
155 // It's important to listen to the ownerDocument to support the browser extension.
@@ -163,8 +169,8 @@ export default function Tree(props: Props) {
169 store,
170 ]);
171
172 + // Focus management.
173 const handleBlur = useCallback(() => setTreeFocused(false));
167 -
174 const handleFocus = useCallback(() => {
175 setTreeFocused(true);
176
@@ -189,6 +195,53 @@ export default function Tree(props: Props) {
195 [selectedElementID, selectOwner]
196 );
197
198 + const highlightElementInDOM = useCallback(
199 + (id: number) => {
200 + const element = store.getElementByID(id);
201 + const rendererID = store.getRendererIDForElement(id);
202 + if (element !== null) {
203 + bridge.send('highlightElementInDOM', {
204 + displayName: element.displayName,
205 + hideAfterTimeout: false,
206 + id,
207 + rendererID,
208 + scrollIntoView: false,
209 + });
210 + }
211 + },
212 + [store, bridge]
213 + );
214 +
215 + // If we switch the selected element while using the keyboard,
216 + // start highlighting it in the DOM instead of the last hovered node.
217 + useEffect(() => {
218 + if (isNavigatingWithKeyboard && selectedElementID !== null) {
219 + highlightElementInDOM(selectedElementID);
220 + }
221 + }, [isNavigatingWithKeyboard, highlightElementInDOM, selectedElementID]);
222 +
223 + // Highlight last hovered element.
224 + const handleElementMouseEnter = useCallback(
225 + id => {
226 + // Ignore hover while we're navigating with keyboard.
227 + // This avoids flicker from the hovered nodes under the mouse.
228 + if (!isNavigatingWithKeyboard) {
229 + highlightElementInDOM(id);
230 + }
231 + },
232 + [isNavigatingWithKeyboard, highlightElementInDOM]
233 + );
234 +
235 + const handleMouseMove = useCallback(() => {
236 + // We started using the mouse again.
237 + // This will enable hover styles in individual rows.
238 + setIsNavigatingWithKeyboard(false);
239 + }, []);
240 +
241 + const handleMouseLeave = useCallback(() => {
242 + bridge.send('clearHighlightedElementInDOM');
243 + }, [bridge]);
244 +
245 // Let react-window know to re-render any time the underlying tree data changes.
246 // This includes the owner context, since it controls a filtered view of the tree.
247 const itemData = useMemo<ItemData>(
@@ -196,16 +249,22 @@ export default function Tree(props: Props) {
249 baseDepth,
250 numElements,
251 getElementAtIndex,
252 + isNavigatingWithKeyboard,
253 + onElementMouseEnter: handleElementMouseEnter,
254 lastScrolledIDRef,
255 treeFocused,
256 }),
202 - [baseDepth, numElements, getElementAtIndex, lastScrolledIDRef, treeFocused]
257 + [
258 + baseDepth,
259 + numElements,
260 + getElementAtIndex,
261 + isNavigatingWithKeyboard,
262 + handleElementMouseEnter,
263 + lastScrolledIDRef,
264 + treeFocused,
265 + ]
266 );
267
205 - const handleMouseLeave = useCallback(() => {
206 - bridge.send('clearHighlightedElementInDOM');
207 - }, [bridge]);
208 -
268 return (
269 <div className={styles.Tree} ref={treeRef}>
270 <div className={styles.SearchInput}>
@@ -217,6 +276,7 @@ export default function Tree(props: Props) {
276 onBlur={handleBlur}
277 onFocus={handleFocus}
278 onKeyPress={handleKeyPress}
279 + onMouseMove={handleMouseMove}
280 onMouseLeave={handleMouseLeave}
281 ref={focusTargetRef}
282 tabIndex={0}