mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d2a5d2803 | ||
|
|
61d68d0d87 | ||
|
|
9f772f4ea7 | ||
|
|
e7b53047be | ||
|
|
ad7a02d613 | ||
|
|
aaebad516a | ||
|
|
f6188c47e9 | ||
|
|
8ee42bbc1e | ||
|
|
23763abf79 | ||
|
|
470decca7b | ||
|
|
3216108ca1 |
1
core/src/components.d.ts
vendored
1
core/src/components.d.ts
vendored
@@ -2303,6 +2303,7 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
|
||||
@@ -167,13 +167,34 @@ export const handleToolbarIntersection = (
|
||||
|
||||
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
|
||||
const headerEl = headerIndex.el;
|
||||
const toolbars = headerIndex.toolbars;
|
||||
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
|
||||
|
||||
if (active) {
|
||||
headerEl.classList.remove('header-collapse-condense-inactive');
|
||||
headerEl.removeAttribute('aria-hidden');
|
||||
|
||||
ionTitles.forEach((ionTitle) => {
|
||||
if (ionTitle) {
|
||||
ionTitle.removeAttribute('aria-hidden');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
headerEl.classList.add('header-collapse-condense-inactive');
|
||||
headerEl.setAttribute('aria-hidden', 'true');
|
||||
|
||||
/**
|
||||
* The small title should only be accessed by screen readers
|
||||
* when the large title collapses into the small title due
|
||||
* to scrolling.
|
||||
*
|
||||
* Originally, the header was given `aria-hidden="true"`
|
||||
* but this caused issues with screen readers not being
|
||||
* able to access any focusable elements within the header.
|
||||
*/
|
||||
ionTitles.forEach((ionTitle) => {
|
||||
if (ionTitle) {
|
||||
ionTitle.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@ import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('header: condense'), () => {
|
||||
test('should be hidden from screen readers when collapsed', async ({ page }) => {
|
||||
test('should hide small title from screen readers when collapsed', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/29347',
|
||||
});
|
||||
|
||||
await page.goto('/src/components/header/test/condense', config);
|
||||
const largeTitleHeader = page.locator('#largeTitleHeader');
|
||||
const smallTitleHeader = page.locator('#smallTitleHeader');
|
||||
const smallTitle = smallTitleHeader.locator('ion-title');
|
||||
const content = page.locator('ion-content');
|
||||
|
||||
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
await expect(largeTitleHeader).toHaveScreenshot(screenshot(`header-condense-large-title-initial-diff`));
|
||||
|
||||
@@ -24,7 +30,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
|
||||
* Playwright can't do .not.toHaveAttribute() because a value is expected,
|
||||
* and toHaveAttribute can't accept a value of type null.
|
||||
*/
|
||||
const ariaHidden = await smallTitleHeader.getAttribute('aria-hidden');
|
||||
const ariaHidden = await smallTitle.getAttribute('aria-hidden');
|
||||
expect(ariaHidden).toBeNull();
|
||||
|
||||
await content.evaluate(async (el: HTMLIonContentElement) => {
|
||||
@@ -32,7 +38,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
|
||||
});
|
||||
await page.locator('#smallTitleHeader.header-collapse-condense-inactive').waitFor();
|
||||
|
||||
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
|
||||
import { renderHiddenInput } from '@utils/helpers';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -217,6 +217,13 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
|
||||
radioToFocus?.setFocus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, labelId, el, name, value } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { configs, test, dragElementBy } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
@@ -105,8 +105,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
// TODO FW-3021
|
||||
test.describe.skip('when the pointer is released', () => {
|
||||
test.describe('when the pointer is released', () => {
|
||||
test('should emit if the value has changed', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
@@ -136,14 +135,22 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const firstButton = page.locator('ion-segment-button[value="1"]');
|
||||
const lastButton = page.locator('ion-segment-button[value="3"]');
|
||||
|
||||
await firstButton.hover();
|
||||
await page.mouse.down();
|
||||
/*
|
||||
* `dragByX` should represent the total width of all segment buttons,
|
||||
* excluding the first half of the first button and the second half
|
||||
* of the last button. This calculation accounts for dragging from
|
||||
* the center of the first button to the center of the last button.
|
||||
*/
|
||||
const segmentWidth = await segment.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const firstButtonWidth = await firstButton.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const lastButtonWidth = await lastButton.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const dragByX = segmentWidth - firstButtonWidth / 2 - lastButtonWidth / 2;
|
||||
|
||||
await lastButton.hover();
|
||||
await page.mouse.up();
|
||||
await dragElementBy(firstButton, page, dragByX);
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '3' });
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
background: var(--background);
|
||||
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-top {
|
||||
@@ -115,7 +117,6 @@
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
|
||||
height: inherit;
|
||||
min-height: inherit;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
|
||||
* valid usage for the disabled property on ion-button.
|
||||
*/
|
||||
export const focusableQueryString =
|
||||
'[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])';
|
||||
'[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])';
|
||||
|
||||
/**
|
||||
* Focuses the first descendant in a context
|
||||
@@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
|
||||
}
|
||||
|
||||
if (elementToFocus) {
|
||||
focusVisibleElement(elementToFocus);
|
||||
const radioGroup = elementToFocus.closest('ion-radio-group');
|
||||
|
||||
if (radioGroup) {
|
||||
radioGroup.setFocus();
|
||||
} else {
|
||||
focusVisibleElement(elementToFocus);
|
||||
}
|
||||
} else {
|
||||
// Focus fallback element instead of letting focus escape
|
||||
fallbackElement.focus();
|
||||
|
||||
@@ -13,6 +13,7 @@ const FOCUS_KEYS = [
|
||||
'ArrowUp',
|
||||
'Home',
|
||||
'End',
|
||||
'Meta',
|
||||
];
|
||||
|
||||
export interface FocusVisibleUtility {
|
||||
@@ -28,24 +29,34 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility =>
|
||||
const root = rootEl ? rootEl : document.body;
|
||||
|
||||
const setFocus = (elements: Element[]) => {
|
||||
console.log('setFocus', elements, currentFocus);
|
||||
console.log('keyboardMode', keyboardMode);
|
||||
currentFocus.forEach((el) => el.classList.remove(ION_FOCUSED));
|
||||
elements.forEach((el) => el.classList.add(ION_FOCUSED));
|
||||
currentFocus = elements;
|
||||
};
|
||||
const pointerDown = () => {
|
||||
// SPACE triggers on iOS device with physical keyboard
|
||||
const pointerDown = (ev: Event) => {
|
||||
console.log('pointerDown', ev);
|
||||
console.log('keyboardMode', keyboardMode);
|
||||
keyboardMode = false;
|
||||
setFocus([]);
|
||||
};
|
||||
|
||||
// Enter triggers on iOS device with physical keyboard
|
||||
const onKeydown = (ev: Event) => {
|
||||
console.log('onKeydown', ev);
|
||||
console.log('keyboardMode', keyboardMode);
|
||||
keyboardMode = FOCUS_KEYS.includes((ev as KeyboardEvent).key);
|
||||
if (!keyboardMode) {
|
||||
setFocus([]);
|
||||
}
|
||||
};
|
||||
const onFocusin = (ev: Event) => {
|
||||
console.log('onFocusin', ev);
|
||||
console.log('keyboardMode', keyboardMode);
|
||||
let toFocus: Element[] = [];
|
||||
if (keyboardMode && ev.composedPath !== undefined) {
|
||||
const toFocus = ev.composedPath().filter((el: any) => {
|
||||
toFocus = ev.composedPath().filter((el: any) => {
|
||||
// TODO(FW-2832): type
|
||||
if (el.classList) {
|
||||
return el.classList.contains(ION_FOCUSABLE);
|
||||
@@ -54,8 +65,12 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility =>
|
||||
}) as Element[];
|
||||
setFocus(toFocus);
|
||||
}
|
||||
console.log('toFocus', toFocus);
|
||||
keyboardMode = true;
|
||||
};
|
||||
const onFocusout = () => {
|
||||
console.log('onFocusout');
|
||||
console.log('keyboardMode', keyboardMode);
|
||||
if (ref.activeElement === root) {
|
||||
setFocus([]);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
removeEventListener,
|
||||
} from './helpers';
|
||||
import { printIonWarning } from './logging';
|
||||
import { isPlatform } from './platform';
|
||||
|
||||
let lastOverlayIndex = 0;
|
||||
let lastId = 0;
|
||||
@@ -973,18 +974,26 @@ export const createTriggerController = () => {
|
||||
* 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.
|
||||
* correctly displayed in the final location of the elements. This only
|
||||
* applies to Android devices.
|
||||
*
|
||||
* If this solution is applied to iOS devices, then it leads to a bug where
|
||||
* the overlays cannot be accessed by screen readers. This is due to
|
||||
* VoiceOver not being able to update the accessibility tree when the
|
||||
* `aria-hidden` is removed.
|
||||
*
|
||||
* @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');
|
||||
if (isPlatform('android')) {
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user