@@ -32,7 +32,6 @@ import {
3232 getElementRoot ,
3333 removeEventListener ,
3434} from './helpers' ;
35- import { isPlatform } from './platform' ;
3635
3736let lastOverlayIndex = 0 ;
3837let lastId = 0 ;
@@ -513,6 +512,18 @@ export const present = async <OverlayPresentOptions>(
513512 return ;
514513 }
515514
515+ /**
516+ * When an overlay that steals focus
517+ * is dismissed, focus should be returned
518+ * to the element that was focused
519+ * prior to the overlay opening. Toast
520+ * does not steal focus and is excluded
521+ * from returning focus as a result.
522+ */
523+ if ( overlay . el . tagName !== 'ION-TOAST' ) {
524+ restoreElementFocus ( overlay . el ) ;
525+ }
526+
516527 /**
517528 * Due to accessibility guidelines, toasts do not have
518529 * focus traps.
@@ -525,9 +536,6 @@ export const present = async <OverlayPresentOptions>(
525536 document . body . classList . add ( BACKDROP_NO_SCROLL ) ;
526537 }
527538
528- hideUnderlyingOverlaysFromScreenReaders ( overlay . el ) ;
529- hideAnimatingOverlayFromScreenReaders ( overlay . el ) ;
530-
531539 overlay . presented = true ;
532540 overlay . willPresent . emit ( ) ;
533541 overlay . willPresentShorthand ?. emit ( ) ;
@@ -544,18 +552,6 @@ export const present = async <OverlayPresentOptions>(
544552 overlay . didPresentShorthand ?. emit ( ) ;
545553 }
546554
547- /**
548- * When an overlay that steals focus
549- * is dismissed, focus should be returned
550- * to the element that was focused
551- * prior to the overlay opening. Toast
552- * does not steal focus and is excluded
553- * from returning focus as a result.
554- */
555- if ( overlay . el . tagName !== 'ION-TOAST' ) {
556- restoreElementFocus ( overlay . el ) ;
557- }
558-
559555 /**
560556 * If the focused element is already
561557 * inside the overlay component then
@@ -598,6 +594,9 @@ const restoreElementFocus = async (overlayEl: any) => {
598594 return ;
599595 }
600596
597+ // Ensure active element is blurred to prevent a11y warning issues
598+ previousElement . blur ( ) ;
599+
601600 const shadowRoot = previousElement ?. shadowRoot ;
602601 if ( shadowRoot ) {
603602 // If there are no inner focusable elements, just focus the host element.
@@ -677,13 +676,6 @@ export const dismiss = async <OverlayDismissOptions>(
677676 overlay . presented = false ;
678677
679678 try {
680- /**
681- * There is no need to show the overlay to screen readers during
682- * the dismiss animation. This is because the overlay will be removed
683- * from the DOM after the animation is complete.
684- */
685- hideAnimatingOverlayFromScreenReaders ( overlay . el ) ;
686-
687679 // Overlay contents should not be clickable during dismiss
688680 overlay . el . style . setProperty ( 'pointer-events' , 'none' ) ;
689681 overlay . willDismiss . emit ( { data, role } ) ;
@@ -731,8 +723,6 @@ export const dismiss = async <OverlayDismissOptions>(
731723
732724 overlay . el . remove ( ) ;
733725
734- revealOverlaysToScreenReaders ( ) ;
735-
736726 return true ;
737727} ;
738728
@@ -969,101 +959,4 @@ export const createTriggerController = () => {
969959 } ;
970960} ;
971961
972- /**
973- * The overlay that is being animated also needs to hide from screen
974- * readers during its animation. This ensures that assistive technologies
975- * like TalkBack do not announce or interact with the content until the
976- * animation is complete, avoiding confusion for users.
977- *
978- * When the overlay is presented on an Android device, TalkBack's focus rings
979- * may appear in the wrong position due to the transition (specifically
980- * `transform` styles). This occurs because the focus rings are initially
981- * displayed at the starting position of the elements before the transition
982- * begins. This workaround ensures the focus rings do not appear in the
983- * incorrect location.
984- *
985- * If this solution is applied to iOS devices, then it leads to a bug where
986- * the overlays cannot be accessed by screen readers. This is due to
987- * VoiceOver not being able to update the accessibility tree when the
988- * `aria-hidden` is removed.
989- *
990- * @param overlay - The overlay that is being animated.
991- */
992- const hideAnimatingOverlayFromScreenReaders = ( overlay : HTMLIonOverlayElement ) => {
993- if ( doc === undefined ) return ;
994-
995- if ( isPlatform ( 'android' ) ) {
996- /**
997- * Once the animation is complete, this attribute will be removed.
998- * This is done at the end of the `present` method.
999- */
1000- overlay . setAttribute ( 'aria-hidden' , 'true' ) ;
1001- overlay . setAttribute ( 'inert' , '' ) ;
1002- }
1003- } ;
1004-
1005- /**
1006- * Ensure that underlying overlays have aria-hidden if necessary so that screen readers
1007- * cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout
1008- * events here because those events do not fire when the screen readers moves to a non-focusable
1009- * element such as text.
1010- * Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
1011- *
1012- * @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been
1013- * fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result.
1014- */
1015- const hideUnderlyingOverlaysFromScreenReaders = ( newTopMostOverlay : HTMLIonOverlayElement ) => {
1016- if ( doc === undefined ) return ;
1017-
1018- const overlays = getPresentedOverlays ( doc ) ;
1019-
1020- for ( let i = overlays . length - 1 ; i >= 0 ; i -- ) {
1021- const presentedOverlay = overlays [ i ] ;
1022- const nextPresentedOverlay = overlays [ i + 1 ] ?? newTopMostOverlay ;
1023-
1024- /**
1025- * If next overlay has aria-hidden then all remaining overlays will have it too.
1026- * Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
1027- * should not have aria-hidden either so focus can remain in the current overlay.
1028- */
1029- if ( nextPresentedOverlay . hasAttribute ( 'aria-hidden' ) || nextPresentedOverlay . tagName !== 'ION-TOAST' ) {
1030- presentedOverlay . setAttribute ( 'aria-hidden' , 'true' ) ;
1031- presentedOverlay . setAttribute ( 'inert' , '' ) ;
1032- }
1033- }
1034- } ;
1035-
1036- /**
1037- * When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
1038- * If the top-most overlay is a Toast we potentially need to reveal more overlays since
1039- * focus is never automatically moved to the Toast.
1040- */
1041- const revealOverlaysToScreenReaders = ( ) => {
1042- if ( doc === undefined ) return ;
1043-
1044- const overlays = getPresentedOverlays ( doc ) ;
1045-
1046- for ( let i = overlays . length - 1 ; i >= 0 ; i -- ) {
1047- const currentOverlay = overlays [ i ] ;
1048-
1049- /**
1050- * If the current we are looking at is a Toast then we can remove aria-hidden.
1051- * However, we potentially need to keep looking at the overlay stack because there
1052- * could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
1053- * overlay too so focus can move there since focus is never automatically moved to the Toast.
1054- */
1055- currentOverlay . removeAttribute ( 'aria-hidden' ) ;
1056- currentOverlay . removeAttribute ( 'inert' ) ;
1057-
1058- /**
1059- * If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
1060- * since this overlay should always receive focus. As a result, all underlying overlays should still
1061- * be hidden from screen readers.
1062- */
1063- if ( currentOverlay . tagName !== 'ION-TOAST' ) {
1064- break ;
1065- }
1066- }
1067- } ;
1068-
1069962export const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap' ;
0 commit comments