-
-
+
+
+
@@ -135,28 +126,58 @@ export class Radio extends LitElement {
mask="url(#${this.maskId})" />
+
`;
}
- private handleChange(event: Event) {
+ protected override updated() {
+ // Firefox does not support ElementInternals aria yet, so we need to hydrate
+ // an attribute.
+ if (!('ariaChecked' in this.internals)) {
+ this.setAttribute('aria-checked', String(this.checked));
+ return;
+ }
+
+ this.internals.ariaChecked = String(this.checked);
+ }
+
+ private async handleClick(event: Event) {
if (this.disabled) {
return;
}
- // Per spec, the change event on a radio input always represents checked.
+ // allow event to propagate to user code after a microtask.
+ await 0;
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ if (isActivationClick(event)) {
+ this.focus();
+ }
+
+ // Per spec, clicking on a radio input always selects it.
this.checked = true;
- redispatchEvent(this, event);
+ this.dispatchEvent(new Event('change', {bubbles: true}));
+ }
+
+ private async handleKeydown(event: KeyboardEvent) {
+ // allow event to propagate to user code after a microtask.
+ await 0;
+ if (event.key !== ' ' || event.defaultPrevented) {
+ return;
+ }
+
+ this.click();
}
/** @private */
diff --git a/radio/internal/single-selection-controller.ts b/radio/internal/single-selection-controller.ts
index c1c935ebc4..0af2e2b280 100644
--- a/radio/internal/single-selection-controller.ts
+++ b/radio/internal/single-selection-controller.ts
@@ -122,7 +122,7 @@ export class SingleSelectionController implements ReactiveController {
// 2. If an element is focused, the others are no longer focusable.
if (checkedSibling || this.focused) {
const focusable = checkedSibling || this.host;
- focusable.removeAttribute('tabindex');
+ focusable.tabIndex = 0;
for (const sibling of siblings) {
if (sibling !== focusable) {
@@ -134,7 +134,7 @@ export class SingleSelectionController implements ReactiveController {
// 3. If none are checked or focused, all are focusable.
for (const sibling of siblings) {
- sibling.removeAttribute('tabindex');
+ sibling.tabIndex = 0;
}
}
@@ -212,12 +212,13 @@ export class SingleSelectionController implements ReactiveController {
if (sibling !== nextSibling) {
sibling.checked = false;
sibling.tabIndex = -1;
+ sibling.blur();
}
}
// The next sibling should be checked, focused and dispatch a change event
nextSibling.checked = true;
- nextSibling.removeAttribute('tabindex');
+ nextSibling.tabIndex = 0;
nextSibling.focus();
// Fire a change event since the change is triggered by a user action.
// This matches native
behavior.
diff --git a/radio/radio_test.ts b/radio/radio_test.ts
index 755e205e48..e6865957b3 100644
--- a/radio/radio_test.ts
+++ b/radio/radio_test.ts
@@ -77,6 +77,23 @@ describe('
', () => {
.toBeTrue();
});
+ it('clicking a radio can be default prevented', async () => {
+ const {harnesses} = await setupTest(radioGroup);
+ const unselected = harnesses[1];
+ expect(unselected.element.checked)
+ .withContext('unselected checked')
+ .toBeFalse();
+
+ unselected.element.addEventListener('click', event => {
+ event.preventDefault();
+ });
+
+ await unselected.clickWithMouse();
+ expect(unselected.element.checked)
+ .withContext('after clicking checked')
+ .toBeFalse();
+ });
+
it('clicking a radio should unselect other radio which is already selected',
async () => {
const {harnesses} = await setupTest(radioGroupPreSelected);