Skip to content

Commit 01ae835

Browse files
committed
chore: sync with main
2 parents ac01423 + 636531f commit 01ae835

31 files changed

+325
-127
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
7+
8+
9+
### Bug Fixes
10+
11+
* **angular:** add ionNavWillChange and ionNavDidChange types for nav ([#29122](https://github.com/ionic-team/ionic-framework/issues/29122)) ([85b9d5c](https://github.com/ionic-team/ionic-framework/commit/85b9d5c35f626ffc273d220549b0126ddc1f7e4b)), closes [#29114](https://github.com/ionic-team/ionic-framework/issues/29114)
12+
* **checkbox:** set aria-checked of indeterminate checkbox to 'mixed' ([#29115](https://github.com/ionic-team/ionic-framework/issues/29115)) ([b2d636f](https://github.com/ionic-team/ionic-framework/commit/b2d636f14dcd33313f6604cfd4a64b542c831b34))
13+
* **overlay:** do not hide overlay if toast is presented ([#29140](https://github.com/ionic-team/ionic-framework/issues/29140)) ([c0f5e5e](https://github.com/ionic-team/ionic-framework/commit/c0f5e5ebc0c9d45d71e10e09903b00b3ba8e6bba)), closes [#29139](https://github.com/ionic-team/ionic-framework/issues/29139)
14+
15+
16+
17+
18+
619
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
720

821

core/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
7+
8+
9+
### Bug Fixes
10+
11+
* **checkbox:** set aria-checked of indeterminate checkbox to 'mixed' ([#29115](https://github.com/ionic-team/ionic-framework/issues/29115)) ([b2d636f](https://github.com/ionic-team/ionic-framework/commit/b2d636f14dcd33313f6604cfd4a64b542c831b34))
12+
* **overlay:** do not hide overlay if toast is presented ([#29140](https://github.com/ionic-team/ionic-framework/issues/29140)) ([c0f5e5e](https://github.com/ionic-team/ionic-framework/commit/c0f5e5ebc0c9d45d71e10e09903b00b3ba8e6bba)), closes [#29139](https://github.com/ionic-team/ionic-framework/issues/29139)
13+
14+
15+
16+
17+
618
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
719

820

core/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ionic/core",
3-
"version": "7.7.4",
3+
"version": "7.7.5",
44
"description": "Base components for Ionic",
55
"keywords": [
66
"ionic",

core/src/components/app/app.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ComponentInterface } from '@stencil/core';
22
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
33
import type { FocusVisibleUtility } from '@utils/focus-visible';
4-
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
4+
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
55
import { printIonWarning } from '@utils/logging';
66
import { isPlatform } from '@utils/platform';
77

@@ -36,15 +36,15 @@ export class App implements ComponentInterface {
3636
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
3737
}
3838
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
39-
const supportsHardwareBackButtonEvents = isHybrid || shoudUseCloseWatcher();
39+
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
4040
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
4141
hardwareBackButtonModule.startHardwareBackButton();
4242
} else {
4343
/**
4444
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
4545
* then the close watcher will not be used.
4646
*/
47-
if (shoudUseCloseWatcher()) {
47+
if (shouldUseCloseWatcher()) {
4848
printIonWarning(
4949
'experimentalCloseWatcher was set to `true`, but hardwareBackButton was set to `false`. Both config options must be `true` for the Close Watcher API to be used.'
5050
);

core/src/components/menu/menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
33
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
44
import { GESTURE_CONTROLLER } from '@utils/gesture';
5-
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
5+
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
66
import type { Attributes } from '@utils/helpers';
77
import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers';
88
import { menuController } from '@utils/menu-controller';
@@ -788,7 +788,7 @@ export class Menu implements ComponentInterface, MenuI {
788788
*/
789789
return (
790790
<Host
791-
onKeyDown={shoudUseCloseWatcher() ? null : this.onKeydown}
791+
onKeyDown={shouldUseCloseWatcher() ? null : this.onKeydown}
792792
role="navigation"
793793
aria-label={inheritedAttributes['aria-label'] || 'menu'}
794794
class={{

core/src/utils/hardware-back-button.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface HandlerRegister {
3030
* moment this file is evaluated which could be
3131
* before the config is set.
3232
*/
33-
export const shoudUseCloseWatcher = () =>
33+
export const shouldUseCloseWatcher = () =>
3434
config.get('experimentalCloseWatcher', false) && win !== undefined && 'CloseWatcher' in win;
3535

3636
/**
@@ -109,7 +109,7 @@ export const startHardwareBackButton = () => {
109109
* backbutton event otherwise we may get duplicate
110110
* events firing.
111111
*/
112-
if (shoudUseCloseWatcher()) {
112+
if (shouldUseCloseWatcher()) {
113113
let watcher: CloseWatcher | undefined;
114114

115115
const configureWatcher = () => {

core/src/utils/overlays.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { doc } from '@utils/browser';
22
import type { BackButtonEvent } from '@utils/hardware-back-button';
3-
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
3+
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
44

55
import { config } from '../global/config';
66
import { getIonMode } from '../global/ionic-global';
@@ -428,7 +428,7 @@ const connectListeners = (doc: Document) => {
428428
* this behavior will be handled via the ionBackButton
429429
* event.
430430
*/
431-
if (!shoudUseCloseWatcher()) {
431+
if (!shouldUseCloseWatcher()) {
432432
doc.addEventListener('keydown', (ev) => {
433433
if (ev.key === 'Escape') {
434434
const lastOverlay = getPresentedOverlay(doc);
@@ -541,16 +541,7 @@ export const present = async <OverlayPresentOptions>(
541541
}
542542

543543
setRootAriaHidden(true);
544-
545-
/**
546-
* Hide all other overlays from screen readers so only this one
547-
* can be read. Note that presenting an overlay always makes
548-
* it the topmost one.
549-
*/
550-
if (doc !== undefined) {
551-
const presentedOverlays = getPresentedOverlays(doc);
552-
presentedOverlays.forEach((o) => o.setAttribute('aria-hidden', 'true'));
553-
}
544+
hideOverlaysFromScreenReaders(overlay.el);
554545

555546
overlay.presented = true;
556547
overlay.willPresent.emit();
@@ -723,13 +714,7 @@ export const dismiss = async <OverlayDismissOptions>(
723714

724715
overlay.el.remove();
725716

726-
/**
727-
* If there are other overlays presented, unhide the new
728-
* topmost one from screen readers.
729-
*/
730-
if (doc !== undefined) {
731-
getPresentedOverlay(doc)?.removeAttribute('aria-hidden');
732-
}
717+
revealOverlaysToScreenReaders();
733718

734719
return true;
735720
};
@@ -966,3 +951,65 @@ export const createTriggerController = () => {
966951
removeClickListener,
967952
};
968953
};
954+
955+
/**
956+
* Ensure that underlying overlays have aria-hidden if necessary so that screen readers
957+
* cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout
958+
* events here because those events do not fire when the screen readers moves to a non-focusable
959+
* element such as text.
960+
* Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
961+
*
962+
* @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been
963+
* fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result.
964+
*/
965+
const hideOverlaysFromScreenReaders = (newTopMostOverlay: HTMLIonOverlayElement) => {
966+
if (doc === undefined) return;
967+
968+
const overlays = getPresentedOverlays(doc);
969+
970+
for (let i = overlays.length - 1; i >= 0; i--) {
971+
const presentedOverlay = overlays[i];
972+
const nextPresentedOverlay = overlays[i + 1] ?? newTopMostOverlay;
973+
974+
/**
975+
* If next overlay has aria-hidden then all remaining overlays will have it too.
976+
* Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
977+
* should not have aria-hidden either so focus can remain in the current overlay.
978+
*/
979+
if (nextPresentedOverlay.hasAttribute('aria-hidden') || nextPresentedOverlay.tagName !== 'ION-TOAST') {
980+
presentedOverlay.setAttribute('aria-hidden', 'true');
981+
}
982+
}
983+
};
984+
985+
/**
986+
* When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
987+
* If the top-most overlay is a Toast we potentially need to reveal more overlays since
988+
* focus is never automatically moved to the Toast.
989+
*/
990+
const revealOverlaysToScreenReaders = () => {
991+
if (doc === undefined) return;
992+
993+
const overlays = getPresentedOverlays(doc);
994+
995+
for (let i = overlays.length - 1; i >= 0; i--) {
996+
const currentOverlay = overlays[i];
997+
998+
/**
999+
* If the current we are looking at is a Toast then we can remove aria-hidden.
1000+
* However, we potentially need to keep looking at the overlay stack because there
1001+
* could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
1002+
* overlay too so focus can move there since focus is never automatically moved to the Toast.
1003+
*/
1004+
currentOverlay.removeAttribute('aria-hidden');
1005+
1006+
/**
1007+
* If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
1008+
* since this overlay should always receive focus. As a result, all underlying overlays should still
1009+
* be hidden from screen readers.
1010+
*/
1011+
if (currentOverlay.tagName !== 'ION-TOAST') {
1012+
break;
1013+
}
1014+
}
1015+
};

core/src/utils/test/overlays/overlays.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { newSpecPage } from '@stencil/core/testing';
22

33
import { Modal } from '../../../components/modal/modal';
4+
import { Toast } from '../../../components/toast/toast';
45
import { Nav } from '../../../components/nav/nav';
56
import { RouterOutlet } from '../../../components/router-outlet/router-outlet';
67
import { setRootAriaHidden } from '../../overlays';
@@ -193,4 +194,70 @@ describe('aria-hidden on individual overlays', () => {
193194
await modalOne.present();
194195
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
195196
});
197+
198+
it('should not hide previous overlay if top-most overlay is toast', async () => {
199+
const page = await newSpecPage({
200+
components: [Modal, Toast],
201+
html: `
202+
<ion-modal id="m-one"></ion-modal>
203+
<ion-modal id="m-two"></ion-modal>
204+
<ion-toast id="t-one"></ion-toast>
205+
<ion-toast id="t-two"></ion-toast>
206+
`,
207+
});
208+
209+
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
210+
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
211+
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
212+
const toastTwo = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-two')!;
213+
214+
await modalOne.present();
215+
await modalTwo.present();
216+
await toastOne.present();
217+
await toastTwo.present();
218+
219+
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
220+
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
221+
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
222+
expect(toastTwo.hasAttribute('aria-hidden')).toEqual(false);
223+
224+
await toastTwo.dismiss();
225+
226+
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
227+
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
228+
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
229+
230+
await toastOne.dismiss();
231+
232+
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
233+
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
234+
});
235+
236+
it('should hide previous overlay even with a toast that is not the top-most overlay', async () => {
237+
const page = await newSpecPage({
238+
components: [Modal, Toast],
239+
html: `
240+
<ion-modal id="m-one"></ion-modal>
241+
<ion-toast id="t-one"></ion-toast>
242+
<ion-modal id="m-two"></ion-modal>
243+
`,
244+
});
245+
246+
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
247+
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
248+
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
249+
250+
await modalOne.present();
251+
await toastOne.present();
252+
await modalTwo.present();
253+
254+
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
255+
expect(toastOne.hasAttribute('aria-hidden')).toEqual(true);
256+
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
257+
258+
await modalTwo.dismiss();
259+
260+
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
261+
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
262+
});
196263
});

docs/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
7+
8+
**Note:** Version bump only for package @ionic/docs
9+
10+
11+
12+
13+
614
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
715

816
**Note:** Version bump only for package @ionic/docs

0 commit comments

Comments
 (0)