diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index 4e06b0ec52..6e2d48186d 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -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 ( { + it('should not have a role when used without list', async () => { + const page = await newSpecPage({ + components: [Item], + html: `Hello World`, + }); + + 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: ` + + + Hello World + + + `, + }); + + 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: ` + + + + + + + + `, + }); + + const item = page.body.querySelector('ion-item'); + expect(item.getAttribute('role')).toBe(null); + }); +}); diff --git a/core/src/components/radio-group/test/basic/index.html b/core/src/components/radio-group/test/basic/index.html index 5b690307e3..650695cd2b 100644 --- a/core/src/components/radio-group/test/basic/index.html +++ b/core/src/components/radio-group/test/basic/index.html @@ -30,23 +30,19 @@ - Item 1 - + Item 1 - Item 2 - + Item 2 - Item 3 - + Item 3 - Item 4 - + Item 4 diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts b/core/src/components/radio-group/test/basic/radio-group.e2e.ts index 06d6dbad14..96b7b5d955 100644 --- a/core/src/components/radio-group/test/basic/radio-group.e2e.ts +++ b/core/src/components/radio-group/test/basic/radio-group.e2e.ts @@ -1,19 +1,7 @@ import { expect } from '@playwright/test'; -import type { Locator } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; -import type { E2EPage } from '@utils/test/playwright'; -configs().forEach(({ title, screenshot, config }) => { - test.describe(title('radio-group: basic'), () => { - test('should not have visual regressions', async ({ page }) => { - await page.goto(`/src/components/radio-group/test/basic`, config); - - const list = page.locator('ion-list'); - - await expect(list).toHaveScreenshot(screenshot(`radio-group-diff`)); - }); - }); -}); +import { RadioFixture } from '../fixtures'; /** * This behavior does not vary across modes/directions. @@ -31,8 +19,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => ` - One - + One `, @@ -48,8 +35,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => ` - One - + One `, @@ -65,8 +51,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => ` - One - + One `, @@ -82,8 +67,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => ` - One - + One `, @@ -99,18 +83,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => ` - Item 1 - + Item 1 - Item 2 - + Item 2 - Item 3 - + Item 3 `, @@ -130,34 +111,3 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => }); }); }); - -class RadioFixture { - readonly page: E2EPage; - - private radio!: Locator; - - constructor(page: E2EPage) { - this.page = page; - } - - async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') { - const { page } = this; - const radio = (this.radio = page.locator(selector)); - - if (method === 'keyboard') { - await radio.focus(); - await page.keyboard.press('Space'); - } else { - await radio.click(); - } - - await page.waitForChanges(); - - return radio; - } - - async expectChecked(state: boolean) { - const { radio } = this; - await expect(radio.locator('input')).toHaveJSProperty('checked', state); - } -} diff --git a/core/src/components/radio-group/test/fixtures.ts b/core/src/components/radio-group/test/fixtures.ts new file mode 100644 index 0000000000..21722126d7 --- /dev/null +++ b/core/src/components/radio-group/test/fixtures.ts @@ -0,0 +1,39 @@ +import type { Locator } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { E2EPage } from '@utils/test/playwright'; + +export class RadioFixture { + readonly page: E2EPage; + + private radio!: Locator; + + constructor(page: E2EPage) { + this.page = page; + } + + async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') { + const { page } = this; + const radio = (this.radio = page.locator(selector)); + + if (method === 'keyboard') { + await radio.focus(); + await page.keyboard.press('Space'); + } else { + await radio.click(); + } + + await page.waitForChanges(); + + return radio; + } + + async expectChecked(state: boolean) { + const { radio } = this; + + if (state) { + await expect(radio).toHaveClass(/radio-checked/); + } else { + await expect(radio).not.toHaveClass(/radio-checked/); + } + } +} diff --git a/core/src/components/radio-group/test/form/index.html b/core/src/components/radio-group/test/form/index.html index 159f8c6249..6cbe354b4c 100644 --- a/core/src/components/radio-group/test/form/index.html +++ b/core/src/components/radio-group/test/form/index.html @@ -31,32 +31,19 @@ - Biff - - - + Biff - Griff - - - + Griff - Buford - - - + Buford - George - + George diff --git a/core/src/components/radio-group/test/legacy/basic/index.html b/core/src/components/radio-group/test/legacy/basic/index.html new file mode 100644 index 0000000000..5b690307e3 --- /dev/null +++ b/core/src/components/radio-group/test/legacy/basic/index.html @@ -0,0 +1,56 @@ + + + + + Radio Group - Basic + + + + + + + + + + + + + Radio Group - Basic + + + + + + + + Radio Group Header + + + + Item 1 + + + + + Item 2 + + + + + Item 3 + + + + + Item 4 + + + + + + + + diff --git a/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts new file mode 100644 index 0000000000..470ce52bcc --- /dev/null +++ b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts @@ -0,0 +1,163 @@ +import { expect } from '@playwright/test'; +import type { Locator } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; +import type { E2EPage } from '@utils/test/playwright'; + +configs().forEach(({ title, screenshot, config }) => { + test.describe(title('radio-group: basic'), () => { + test('should not have visual regressions', async ({ page }) => { + await page.goto(`/src/components/radio-group/test/legacy/basic`, config); + + const list = page.locator('ion-list'); + + await expect(list).toHaveScreenshot(screenshot(`radio-group-diff`)); + }); + }); +}); + +/** + * This behavior does not vary across modes/directions. + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('radio-group: interaction'), () => { + let radioFixture: RadioFixture; + + test.beforeEach(({ page }) => { + radioFixture = new RadioFixture(page); + }); + + test('spacebar should not deselect without allowEmptySelection', async ({ page }) => { + await page.setContent( + ` + + + One + + + + `, + config + ); + + await radioFixture.checkRadio('keyboard'); + await radioFixture.expectChecked(true); + }); + + test('spacebar should deselect with allowEmptySelection', async ({ page }) => { + await page.setContent( + ` + + + One + + + + `, + config + ); + + await radioFixture.checkRadio('keyboard'); + await radioFixture.expectChecked(false); + }); + + test('click should not deselect without allowEmptySelection', async ({ page }) => { + await page.setContent( + ` + + + One + + + + `, + config + ); + + await radioFixture.checkRadio('mouse'); + await radioFixture.expectChecked(true); + }); + + test('click should deselect with allowEmptySelection', async ({ page }) => { + await page.setContent( + ` + + + One + + + + `, + config + ); + + await radioFixture.checkRadio('mouse'); + await radioFixture.expectChecked(false); + }); + + test('programmatically assigning a value should update the checked radio', async ({ page }) => { + await page.setContent( + ` + + + Item 1 + + + + + Item 2 + + + + + Item 3 + + + + `, + config + ); + + const radioGroup = page.locator('ion-radio-group'); + const radioOne = page.locator('ion-radio[value="1"]'); + const radioTwo = page.locator('ion-radio[value="2"]'); + + await radioGroup.evaluate((el: HTMLIonRadioGroupElement) => (el.value = '2')); + + await page.waitForChanges(); + + await expect(radioOne).not.toHaveClass(/radio-checked/); + await expect(radioTwo).toHaveClass(/radio-checked/); + }); + }); +}); + +class RadioFixture { + readonly page: E2EPage; + + private radio!: Locator; + + constructor(page: E2EPage) { + this.page = page; + } + + async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') { + const { page } = this; + const radio = (this.radio = page.locator(selector)); + + if (method === 'keyboard') { + await radio.focus(); + await page.keyboard.press('Space'); + } else { + await radio.click(); + } + + await page.waitForChanges(); + + return radio; + } + + async expectChecked(state: boolean) { + const { radio } = this; + await expect(radio.locator('input')).toHaveJSProperty('checked', state); + } +} diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Chrome-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Chrome-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Chrome-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Firefox-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Firefox-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Firefox-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Safari-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Safari-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-ltr-Mobile-Safari-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Chrome-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Chrome-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Chrome-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Firefox-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Firefox-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Firefox-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Safari-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Safari-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Safari-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-ios-rtl-Mobile-Safari-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Chrome-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Chrome-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Chrome-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Firefox-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Firefox-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Firefox-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Safari-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Safari-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-ltr-Mobile-Safari-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Chrome-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Chrome-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Chrome-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Chrome-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Firefox-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Firefox-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Firefox-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Firefox-linux.png diff --git a/core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Safari-linux.png b/core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Safari-linux.png similarity index 100% rename from core/src/components/radio-group/test/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Safari-linux.png rename to core/src/components/radio-group/test/legacy/basic/radio-group.e2e.ts-snapshots/radio-group-diff-md-rtl-Mobile-Safari-linux.png diff --git a/core/src/components/radio-group/test/legacy/form/index.html b/core/src/components/radio-group/test/legacy/form/index.html new file mode 100644 index 0000000000..159f8c6249 --- /dev/null +++ b/core/src/components/radio-group/test/legacy/form/index.html @@ -0,0 +1,114 @@ + + + + + Radio Group - Form + + + + + + + + + + + + + Radio Group - Form + + + + +
+ + + + Luckiest Man On Earth + + + + Biff + + + + + + + Griff + + + + + + + Buford + + + + + + + George + + + + + Submit + + + +
+ +

+ Value: + +

+ +

+ Changes: + 0 +

+
+ + + +
+ + diff --git a/core/src/components/radio-group/test/legacy/form/radio-group.e2e.ts b/core/src/components/radio-group/test/legacy/form/radio-group.e2e.ts new file mode 100644 index 0000000000..72e3811d1c --- /dev/null +++ b/core/src/components/radio-group/test/legacy/form/radio-group.e2e.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('radio-group: form'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/radio-group/test/legacy/form', config); + }); + + test('selecting an option should update the value', async ({ page }) => { + const radioGroup = page.locator('ion-radio-group'); + const ionChange = await page.spyOnEvent('ionChange'); + const griffRadio = page.locator('ion-radio[value="griff"]'); + await expect(radioGroup).toHaveAttribute('value', 'biff'); + + await griffRadio.click(); + await page.waitForChanges(); + + await expect(ionChange).toHaveReceivedEventDetail({ value: 'griff', event: { isTrusted: true } }); + }); + + test('selecting a disabled option should not update the value', async ({ page }) => { + const value = page.locator('#value'); + const disabledRadio = page.locator('ion-radio[value="george"]'); + + await expect(value).toHaveText(''); + await expect(disabledRadio).toHaveAttribute('disabled', ''); + + await disabledRadio.click({ force: true }); + await page.waitForChanges(); + + await expect(value).toHaveText(''); + }); + }); +}); diff --git a/core/src/components/radio-group/test/legacy/search/index.html b/core/src/components/radio-group/test/legacy/search/index.html new file mode 100644 index 0000000000..fb0bb926f3 --- /dev/null +++ b/core/src/components/radio-group/test/legacy/search/index.html @@ -0,0 +1,82 @@ + + + + + Radio Group - Search + + + + + + + + + + + + + Radio Group - Form + + + + + + Current value: + + + + + + + + + + + + diff --git a/core/src/components/radio-group/test/legacy/search/radio-group.e2e.ts b/core/src/components/radio-group/test/legacy/search/radio-group.e2e.ts new file mode 100644 index 0000000000..ec40c75605 --- /dev/null +++ b/core/src/components/radio-group/test/legacy/search/radio-group.e2e.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not var across modes/directions. + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('radio-group'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/radio-group/test/legacy/search', config); + }); + + test.describe('radio-group: state', () => { + test('radio should remain checked after being removed/readded to the dom', async ({ page }) => { + const radioGroup = page.locator('ion-radio-group'); + const radio = page.locator('ion-radio[value=two]'); + const searchbarInput = page.locator('ion-searchbar input'); + + // select radio + await radio.click(); + await expect(radio.locator('input')).toHaveJSProperty('checked', true); + + // filter radio so it is not in DOM + await page.fill('ion-searchbar input', 'zero'); + await searchbarInput.evaluate((el) => el.blur()); + await page.waitForChanges(); + await expect(radio).toBeHidden(); + + // ensure radio group has the same value + await expect(radioGroup).toHaveJSProperty('value', 'two'); + + // clear the search so the radio appears + await page.fill('ion-searchbar input', ''); + await searchbarInput.evaluate((el) => el.blur()); + await page.waitForChanges(); + + // ensure that the new radio instance is still checked + await expect(radio.locator('input')).toHaveJSProperty('checked', true); + }); + }); + }); +}); diff --git a/core/src/components/radio-group/test/search/index.html b/core/src/components/radio-group/test/search/index.html index fb0bb926f3..825ac1c81c 100644 --- a/core/src/components/radio-group/test/search/index.html +++ b/core/src/components/radio-group/test/search/index.html @@ -65,8 +65,7 @@ if (d.value.includes(query)) { html += ` - Item ${d.value} - + Item ${d.value} `; } diff --git a/core/src/components/radio-group/test/search/radio-group.e2e.ts b/core/src/components/radio-group/test/search/radio-group.e2e.ts index ef9ac5828d..121acd4cd6 100644 --- a/core/src/components/radio-group/test/search/radio-group.e2e.ts +++ b/core/src/components/radio-group/test/search/radio-group.e2e.ts @@ -1,42 +1,41 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; +import { RadioFixture } from '../fixtures'; + /** * This behavior does not var across modes/directions. */ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { - test.describe(title('radio-group'), () => { - test.beforeEach(async ({ page }) => { + test.describe(title('radio-group: search'), () => { + test('radio should remain checked after being removed/readded to the dom', async ({ page }) => { await page.goto('/src/components/radio-group/test/search', config); - }); - test.describe('radio-group: state', () => { - test('radio should remain checked after being removed/readded to the dom', async ({ page }) => { - const radioGroup = page.locator('ion-radio-group'); - const radio = page.locator('ion-radio[value=two]'); - const searchbarInput = page.locator('ion-searchbar input'); + const radioFixture = new RadioFixture(page); - // select radio - await radio.click(); - await expect(radio.locator('input')).toHaveJSProperty('checked', true); + const radioGroup = page.locator('ion-radio-group'); + const searchbarInput = page.locator('ion-searchbar input'); - // filter radio so it is not in DOM - await page.fill('ion-searchbar input', 'zero'); - await searchbarInput.evaluate((el) => el.blur()); - await page.waitForChanges(); - await expect(radio).toBeHidden(); + // select radio + const radio = await radioFixture.checkRadio('mouse', 'ion-radio[value=two]'); + await radioFixture.expectChecked(true); - // ensure radio group has the same value - await expect(radioGroup).toHaveJSProperty('value', 'two'); + // filter radio so it is not in DOM + await page.fill('ion-searchbar input', 'zero'); + await searchbarInput.evaluate((el) => el.blur()); + await page.waitForChanges(); + await expect(radio).toBeHidden(); - // clear the search so the radio appears - await page.fill('ion-searchbar input', ''); - await searchbarInput.evaluate((el) => el.blur()); - await page.waitForChanges(); + // ensure radio group has the same value + await expect(radioGroup).toHaveJSProperty('value', 'two'); - // ensure that the new radio instance is still checked - await expect(radio.locator('input')).toHaveJSProperty('checked', true); - }); + // clear the search so the radio appears + await page.fill('ion-searchbar input', ''); + await searchbarInput.evaluate((el) => el.blur()); + await page.waitForChanges(); + + // ensure that the new radio instance is still checked + await radioFixture.expectChecked(true); }); }); }); diff --git a/core/src/components/radio/radio.tsx b/core/src/components/radio/radio.tsx index 14ae1f4d68..d6618dcffe 100644 --- a/core/src/components/radio/radio.tsx +++ b/core/src/components/radio/radio.tsx @@ -2,8 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core'; import type { LegacyFormController } from '@utils/forms'; import { createLegacyFormController } from '@utils/forms'; -import type { Attributes } from '@utils/helpers'; -import { addEventListener, getAriaLabel, inheritAriaAttributes, removeEventListener } from '@utils/helpers'; +import { addEventListener, getAriaLabel, removeEventListener } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -31,7 +30,6 @@ export class Radio implements ComponentInterface { private radioGroup: HTMLIonRadioGroupElement | null = null; private nativeInput!: HTMLInputElement; private legacyFormController!: LegacyFormController; - private inheritedAttributes: Attributes = {}; // This flag ensures we log the deprecation warning at most once. private hasLoggedDeprecationWarning = false; @@ -135,8 +133,7 @@ export class Radio implements ComponentInterface { ev.stopPropagation(); ev.preventDefault(); - const element = this.legacyFormController.hasLegacyControl() ? this.el : this.nativeInput; - element.focus(); + this.el.focus(); } /** @internal */ @@ -167,12 +164,6 @@ export class Radio implements ComponentInterface { componentWillLoad() { this.emitStyle(); - - if (!this.legacyFormController.hasLegacyControl()) { - this.inheritedAttributes = { - ...inheritAriaAttributes(this.el), - }; - } } @Watch('checked') @@ -201,7 +192,34 @@ export class Radio implements ComponentInterface { }; private onClick = () => { - this.checked = this.nativeInput.checked; + const { radioGroup, checked } = this; + + /** + * The legacy control uses a native input inside + * of the radio host, so we can set this.checked + * to the state of the nativeInput. RadioGroup + * will prevent the native input from checking if + * allowEmptySelection="false" by calling ev.preventDefault(). + */ + if (this.legacyFormController.hasLegacyControl()) { + this.checked = this.nativeInput.checked; + return; + } + + /** + * The modern control does not use a native input + * inside of the radio host, so we cannot rely on the + * ev.preventDefault() behavior above. If the radio + * is checked and the parent radio group allows for empty + * selection, then we can set the checked state to false. + * Otherwise, the checked state should always be set + * to true because the checked state cannot be toggled. + */ + if (checked && radioGroup?.allowEmptySelection) { + this.checked = false; + } else { + this.checked = true; + } }; private onFocus = () => { @@ -232,23 +250,14 @@ export class Radio implements ComponentInterface { } private renderRadio() { - const { - checked, - disabled, - inputId, - color, - el, - justify, - labelPlacement, - inheritedAttributes, - hasLabel, - buttonTabindex, - } = this; + const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex } = this; const mode = getIonMode(this); const inItem = hostContext('ion-item', el); return (