1
1
import { polyfill as eventListenerSignalPolyfill } from './polyfills/event-listener-signal.js'
2
2
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'
4
4
import { uniqueId } from './utils/unique-id.js'
5
5
6
6
eventListenerSignalPolyfill ( )
@@ -527,19 +527,35 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
527
527
endFocusManagement ( ...iterateFocusableElements ( removedNode , iterateFocusableElementsOptions ) )
528
528
}
529
529
}
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
+ }
530
536
}
531
537
for ( const mutation of mutations ) {
532
538
for ( const addedNode of mutation . addedNodes ) {
533
539
if ( addedNode instanceof HTMLElement ) {
534
540
beginFocusManagement ( ...iterateFocusableElements ( addedNode , iterateFocusableElementsOptions ) )
535
541
}
536
542
}
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
+ }
537
551
}
538
552
} )
539
553
540
554
observer . observe ( container , {
541
555
subtree : true ,
542
556
childList : true ,
557
+ attributeFilter : [ 'hidden' , 'disabled' ] ,
558
+ attributeOldValue : true ,
543
559
} )
544
560
545
561
const controller = new AbortController ( )
@@ -686,40 +702,6 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
686
702
return focusedIndex !== - 1 ? focusedIndex : fallbackIndex
687
703
}
688
704
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
-
723
705
// "keydown" is the event that triggers DOM focus change, so that is what we use here
724
706
keyboardEventRecipient . addEventListener (
725
707
'keydown' ,
@@ -742,23 +724,46 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
742
724
nextElementToFocus = settings . getNextFocusable ( direction , document . activeElement ?? undefined , event )
743
725
}
744
726
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
+ }
746
758
}
747
759
748
760
if ( activeDescendantControl ) {
749
761
updateFocusedElement ( nextElementToFocus || currentFocusedElement , true )
750
762
} 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
-
758
763
lastKeyboardFocusDirection = direction
759
764
760
765
// 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} )
762
767
}
763
768
// Tab should always allow escaping from this container, so only
764
769
// preventDefault if tab key press already resulted in a focus movement
0 commit comments