Skip to content

Commit c10776b

Browse files
committed
Merge branch 'main' of github.com:ionic-team/ionic-framework into ROU-11380
2 parents 5f69f6a + f6188c4 commit c10776b

File tree

6 files changed

+65
-15
lines changed

6 files changed

+65
-15
lines changed

core/src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,6 +2303,7 @@ export namespace Components {
23032303
* The name of the control, which is submitted with the form data.
23042304
*/
23052305
"name": string;
2306+
"setFocus": () => Promise<void>;
23062307
/**
23072308
* the value of the radio group.
23082309
*/

core/src/components/header/header.utils.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,34 @@ export const handleToolbarIntersection = (
167167

168168
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
169169
const headerEl = headerIndex.el;
170+
const toolbars = headerIndex.toolbars;
171+
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
170172

171173
if (active) {
172174
headerEl.classList.remove('header-collapse-condense-inactive');
173-
headerEl.removeAttribute('aria-hidden');
175+
176+
ionTitles.forEach((ionTitle) => {
177+
if (ionTitle) {
178+
ionTitle.removeAttribute('aria-hidden');
179+
}
180+
});
174181
} else {
175182
headerEl.classList.add('header-collapse-condense-inactive');
176-
headerEl.setAttribute('aria-hidden', 'true');
183+
184+
/**
185+
* The small title should only be accessed by screen readers
186+
* when the large title collapses into the small title due
187+
* to scrolling.
188+
*
189+
* Originally, the header was given `aria-hidden="true"`
190+
* but this caused issues with screen readers not being
191+
* able to access any focusable elements within the header.
192+
*/
193+
ionTitles.forEach((ionTitle) => {
194+
if (ionTitle) {
195+
ionTitle.setAttribute('aria-hidden', 'true');
196+
}
197+
});
177198
}
178199
};
179200

core/src/components/header/test/condense/header.e2e.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import { configs, test } from '@utils/test/playwright';
33

44
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
55
test.describe(title('header: condense'), () => {
6-
test('should be hidden from screen readers when collapsed', async ({ page }) => {
6+
test('should hide small title from screen readers when collapsed', async ({ page }) => {
7+
test.info().annotations.push({
8+
type: 'issue',
9+
description: 'https://github.com/ionic-team/ionic-framework/issues/29347',
10+
});
11+
712
await page.goto('/src/components/header/test/condense', config);
813
const largeTitleHeader = page.locator('#largeTitleHeader');
914
const smallTitleHeader = page.locator('#smallTitleHeader');
15+
const smallTitle = smallTitleHeader.locator('ion-title');
1016
const content = page.locator('ion-content');
1117

12-
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
18+
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
1319

1420
await expect(largeTitleHeader).toHaveScreenshot(screenshot(`header-condense-large-title-initial-diff`));
1521

@@ -24,15 +30,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
2430
* Playwright can't do .not.toHaveAttribute() because a value is expected,
2531
* and toHaveAttribute can't accept a value of type null.
2632
*/
27-
const ariaHidden = await smallTitleHeader.getAttribute('aria-hidden');
33+
const ariaHidden = await smallTitle.getAttribute('aria-hidden');
2834
expect(ariaHidden).toBeNull();
2935

3036
await content.evaluate(async (el: HTMLIonContentElement) => {
3137
await el.scrollToTop();
3238
});
3339
await page.locator('#smallTitleHeader.header-collapse-condense-inactive').waitFor();
3440

35-
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
41+
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
3642
});
3743
});
3844
});

core/src/components/radio-group/radio-group.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
2+
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
33
import { renderHiddenInput } from '@utils/helpers';
44

55
import { getIonMode } from '../../global/ionic-global';
@@ -217,6 +217,13 @@ export class RadioGroup implements ComponentInterface {
217217
}
218218
}
219219

220+
/** @internal */
221+
@Method()
222+
async setFocus() {
223+
const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
224+
radioToFocus?.setFocus();
225+
}
226+
220227
render() {
221228
const { label, labelId, el, name, value } = this;
222229
const mode = getIonMode(this);

core/src/utils/focus-trap.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
1313
* valid usage for the disabled property on ion-button.
1414
*/
1515
export const focusableQueryString =
16-
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
16+
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
1717

1818
/**
1919
* Focuses the first descendant in a context
@@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
7878
}
7979

8080
if (elementToFocus) {
81-
focusVisibleElement(elementToFocus);
81+
const radioGroup = elementToFocus.closest('ion-radio-group');
82+
83+
if (radioGroup) {
84+
radioGroup.setFocus();
85+
} else {
86+
focusVisibleElement(elementToFocus);
87+
}
8288
} else {
8389
// Focus fallback element instead of letting focus escape
8490
fallbackElement.focus();

core/src/utils/overlays.ts

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

3637
let lastOverlayIndex = 0;
3738
let lastId = 0;
@@ -973,18 +974,26 @@ export const createTriggerController = () => {
973974
* If the overlay is being presented, it prevents focus rings from appearing
974975
* in incorrect positions due to the transition (specifically `transform`
975976
* styles), ensuring that when aria-hidden is removed, the focus rings are
976-
* correctly displayed in the final location of the elements.
977+
* correctly displayed in the final location of the elements. This only
978+
* applies to Android devices.
979+
*
980+
* If this solution is applied to iOS devices, then it leads to a bug where
981+
* the overlays cannot be accessed by screen readers. This is due to
982+
* VoiceOver not being able to update the accessibility tree when the
983+
* `aria-hidden` is removed.
977984
*
978985
* @param overlay - The overlay that is being animated.
979986
*/
980987
const hideAnimatingOverlayFromScreenReaders = (overlay: HTMLIonOverlayElement) => {
981988
if (doc === undefined) return;
982989

983-
/**
984-
* Once the animation is complete, this attribute will be removed.
985-
* This is done at the end of the `present` method.
986-
*/
987-
overlay.setAttribute('aria-hidden', 'true');
990+
if (isPlatform('android')) {
991+
/**
992+
* Once the animation is complete, this attribute will be removed.
993+
* This is done at the end of the `present` method.
994+
*/
995+
overlay.setAttribute('aria-hidden', 'true');
996+
}
988997
};
989998

990999
/**

0 commit comments

Comments
 (0)