fix(radio): radios can be focused and are announced with group (#27817)

Issue number: resolves #27438

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

There are a few issues with the modern radio syntax:

1. The native radio is inside the Shadow DOM. As a result, radios are
not announced with their parent group with screen readers (i.e. "1 of
3")
2. The native radio cannot be focused inside of `ion-select-popover` on
Firefox.
3. The `ionFocus` and `ionBlur` events do not fire.

I also discovered an issue with item:

1. Items inside of a Radio Group have a role of `listitem` which prevent
radios from being grouped correctly in some browsers. According to
https://bugzilla.mozilla.org/show_bug.cgi?id=1840916, browsers are
behaving correctly here. The `listitem` role should not be present when
an item is used in a radio group (even if the radio group itself is
inside a list).

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Most of the changes are test-related, but I broke it down per commit to
make this easier to review:


ae77002afd

- Item no longer has `role="listitem"` when used inside of a radio
group.
- Added spec tests to verify the role behavior


0a9b7fb91d

- I discovered that some the legacy basic test were accidentally using
the modern syntax. I corrected this by adding `legacy="true"` to the
radios.


a8a90e53b2,
412d1d54e7,
and
1d1179b69a

- The current radio group tests only tested the legacy radio syntax, and
not the modern syntax.
- I created a `legacy` directory to house the legacy syntax tests.
- I created new tests in the root test directory for the modern syntax.
- I also deleted the screenshots for the modern tests here because the
tests for `ion-radio` already take screenshots of the radio (even in an
item).


e2c966e68b
- Moved radio roles to the host. This allows Firefox to focus radios and
for screen readers to announce the radios as part of a group.
- I also added focus/blur listeners so ionFocus and ionBlur fire


f10eff47a5

- I cleaned up the tests here to use a common radio fixture

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

I tested this with the following setups.  indicates the screen reader
announces the group count (i.e. "1 of 4").  indicates the screen reader
does not announce the group count.

**Radio in Radio Group:**
- iOS + VoiceOver: 
- Android + TalkBack: 
- macOS + VoiceOver + Safari: 
- macOS + VoiceOver + Firefox: 
- macOS + VoiceOver + Chrome: 
- Windows + NVDA + Chrome: 
- Windows + NVDA + Firefox: 

**Radio in Item in Radio Group :**
- iOS + VoiceOver: 
- Android + TalkBack: 
(https://bugs.chromium.org/p/chromium/issues/detail?id=1459006)
- macOS + VoiceOver + Safari: 
- macOS + VoiceOver + Firefox: 
- macOS + VoiceOver + Chrome: 
(https://bugs.chromium.org/p/chromium/issues/detail?id=1459003)
- Windows + NVDA + Chrome: 
- Windows + NVDA + Firefox: 
This commit is contained in:
Liam DeBeasi
2023-07-31 10:07:44 -04:00
committed by GitHub
parent a08a5894ba
commit ba2f49b8a4
29 changed files with 669 additions and 159 deletions

View File

@ -398,7 +398,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
});
const ariaDisabled = disabled || childStyles['item-interactive-disabled'] ? 'true' : null;
const fillValue = fill || 'none';
const inList = hostContext('ion-list', this.el);
const inList = hostContext('ion-list', this.el) && !hostContext('ion-radio-group', this.el);
return (
<Host

View File

@ -0,0 +1,51 @@
import { Radio } from '../../../radio/radio.tsx';
import { RadioGroup } from '../../../radio-group/radio-group.tsx';
import { Item } from '../../item.tsx';
import { List } from '../../../list/list.tsx';
import { newSpecPage } from '@stencil/core/testing';
describe('ion-item', () => {
it('should not have a role when used without list', async () => {
const page = await newSpecPage({
components: [Item],
html: `<ion-item>Hello World</ion-item>`,
});
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
it('should have a listitem role when used inside list', async () => {
const page = await newSpecPage({
components: [Item, List],
html: `
<ion-list>
<ion-item>
Hello World
</ion-item>
</ion-list>
`,
});
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe('listitem');
});
it('should not have a role when used inside radio group and list', async () => {
const page = await newSpecPage({
components: [Radio, RadioGroup, Item, List],
html: `
<ion-list>
<ion-radio-group value="a">
<ion-item>
<ion-radio value="other-value" aria-label="my radio"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
`,
});
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
});