Skip to content

Commit c59b206

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

File tree

5 files changed

+57
-22
lines changed

5 files changed

+57
-22
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: 40 additions & 14 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,21 @@ 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 (
368+
!activeDescendantControl ||
369+
(!activatedByKeyboardNavigation && activeDescendantControl.getAttribute('aria-activedescendant') === to.id)
370+
) {
371+
// prevent active descendant callback from being called repeatedly if the same element is activated (e.g. via mousemove)
372+
return
373+
}
374+
375+
activeDescendantControl.setAttribute('aria-activedescendant', to.id)
376+
activeDescendantCallback?.(to, from, activatedByKeyboardNavigation)
368377
}
369378

370379
function clearActiveDescendant(previouslyActiveElement = currentFocusedElement) {
@@ -373,7 +382,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
373382
}
374383

375384
activeDescendantControl?.removeAttribute('aria-activedescendant')
376-
activeDescendantCallback?.(undefined, previouslyActiveElement)
385+
activeDescendantCallback?.(undefined, previouslyActiveElement, false)
377386
}
378387

379388
function beginFocusManagement(...elements: HTMLElement[]) {
@@ -484,6 +493,23 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484493
updateFocusedElement(event.target)
485494
}
486495
})
496+
container.addEventListener(
497+
'mousemove',
498+
({target}) => {
499+
if (!(target instanceof HTMLElement)) {
500+
return
501+
}
502+
503+
const focusableElement = focusableElements.find(element => element.contains(target))
504+
505+
if (focusableElement) {
506+
updateFocusedElement(focusableElement)
507+
}
508+
},
509+
{signal, capture: true}
510+
)
511+
512+
// Listeners specifically on the controlling element
487513
activeDescendantControl.addEventListener('focusin', () => {
488514
// Focus moved into the active descendant input. Activate current or first descendant.
489515
if (!currentFocusedElement) {
@@ -637,13 +663,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
637663
}
638664
}
639665

640-
if (nextElementToFocus) {
641-
if (activeDescendantControl) {
642-
updateFocusedElement(nextElementToFocus)
643-
} else {
644-
lastKeyboardFocusDirection = direction
645-
nextElementToFocus.focus()
646-
}
666+
if (activeDescendantControl) {
667+
updateFocusedElement(nextElementToFocus || currentFocusedElement, true)
668+
} else if (nextElementToFocus) {
669+
lastKeyboardFocusDirection = direction
670+
671+
// updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
672+
nextElementToFocus.focus()
647673
}
648674
// Tab should always allow escaping from this container, so only
649675
// preventDefault if tab key press already resulted in a focus movement

0 commit comments

Comments
 (0)