mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-26 08:13:34 +08:00
chore: sync with main
This commit is contained in:
@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
|
||||
|
||||
|
||||
|
4
core/package-lock.json
generated
4
core/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.7.4",
|
||||
"version": "7.7.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "7.7.4",
|
||||
"version": "7.7.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.12.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.7.4",
|
||||
"version": "7.7.5",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
|
||||
import type { FocusVisibleUtility } from '@utils/focus-visible';
|
||||
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
|
||||
@ -36,7 +36,7 @@ export class App implements ComponentInterface {
|
||||
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
|
||||
}
|
||||
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
|
||||
const supportsHardwareBackButtonEvents = isHybrid || shoudUseCloseWatcher();
|
||||
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
|
||||
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
|
||||
hardwareBackButtonModule.startHardwareBackButton();
|
||||
} else {
|
||||
@ -44,7 +44,7 @@ export class App implements ComponentInterface {
|
||||
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
|
||||
* then the close watcher will not be used.
|
||||
*/
|
||||
if (shoudUseCloseWatcher()) {
|
||||
if (shouldUseCloseWatcher()) {
|
||||
printIonWarning(
|
||||
'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.'
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
|
||||
import { GESTURE_CONTROLLER } from '@utils/gesture';
|
||||
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers';
|
||||
import { menuController } from '@utils/menu-controller';
|
||||
@ -788,7 +788,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
*/
|
||||
return (
|
||||
<Host
|
||||
onKeyDown={shoudUseCloseWatcher() ? null : this.onKeydown}
|
||||
onKeyDown={shouldUseCloseWatcher() ? null : this.onKeydown}
|
||||
role="navigation"
|
||||
aria-label={inheritedAttributes['aria-label'] || 'menu'}
|
||||
class={{
|
||||
|
@ -30,7 +30,7 @@ interface HandlerRegister {
|
||||
* moment this file is evaluated which could be
|
||||
* before the config is set.
|
||||
*/
|
||||
export const shoudUseCloseWatcher = () =>
|
||||
export const shouldUseCloseWatcher = () =>
|
||||
config.get('experimentalCloseWatcher', false) && win !== undefined && 'CloseWatcher' in win;
|
||||
|
||||
/**
|
||||
@ -109,7 +109,7 @@ export const startHardwareBackButton = () => {
|
||||
* backbutton event otherwise we may get duplicate
|
||||
* events firing.
|
||||
*/
|
||||
if (shoudUseCloseWatcher()) {
|
||||
if (shouldUseCloseWatcher()) {
|
||||
let watcher: CloseWatcher | undefined;
|
||||
|
||||
const configureWatcher = () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { doc } from '@utils/browser';
|
||||
import type { BackButtonEvent } from '@utils/hardware-back-button';
|
||||
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
|
||||
import { config } from '../global/config';
|
||||
import { getIonMode } from '../global/ionic-global';
|
||||
@ -428,7 +428,7 @@ const connectListeners = (doc: Document) => {
|
||||
* this behavior will be handled via the ionBackButton
|
||||
* event.
|
||||
*/
|
||||
if (!shoudUseCloseWatcher()) {
|
||||
if (!shouldUseCloseWatcher()) {
|
||||
doc.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape') {
|
||||
const lastOverlay = getPresentedOverlay(doc);
|
||||
@ -541,16 +541,7 @@ export const present = async <OverlayPresentOptions>(
|
||||
}
|
||||
|
||||
setRootAriaHidden(true);
|
||||
|
||||
/**
|
||||
* Hide all other overlays from screen readers so only this one
|
||||
* can be read. Note that presenting an overlay always makes
|
||||
* it the topmost one.
|
||||
*/
|
||||
if (doc !== undefined) {
|
||||
const presentedOverlays = getPresentedOverlays(doc);
|
||||
presentedOverlays.forEach((o) => o.setAttribute('aria-hidden', 'true'));
|
||||
}
|
||||
hideOverlaysFromScreenReaders(overlay.el);
|
||||
|
||||
overlay.presented = true;
|
||||
overlay.willPresent.emit();
|
||||
@ -723,13 +714,7 @@ export const dismiss = async <OverlayDismissOptions>(
|
||||
|
||||
overlay.el.remove();
|
||||
|
||||
/**
|
||||
* If there are other overlays presented, unhide the new
|
||||
* topmost one from screen readers.
|
||||
*/
|
||||
if (doc !== undefined) {
|
||||
getPresentedOverlay(doc)?.removeAttribute('aria-hidden');
|
||||
}
|
||||
revealOverlaysToScreenReaders();
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -966,3 +951,65 @@ export const createTriggerController = () => {
|
||||
removeClickListener,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* events here because those events do not fire when the screen readers moves to a non-focusable
|
||||
* element such as text.
|
||||
* Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
|
||||
*
|
||||
* @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) => {
|
||||
if (doc === undefined) return;
|
||||
|
||||
const overlays = getPresentedOverlays(doc);
|
||||
|
||||
for (let i = overlays.length - 1; i >= 0; i--) {
|
||||
const presentedOverlay = overlays[i];
|
||||
const nextPresentedOverlay = overlays[i + 1] ?? newTopMostOverlay;
|
||||
|
||||
/**
|
||||
* If next overlay has aria-hidden then all remaining overlays will have it too.
|
||||
* Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
|
||||
* should not have aria-hidden either so focus can remain in the current overlay.
|
||||
*/
|
||||
if (nextPresentedOverlay.hasAttribute('aria-hidden') || nextPresentedOverlay.tagName !== 'ION-TOAST') {
|
||||
presentedOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
|
||||
* If the top-most overlay is a Toast we potentially need to reveal more overlays since
|
||||
* focus is never automatically moved to the Toast.
|
||||
*/
|
||||
const revealOverlaysToScreenReaders = () => {
|
||||
if (doc === undefined) return;
|
||||
|
||||
const overlays = getPresentedOverlays(doc);
|
||||
|
||||
for (let i = overlays.length - 1; i >= 0; i--) {
|
||||
const currentOverlay = overlays[i];
|
||||
|
||||
/**
|
||||
* If the current we are looking at is a Toast then we can remove aria-hidden.
|
||||
* However, we potentially need to keep looking at the overlay stack because there
|
||||
* could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
|
||||
* overlay too so focus can move there since focus is never automatically moved to the Toast.
|
||||
*/
|
||||
currentOverlay.removeAttribute('aria-hidden');
|
||||
|
||||
/**
|
||||
* If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
|
||||
* since this overlay should always receive focus. As a result, all underlying overlays should still
|
||||
* be hidden from screen readers.
|
||||
*/
|
||||
if (currentOverlay.tagName !== 'ION-TOAST') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Modal } from '../../../components/modal/modal';
|
||||
import { Toast } from '../../../components/toast/toast';
|
||||
import { Nav } from '../../../components/nav/nav';
|
||||
import { RouterOutlet } from '../../../components/router-outlet/router-outlet';
|
||||
import { setRootAriaHidden } from '../../overlays';
|
||||
@ -193,4 +194,70 @@ describe('aria-hidden on individual overlays', () => {
|
||||
await modalOne.present();
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not hide previous overlay if top-most overlay is toast', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal, Toast],
|
||||
html: `
|
||||
<ion-modal id="m-one"></ion-modal>
|
||||
<ion-modal id="m-two"></ion-modal>
|
||||
<ion-toast id="t-one"></ion-toast>
|
||||
<ion-toast id="t-two"></ion-toast>
|
||||
`,
|
||||
});
|
||||
|
||||
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
|
||||
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
|
||||
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
|
||||
const toastTwo = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-two')!;
|
||||
|
||||
await modalOne.present();
|
||||
await modalTwo.present();
|
||||
await toastOne.present();
|
||||
await toastTwo.present();
|
||||
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(toastTwo.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
await toastTwo.dismiss();
|
||||
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
await toastOne.dismiss();
|
||||
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should hide previous overlay even with a toast that is not the top-most overlay', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal, Toast],
|
||||
html: `
|
||||
<ion-modal id="m-one"></ion-modal>
|
||||
<ion-toast id="t-one"></ion-toast>
|
||||
<ion-modal id="m-two"></ion-modal>
|
||||
`,
|
||||
});
|
||||
|
||||
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
|
||||
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
|
||||
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
|
||||
|
||||
await modalOne.present();
|
||||
await toastOne.present();
|
||||
await modalTwo.present();
|
||||
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(toastOne.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
await modalTwo.dismiss();
|
||||
|
||||
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user