Skip to content

Commit 4db22de

Browse files
committed
feat(useFocusZone): update active descendant on mousemove
1 parent 987ad5c commit 4db22de

File tree

5 files changed

+50
-16
lines changed

5 files changed

+50
-16
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
"rollup-plugin-visualizer": "5.5.0",
130130
"semver": "7.3.5",
131131
"styled-components": "4.4.1",
132-
"typescript": "4.3.2"
132+
"typescript": "4.2.2"
133133
},
134134
"peerDependencies": {
135135
"react": "^17.0.0",

src/ActionList/Item.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, '
122122
}
123123

124124
export const itemActiveDescendantClass = `${uniqueId()}active-descendant`
125+
export const itemPassiveActiveDescendantClass = `${uniqueId()}passive-active-descendant`
125126

126127
const getItemVariant = (variant = 'default', disabled?: boolean) => {
127128
if (disabled) {
@@ -226,6 +227,11 @@ const StyledItem = styled.div<
226227
outline: none;
227228
}
228229
230+
// Passive active descendant is still an active descendant, but not focused by keyboard navigation (e.g. default item, hover, etc)
231+
&.${itemPassiveActiveDescendantClass} {
232+
background: ${({hoverBackground}) => hoverBackground};
233+
}
234+
229235
${sx}
230236
`
231237

src/FilteredActionList/FilteredActionList.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {ActionList} from '../ActionList'
77
import Spinner from '../Spinner'
88
import {useFocusZone} from '../hooks/useFocusZone'
99
import {uniqueId} from '../utils/uniqueId'
10-
import {itemActiveDescendantClass} from '../ActionList/Item'
10+
import {itemActiveDescendantClass, itemPassiveActiveDescendantClass} from '../ActionList/Item'
1111
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
1212
import styled from 'styled-components'
1313
import {get} from '../constants'
@@ -100,17 +100,20 @@ export function FilteredActionList({
100100
return true
101101
},
102102
activeDescendantFocus: inputRef,
103-
onActiveDescendantChanged: (current, previous) => {
103+
onActiveDescendantChanged: (current, previous, activatedByKeyboardNavigation) => {
104104
activeDescendantRef.current = current
105105

106106
if (previous) {
107107
previous.classList.remove(itemActiveDescendantClass)
108+
previous.classList.remove(itemPassiveActiveDescendantClass)
108109
}
109110

110111
if (current) {
111-
current.classList.add(itemActiveDescendantClass)
112+
current.classList.add(
113+
activatedByKeyboardNavigation ? itemActiveDescendantClass : itemPassiveActiveDescendantClass
114+
)
112115

113-
if (listContainerRef.current) {
116+
if (listContainerRef.current && activatedByKeyboardNavigation) {
114117
scrollIntoViewingArea(current, listContainerRef.current)
115118
}
116119
}

src/behaviors/focusZone.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ export interface FocusZoneSettings {
189189
*/
190190
onActiveDescendantChanged?: (
191191
newActiveDescendant: HTMLElement | undefined,
192-
previousActiveDescendant: HTMLElement | undefined
192+
previousActiveDescendant: HTMLElement | undefined,
193+
activatedByKeyboardNavigation: boolean
193194
) => void
194195

195196
/**
@@ -337,13 +338,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
337338
return document.activeElement === activeDescendantControl
338339
}
339340

340-
function updateFocusedElement(to?: HTMLElement) {
341+
function updateFocusedElement(to?: HTMLElement, causedByKeyboardNavigation = false) {
341342
const from = currentFocusedElement
342343
currentFocusedElement = to
343344

344345
if (activeDescendantControl) {
345346
if (to && isActiveDescendantInputFocused()) {
346-
setActiveDescendant(from, to)
347+
setActiveDescendant(from, to, causedByKeyboardNavigation)
347348
} else {
348349
clearActiveDescendant()
349350
}
@@ -358,13 +359,18 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
358359
to?.setAttribute('tabindex', '0')
359360
}
360361

361-
function setActiveDescendant(from: HTMLElement | undefined, to: HTMLElement) {
362+
function setActiveDescendant(from: HTMLElement | undefined, to: HTMLElement, activatedByKeyboardNavigation = false) {
362363
if (!to.id) {
363364
to.setAttribute('id', uniqueId())
364365
}
365366

366-
activeDescendantControl?.setAttribute('aria-activedescendant', to.id)
367-
activeDescendantCallback?.(to, from === to ? undefined : from)
367+
if (!activeDescendantControl || activeDescendantControl.getAttribute('aria-activedescendant') === to.id) {
368+
// prevent active descendant callback from being called repeatedly if the same element is activated (e.g. via mousemove)
369+
return
370+
}
371+
372+
activeDescendantControl.setAttribute('aria-activedescendant', to.id)
373+
activeDescendantCallback?.(to, from === to ? undefined : from, activatedByKeyboardNavigation)
368374
}
369375

370376
function clearActiveDescendant(previouslyActiveElement = currentFocusedElement) {
@@ -373,7 +379,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
373379
}
374380

375381
activeDescendantControl?.removeAttribute('aria-activedescendant')
376-
activeDescendantCallback?.(undefined, previouslyActiveElement)
382+
activeDescendantCallback?.(undefined, previouslyActiveElement, false)
377383
}
378384

379385
function beginFocusManagement(...elements: HTMLElement[]) {
@@ -484,6 +490,23 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484490
updateFocusedElement(event.target)
485491
}
486492
})
493+
container.addEventListener(
494+
'mousemove',
495+
({target}) => {
496+
if (!(target instanceof HTMLElement)) {
497+
return
498+
}
499+
500+
const focusableElement = focusableElements.find(element => element.contains(target))
501+
502+
if (focusableElement) {
503+
updateFocusedElement(focusableElement)
504+
}
505+
},
506+
{signal, capture: true}
507+
)
508+
509+
// Listeners specifically on the controlling element
487510
activeDescendantControl.addEventListener('focusin', () => {
488511
// Focus moved into the active descendant input. Activate current or first descendant.
489512
if (!currentFocusedElement) {
@@ -639,9 +662,11 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
639662

640663
if (nextElementToFocus) {
641664
if (activeDescendantControl) {
642-
updateFocusedElement(nextElementToFocus)
665+
updateFocusedElement(nextElementToFocus, true)
643666
} else {
644667
lastKeyboardFocusDirection = direction
668+
669+
// updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
645670
nextElementToFocus.focus()
646671
}
647672
}

0 commit comments

Comments
 (0)