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}