Skip to content

Commit 7b8097b

Browse files
committed
(a11y): back with removed code to run some tests
1 parent 22a0d3a commit 7b8097b

File tree

1 file changed

+114
-2
lines changed

1 file changed

+114
-2
lines changed

core/src/utils/overlays.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getElementRoot,
3333
removeEventListener,
3434
} from './helpers';
35+
import { isPlatform } from './platform';
3536

3637
let lastOverlayIndex = 0;
3738
let lastId = 0;
@@ -512,6 +513,8 @@ export const present = async <OverlayPresentOptions>(
512513
return;
513514
}
514515

516+
console.log("presenting overlay...");
517+
515518
/**
516519
* Due to accessibility guidelines, toasts do not have
517520
* focus traps.
@@ -521,9 +524,11 @@ export const present = async <OverlayPresentOptions>(
521524
*/
522525
if (overlay.el.tagName !== 'ION-TOAST') {
523526
setRootAriaHidden(true);
524-
document.body.classList.add(BACKDROP_NO_SCROLL);
525527
}
526528

529+
hideUnderlyingOverlaysFromScreenReaders(overlay.el);
530+
hideAnimatingOverlayFromScreenReaders(overlay.el);
531+
527532
overlay.presented = true;
528533
overlay.willPresent.emit();
529534
overlay.willPresentShorthand?.emit();
@@ -575,6 +580,7 @@ export const present = async <OverlayPresentOptions>(
575580
* screen readers.
576581
*/
577582
overlay.el.removeAttribute('aria-hidden');
583+
overlay.el.removeAttribute('inert');
578584
};
579585

580586
/**
@@ -672,6 +678,13 @@ export const dismiss = async <OverlayDismissOptions>(
672678
overlay.presented = false;
673679

674680
try {
681+
/**
682+
* There is no need to show the overlay to screen readers during
683+
* the dismiss animation. This is because the overlay will be removed
684+
* from the DOM after the animation is complete.
685+
*/
686+
hideAnimatingOverlayFromScreenReaders(overlay.el);
687+
675688
// Overlay contents should not be clickable during dismiss
676689
overlay.el.style.setProperty('pointer-events', 'none');
677690
overlay.willDismiss.emit({ data, role });
@@ -719,6 +732,8 @@ export const dismiss = async <OverlayDismissOptions>(
719732

720733
overlay.el.remove();
721734

735+
revealOverlaysToScreenReaders();
736+
722737
return true;
723738
};
724739

@@ -955,4 +970,101 @@ export const createTriggerController = () => {
955970
};
956971
};
957972

958-
export const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap';
973+
/**
974+
* The overlay that is being animated also needs to hide from screen
975+
* readers during its animation. This ensures that assistive technologies
976+
* like TalkBack do not announce or interact with the content until the
977+
* animation is complete, avoiding confusion for users.
978+
*
979+
* When the overlay is presented on an Android device, TalkBack's focus rings
980+
* may appear in the wrong position due to the transition (specifically
981+
* `transform` styles). This occurs because the focus rings are initially
982+
* displayed at the starting position of the elements before the transition
983+
* begins. This workaround ensures the focus rings do not appear in the
984+
* incorrect location.
985+
*
986+
* If this solution is applied to iOS devices, then it leads to a bug where
987+
* the overlays cannot be accessed by screen readers. This is due to
988+
* VoiceOver not being able to update the accessibility tree when the
989+
* `aria-hidden` is removed.
990+
*
991+
* @param overlay - The overlay that is being animated.
992+
*/
993+
const hideAnimatingOverlayFromScreenReaders = (overlay: HTMLIonOverlayElement) => {
994+
if (doc === undefined) return;
995+
996+
if (isPlatform('android')) {
997+
/**
998+
* Once the animation is complete, this attribute will be removed.
999+
* This is done at the end of the `present` method.
1000+
*/
1001+
overlay.setAttribute('aria-hidden', 'true');
1002+
overlay.setAttribute('inert', '');
1003+
}
1004+
};
1005+
1006+
/**
1007+
* Ensure that underlying overlays have aria-hidden if necessary so that screen readers
1008+
* cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout
1009+
* events here because those events do not fire when the screen readers moves to a non-focusable
1010+
* element such as text.
1011+
* Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
1012+
*
1013+
* @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been
1014+
* fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result.
1015+
*/
1016+
const hideUnderlyingOverlaysFromScreenReaders = (newTopMostOverlay: HTMLIonOverlayElement) => {
1017+
if (doc === undefined) return;
1018+
1019+
const overlays = getPresentedOverlays(doc);
1020+
1021+
for (let i = overlays.length - 1; i >= 0; i--) {
1022+
const presentedOverlay = overlays[i];
1023+
const nextPresentedOverlay = overlays[i + 1] ?? newTopMostOverlay;
1024+
1025+
/**
1026+
* If next overlay has aria-hidden then all remaining overlays will have it too.
1027+
* Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
1028+
* should not have aria-hidden either so focus can remain in the current overlay.
1029+
*/
1030+
if (nextPresentedOverlay.hasAttribute('aria-hidden') || nextPresentedOverlay.tagName !== 'ION-TOAST') {
1031+
presentedOverlay.setAttribute('aria-hidden', 'true');
1032+
presentedOverlay.setAttribute('inert', '');
1033+
}
1034+
}
1035+
};
1036+
1037+
/**
1038+
* When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
1039+
* If the top-most overlay is a Toast we potentially need to reveal more overlays since
1040+
* focus is never automatically moved to the Toast.
1041+
*/
1042+
const revealOverlaysToScreenReaders = () => {
1043+
if (doc === undefined) return;
1044+
1045+
const overlays = getPresentedOverlays(doc);
1046+
1047+
for (let i = overlays.length - 1; i >= 0; i--) {
1048+
const currentOverlay = overlays[i];
1049+
1050+
/**
1051+
* If the current we are looking at is a Toast then we can remove aria-hidden.
1052+
* However, we potentially need to keep looking at the overlay stack because there
1053+
* could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
1054+
* overlay too so focus can move there since focus is never automatically moved to the Toast.
1055+
*/
1056+
currentOverlay.removeAttribute('aria-hidden');
1057+
currentOverlay.removeAttribute('inert');
1058+
1059+
/**
1060+
* If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
1061+
* since this overlay should always receive focus. As a result, all underlying overlays should still
1062+
* be hidden from screen readers.
1063+
*/
1064+
if (currentOverlay.tagName !== 'ION-TOAST') {
1065+
break;
1066+
}
1067+
}
1068+
};
1069+
1070+
export const FOCUS_TRAP_DISABLE_CLASS = 'ion-disable-focus-trap';

0 commit comments

Comments
 (0)