diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index e69bd49dcda..b00e5a6481c 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -514,7 +514,8 @@ export const present = async ( document.body.classList.add(BACKDROP_NO_SCROLL); - hideOverlaysFromScreenReaders(overlay.el); + hideUnderlyingOverlaysFromScreenReaders(overlay.el); + hideAnimatingOverlayFromScreenReaders(overlay.el); overlay.presented = true; overlay.willPresent.emit(); @@ -560,6 +561,11 @@ export const present = async ( * it would still have aria-hidden on being presented again. * Removing it here ensures the overlay is visible to screen * readers. + * + * If this overlay was being presented, then it was hidden + * from screen readers during the animation. Now that the + * animation is complete, we can reveal the overlay to + * screen readers. */ overlay.el.removeAttribute('aria-hidden'); }; @@ -644,6 +650,13 @@ export const dismiss = async ( overlay.presented = false; try { + /** + * There is no need to show the overlay to screen readers during + * the dismiss animation. This is because the overlay will be removed + * from the DOM after the animation is complete. + */ + hideAnimatingOverlayFromScreenReaders(overlay.el); + // Overlay contents should not be clickable during dismiss overlay.el.style.setProperty('pointer-events', 'none'); overlay.willDismiss.emit({ data, role }); @@ -929,6 +942,29 @@ export const createTriggerController = () => { }; }; +/** + * The overlay that is being animated also needs to hide from screen + * readers during its animation. This ensures that assistive technologies + * like TalkBack do not announce or interact with the content until the + * animation is complete, avoiding confusion for users. + * + * If the overlay is being presented, it prevents focus rings from appearing + * in incorrect positions due to the transition (specifically `transform` + * styles), ensuring that when aria-hidden is removed, the focus rings are + * correctly displayed in the final location of the elements. + * + * @param overlay - The overlay that is being animated. + */ +const hideAnimatingOverlayFromScreenReaders = (overlay: HTMLIonOverlayElement) => { + if (doc === undefined) return; + + /** + * Once the animation is complete, this attribute will be removed. + * This is done at the end of the `present` method. + */ + overlay.setAttribute('aria-hidden', 'true'); +}; + /** * Ensure that underlying overlays have aria-hidden if necessary so that screen readers * cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout @@ -939,7 +975,7 @@ export const createTriggerController = () => { * @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been * fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result. */ -const hideOverlaysFromScreenReaders = (newTopMostOverlay: HTMLIonOverlayElement) => { +const hideUnderlyingOverlaysFromScreenReaders = (newTopMostOverlay: HTMLIonOverlayElement) => { if (doc === undefined) return; const overlays = getPresentedOverlays(doc);