Skip to content

Commit 62acf73

Browse files
committed
Adjust mutation observer to watch attributes (hidden, disabled)
1 parent 755f319 commit 62acf73

File tree

1 file changed

+49
-44
lines changed

1 file changed

+49
-44
lines changed

src/focus-zone.ts

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {polyfill as eventListenerSignalPolyfill} from './polyfills/event-listener-signal.js'
22
import {isMacOS} from './utils/user-agent.js'
3-
import {IterateFocusableElements, isFocusable, iterateFocusableElements} from './utils/iterate-focusable-elements.js'
3+
import {IterateFocusableElements, iterateFocusableElements} from './utils/iterate-focusable-elements.js'
44
import {uniqueId} from './utils/unique-id.js'
55

66
eventListenerSignalPolyfill()
@@ -527,19 +527,35 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
527527
endFocusManagement(...iterateFocusableElements(removedNode, iterateFocusableElementsOptions))
528528
}
529529
}
530+
// If an element is hidden or disabled, remove it from the list of focusable elements
531+
if (mutation.type === 'attributes' && mutation.oldValue === null) {
532+
if (mutation.target instanceof HTMLElement) {
533+
endFocusManagement(mutation.target)
534+
}
535+
}
530536
}
531537
for (const mutation of mutations) {
532538
for (const addedNode of mutation.addedNodes) {
533539
if (addedNode instanceof HTMLElement) {
534540
beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions))
535541
}
536542
}
543+
544+
// Similarly, if an element is un-hidden or "enabled", add it to the list of focusable elements
545+
// If `mutation.oldValue` is not null, then we may assume that the element was previously hidden or disabled
546+
if (mutation.type === 'attributes' && mutation.oldValue !== null) {
547+
if (mutation.target instanceof HTMLElement) {
548+
beginFocusManagement(mutation.target)
549+
}
550+
}
537551
}
538552
})
539553

540554
observer.observe(container, {
541555
subtree: true,
542556
childList: true,
557+
attributeFilter: ['hidden', 'disabled'],
558+
attributeOldValue: true,
543559
})
544560

545561
const controller = new AbortController()
@@ -686,40 +702,6 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
686702
return focusedIndex !== -1 ? focusedIndex : fallbackIndex
687703
}
688704

689-
function handleKeydownInteraction(direction: Direction, key: string) {
690-
const lastFocusedIndex = getCurrentFocusedIndex()
691-
let nextFocusedIndex = lastFocusedIndex
692-
if (direction === 'previous') {
693-
nextFocusedIndex -= 1
694-
} else if (direction === 'start') {
695-
nextFocusedIndex = 0
696-
} else if (direction === 'next') {
697-
nextFocusedIndex += 1
698-
} else {
699-
// end
700-
nextFocusedIndex = focusableElements.length - 1
701-
}
702-
703-
if (nextFocusedIndex < 0) {
704-
// Tab should never cause focus to wrap. Use focusTrap for that behavior.
705-
if (focusOutBehavior === 'wrap' && key !== 'Tab') {
706-
nextFocusedIndex = focusableElements.length - 1
707-
} else {
708-
nextFocusedIndex = 0
709-
}
710-
}
711-
if (nextFocusedIndex >= focusableElements.length) {
712-
if (focusOutBehavior === 'wrap' && key !== 'Tab') {
713-
nextFocusedIndex = 0
714-
} else {
715-
nextFocusedIndex = focusableElements.length - 1
716-
}
717-
}
718-
if (lastFocusedIndex !== nextFocusedIndex) {
719-
return focusableElements[nextFocusedIndex]
720-
}
721-
}
722-
723705
// "keydown" is the event that triggers DOM focus change, so that is what we use here
724706
keyboardEventRecipient.addEventListener(
725707
'keydown',
@@ -742,23 +724,46 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
742724
nextElementToFocus = settings.getNextFocusable(direction, document.activeElement ?? undefined, event)
743725
}
744726
if (!nextElementToFocus) {
745-
nextElementToFocus = handleKeydownInteraction(direction, event.key)
727+
const lastFocusedIndex = getCurrentFocusedIndex()
728+
let nextFocusedIndex = lastFocusedIndex
729+
if (direction === 'previous') {
730+
nextFocusedIndex -= 1
731+
} else if (direction === 'start') {
732+
nextFocusedIndex = 0
733+
} else if (direction === 'next') {
734+
nextFocusedIndex += 1
735+
} else {
736+
// end
737+
nextFocusedIndex = focusableElements.length - 1
738+
}
739+
740+
if (nextFocusedIndex < 0) {
741+
// Tab should never cause focus to wrap. Use focusTrap for that behavior.
742+
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
743+
nextFocusedIndex = focusableElements.length - 1
744+
} else {
745+
nextFocusedIndex = 0
746+
}
747+
}
748+
if (nextFocusedIndex >= focusableElements.length) {
749+
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
750+
nextFocusedIndex = 0
751+
} else {
752+
nextFocusedIndex = focusableElements.length - 1
753+
}
754+
}
755+
if (lastFocusedIndex !== nextFocusedIndex) {
756+
nextElementToFocus = focusableElements[nextFocusedIndex]
757+
}
746758
}
747759

748760
if (activeDescendantControl) {
749761
updateFocusedElement(nextElementToFocus || currentFocusedElement, true)
750762
} else if (nextElementToFocus) {
751-
// If for some reason the next element to focus is not focusable (e.g. dynamically hidden), we don't want to attempt to focus it.
752-
// Instead, we want to remove that specific element from focus management.
753-
if (!isFocusable(nextElementToFocus)) {
754-
endFocusManagement(nextElementToFocus)
755-
nextElementToFocus = handleKeydownInteraction(direction, event.key)
756-
}
757-
758763
lastKeyboardFocusDirection = direction
759764

760765
// updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
761-
nextElementToFocus?.focus({preventScroll})
766+
nextElementToFocus.focus({preventScroll})
762767
}
763768
// Tab should always allow escaping from this container, so only
764769
// preventDefault if tab key press already resulted in a focus movement

0 commit comments

Comments
 (0)