mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 22:17:40 +08:00
fix(segment): nested interactive is not rendered (#26575)
This commit is contained in:
1
core/src/components.d.ts
vendored
1
core/src/components.d.ts
vendored
@ -2484,6 +2484,7 @@ export namespace Components {
|
|||||||
* The mode determines which platform styles to use.
|
* The mode determines which platform styles to use.
|
||||||
*/
|
*/
|
||||||
"mode"?: "ios" | "md";
|
"mode"?: "ios" | "md";
|
||||||
|
"setFocus": () => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* The type of the button.
|
* The type of the button.
|
||||||
*/
|
*/
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import type { ComponentInterface } from '@stencil/core';
|
import type { ComponentInterface } from '@stencil/core';
|
||||||
import { Component, Element, Host, Prop, State, forceUpdate, h } from '@stencil/core';
|
import { Component, Element, Host, Prop, Method, State, forceUpdate, h } from '@stencil/core';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import type { SegmentButtonLayout } from '../../interface';
|
import type { SegmentButtonLayout } from '../../interface';
|
||||||
import type { ButtonInterface } from '../../utils/element-interface';
|
import type { ButtonInterface } from '../../utils/element-interface';
|
||||||
import { addEventListener, removeEventListener } from '../../utils/helpers';
|
import type { Attributes } from '../../utils/helpers';
|
||||||
|
import { addEventListener, removeEventListener, inheritAttributes } from '../../utils/helpers';
|
||||||
import { hostContext } from '../../utils/theme';
|
import { hostContext } from '../../utils/theme';
|
||||||
|
|
||||||
let ids = 0;
|
let ids = 0;
|
||||||
@ -26,6 +27,8 @@ let ids = 0;
|
|||||||
})
|
})
|
||||||
export class SegmentButton implements ComponentInterface, ButtonInterface {
|
export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||||
private segmentEl: HTMLIonSegmentElement | null = null;
|
private segmentEl: HTMLIonSegmentElement | null = null;
|
||||||
|
private nativeEl: HTMLButtonElement | undefined;
|
||||||
|
private inheritedAttributes: Attributes = {};
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
@ -69,6 +72,12 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillLoad() {
|
||||||
|
this.inheritedAttributes = {
|
||||||
|
...inheritAttributes(this.el, ['aria-label']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private get hasLabel() {
|
private get hasLabel() {
|
||||||
return !!this.el.querySelector('ion-label');
|
return !!this.el.querySelector('ion-label');
|
||||||
}
|
}
|
||||||
@ -87,20 +96,26 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private get tabIndex() {
|
/**
|
||||||
return this.checked && !this.disabled ? 0 : -1;
|
* @internal
|
||||||
|
* Focuses the native <button> element
|
||||||
|
* inside of ion-segment-button.
|
||||||
|
*/
|
||||||
|
@Method()
|
||||||
|
async setFocus() {
|
||||||
|
const { nativeEl } = this;
|
||||||
|
|
||||||
|
if (nativeEl !== undefined) {
|
||||||
|
nativeEl.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl, tabIndex } = this;
|
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const hasSegmentColor = () => segmentEl?.color !== undefined;
|
const hasSegmentColor = () => segmentEl?.color !== undefined;
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
role="tab"
|
|
||||||
aria-selected={checked ? 'true' : 'false'}
|
|
||||||
aria-disabled={disabled ? 'true' : null}
|
|
||||||
tabIndex={tabIndex}
|
|
||||||
class={{
|
class={{
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-toolbar': hostContext('ion-toolbar', this.el),
|
'in-toolbar': hostContext('ion-toolbar', this.el),
|
||||||
@ -119,7 +134,16 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
'ion-focusable': true,
|
'ion-focusable': true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button type={type} tabIndex={-1} class="button-native" part="native" disabled={disabled}>
|
<button
|
||||||
|
aria-selected={checked ? 'true' : 'false'}
|
||||||
|
role="tab"
|
||||||
|
ref={(el) => (this.nativeEl = el)}
|
||||||
|
type={type}
|
||||||
|
class="button-native"
|
||||||
|
part="native"
|
||||||
|
disabled={disabled}
|
||||||
|
{...this.inheritedAttributes}
|
||||||
|
>
|
||||||
<span class="button-inner">
|
<span class="button-inner">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
|
@ -165,7 +165,6 @@ export class Segment implements ComponentInterface {
|
|||||||
|
|
||||||
async componentDidLoad() {
|
async componentDidLoad() {
|
||||||
this.setCheckedClasses();
|
this.setCheckedClasses();
|
||||||
this.ensureFocusable();
|
|
||||||
|
|
||||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||||
el: this.el,
|
el: this.el,
|
||||||
@ -519,19 +518,7 @@ export class Segment implements ComponentInterface {
|
|||||||
const previous = this.checked || current;
|
const previous = this.checked || current;
|
||||||
this.checkButton(previous, current);
|
this.checkButton(previous, current);
|
||||||
}
|
}
|
||||||
current.focus();
|
current.setFocus();
|
||||||
}
|
|
||||||
|
|
||||||
/* By default, focus is delegated to the selected `ion-segment-button`.
|
|
||||||
* If there is no selected button, focus will instead pass to the first child button.
|
|
||||||
**/
|
|
||||||
private ensureFocusable() {
|
|
||||||
if (this.value !== undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons = this.getButtons();
|
|
||||||
buttons[0]?.setAttribute('tabindex', '0');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -26,6 +26,20 @@
|
|||||||
<ion-label>Shared Links</ion-label>
|
<ion-label>Shared Links</ion-label>
|
||||||
</ion-segment-button>
|
</ion-segment-button>
|
||||||
</ion-segment>
|
</ion-segment>
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
<ion-segment aria-label="Tab Options" color="dark" select-on-focus>
|
||||||
|
<ion-segment-button aria-label="Bookmarks" value="bookmarks">
|
||||||
|
<ion-icon aria-hidden="true" name="bookmark"></ion-icon>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button aria-label="Reading List" value="reading-list">
|
||||||
|
<ion-icon aria-hidden="true" name="glasses"></ion-icon>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button aria-label="Shared Links" value="shared-links">
|
||||||
|
<ion-icon aria-hidden="true" name="link"></ion-icon>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"native" refers to this sample: https://w3c.github.io/aria-practices/examples/tabs/tabs-2/tabs.html
|
"native" refers to this sample: https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual
|
||||||
|
|
||||||
### Tabbing to Segment Button
|
### Tabbing to Segment Button
|
||||||
|
|
||||||
| | native | Ionic |
|
| | native | Ionic |
|
||||||
| ------------------------ | ------------------------------------------------ | ------------------------------------------------ |
|
| ------------------------ | ------------------------------------------------ | ------------------------------------------------ |
|
||||||
| VoiceOver macOS - Chrome | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
| VoiceOver macOS - Chrome | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 1, Tab Options, tab group |
|
||||||
| VoiceOver macOS - Safari | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
| VoiceOver macOS - Safari | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
||||||
| VoiceOver iOS | Bookmarks, tab | Bookmarks, tab |
|
| VoiceOver iOS | Bookmarks, tab, 1 of 3 | Bookmarks, tab, 1 of 3 |
|
||||||
| Android TalkBack | Bookmarks, tab | Bookmarks, tab |
|
| Android TalkBack | Bookmarks, tab | Bookmarks, tab |
|
||||||
| Windows NVDA | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 |
|
| Windows NVDA | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 |
|
||||||
|
|
||||||
@ -14,11 +14,12 @@
|
|||||||
|
|
||||||
| | native | Ionic |
|
| | native | Ionic |
|
||||||
| ------------------------ | -------------------------------------------------------- | ------------------------ |
|
| ------------------------ | -------------------------------------------------------- | ------------------------ |
|
||||||
| VoiceOver macOS - Chrome | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
| VoiceOver macOS - Chrome | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 1, Tab Options, tab group |
|
||||||
| VoiceOver macOS - Safari | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
| VoiceOver macOS - Safari | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
||||||
| VoiceOver iOS | selected, Bookmarks, tab | selected, Bookmarks, tab |
|
| VoiceOver iOS | selected, Bookmarks, tab, 1 of 3 | selected, Bookmarks, tab, 1 of 3 |
|
||||||
| Android TalkBack | selected | selected |
|
| Android TalkBack | selected | selected |
|
||||||
| Windows NVDA | BOOKMARKS, tab, 1 of 3, selected | BOOKMARKS, tab, 1 of 3, selected |
|
| Windows NVDA | BOOKMARKS, tab, 1 of 3, selected | BOOKMARKS, tab, 1 of 3, selected |
|
||||||
|
|
||||||
Note: The `aria-label` for tablist is typically only read on the first interaction.
|
Note: The `aria-label` for tablist is typically only read on the first interaction.
|
||||||
|
|
||||||
|
VoiceOver + Chrome announces the wrong tab count due to the `role="tab"` being in the Shadow DOM: https://bugs.chromium.org/p/chromium/issues/detail?id=1405462
|
||||||
|
@ -6,8 +6,7 @@ test.describe('segment: a11y', () => {
|
|||||||
test('should not have any axe violations', async ({ page }) => {
|
test('should not have any axe violations', async ({ page }) => {
|
||||||
await page.goto('/src/components/segment/test/a11y');
|
await page.goto('/src/components/segment/test/a11y');
|
||||||
|
|
||||||
// TODO(FW-403): Re-enable rule once segment button is updated to avoid nested-interactive
|
const results = await new AxeBuilder({ page }).analyze();
|
||||||
const results = await new AxeBuilder({ page }).disableRules('nested-interactive').analyze();
|
|
||||||
expect(results.violations).toEqual([]);
|
expect(results.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user