Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/radio-invalid-property-removal.md
Comment thread
JinMokai marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@spectrum-web-components/radio': patch
---

**Fixed**: Removed deprecated `invalid` property from `<sp-radio>` component in favor of group-level validation.

The `invalid` property on individual `<sp-radio>` elements has been removed. Validation state should now be managed exclusively at the `<sp-radio-group>` level using the `invalid` property, which correctly applies the `aria-invalid` attribute to the group container. This change aligns with accessibility best practices where validation feedback should be provided at the radio group level rather than on individual radio buttons.

**Migration guide**: If you were using the `invalid` property on individual `<sp-radio>` elements, move it to the parent `<sp-radio-group>`:

```html
<!-- Before -->
<sp-radio-group>
<sp-radio value="option1" invalid>Option 1</sp-radio>
<sp-radio value="option2">Option 2</sp-radio>
</sp-radio-group>

<!-- After -->
<sp-radio-group invalid>
<sp-radio value="option1">Option 1</sp-radio>
<sp-radio value="option2">Option 2</sp-radio>
</sp-radio-group>
```
5 changes: 5 additions & 0 deletions 1st-gen/packages/radio/src/Radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export class Radio extends SizedMixin(
@property({ type: Boolean, reflect: true })
public emphasized = false;

/**
* @deprecated
* The invalid state of a single radio button is deprecated.
* Please use the invalid state of the radio group instead.
*/
@property({ type: Boolean, reflect: true })
public invalid = false;

Expand Down
59 changes: 59 additions & 0 deletions 1st-gen/packages/radio/src/RadioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class RadioGroup extends FocusVisiblePolyfillMixin(FieldGroup) {
@property({ type: String })
public name = '';

@property({ type: Boolean, reflect: true })
public override invalid = false;

@queryAssignedNodes()
public defaultNodes!: Node[];

Expand Down Expand Up @@ -137,7 +140,63 @@ export class RadioGroup extends FocusVisiblePolyfillMixin(FieldGroup) {
}
}

private _childInvalidObserver?: MutationObserver | null;

public override disconnectedCallback(): void {
this.clearInvalidObserver();
super.disconnectedCallback();
}

protected override handleSlotchange(): void {
this.rovingTabindexController.clearElementCache();
this.manageInvalidObserver();
}

private manageInvalidObserver(): void {
if (this._childInvalidObserver) {
this._childInvalidObserver.disconnect();
}
this._childInvalidObserver = new MutationObserver(() => {
this.checkInvalidState();
});
this.buttons.forEach((button) => {
this._childInvalidObserver?.observe(button, {
attributes: true,
attributeFilter: ['invalid'],
});
});
this.checkInvalidState();
}
Comment thread
JinMokai marked this conversation as resolved.
Outdated

private checkInvalidState(): void {
const invalidChild = this.buttons.find((button) => button.invalid);
if (invalidChild) {
if (!this.invalid) {
this.invalid = true;
window.__swc.warn(
this,
'The "invalid" attribute on <sp-radio> is deprecated. Please apply the "invalid" attribute to the parent <sp-radio-group> instead.',
'https://opensource.adobe.com/spectrum-web-components/components/radio',
{ level: 'deprecation' }
);
}
}
}
Comment thread
JinMokai marked this conversation as resolved.
Outdated

private clearInvalidObserver(): void {
this._childInvalidObserver?.disconnect();
this._childInvalidObserver = null;
}

protected override updated(changes: PropertyValues<this>): void {
super.updated(changes);

if (changes.has('invalid')) {
if (this.invalid) {
this.setAttribute('aria-invalid', 'true');
} else {
this.removeAttribute('aria-invalid');
}
}
}
}
54 changes: 54 additions & 0 deletions 1st-gen/packages/radio/test/radio-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,60 @@ describe('Radio Group', () => {

expect(changeSpy.calledWith(undefined)).to.be.false;
});

it('updates [aria-invalid] when [invalid] changes', async () => {
const el = await fixture<RadioGroup>(html`
<sp-radio-group invalid>
<sp-radio value="first">Option 1</sp-radio>
<sp-radio value="second">Option 2</sp-radio>
</sp-radio-group>
`);
await elementUpdated(el);
expect(el.hasAttribute('aria-invalid')).to.be.true;
expect(el.getAttribute('aria-invalid')).to.equal('true');
el.invalid = false;
await elementUpdated(el);
expect(el.hasAttribute('aria-invalid')).to.be.false;
el.invalid = true;
await elementUpdated(el);
expect(el.hasAttribute('aria-invalid')).to.be.true;
expect(el.getAttribute('aria-invalid')).to.equal('true');
});

it('warns when [invalid] is used on children and updates group invalid state', async () => {
const consoleWarnSpy = spy(console, 'warn');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RadioGroup uses window.__swc.warn(...) but you are using spy(console.warn). If window.__swc.warn doesn't call console.warn in the test environment, tests will fail or become flaky.

const el = await fixture<RadioGroup>(html`
<sp-radio-group>
<sp-radio value="first" invalid>Option 1</sp-radio>
<sp-radio value="second">Option 2</sp-radio>
</sp-radio-group>
`);
await elementUpdated(el);

expect(el.invalid).to.be.true;
expect(el.hasAttribute('aria-invalid')).to.be.true;
expect(consoleWarnSpy.called).to.be.true;
expect(consoleWarnSpy.args[0][0]).to.include('DEPRECATION NOTICE');

consoleWarnSpy.restore();
});

it('should not have invalid state when children do not have invalid attribute', async () => {
const consoleWarnSpy = spy(console, 'warn');
const el = await fixture<RadioGroup>(html`
<sp-radio-group>
<sp-radio value="first">Option 1</sp-radio>
<sp-radio value="second">Option 2</sp-radio>
</sp-radio-group>
`);
await elementUpdated(el);

expect(el.invalid).to.be.false;
expect(el.hasAttribute('aria-invalid')).to.be.false;
expect(consoleWarnSpy.called).to.be.false;

consoleWarnSpy.restore();
});
});

describe('Radio Group - late children', () => {
Expand Down
Loading