Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/flat-flowers-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sl-design-system/checkbox': patch
---

Fix NVDA accessibility issues, fix indeterminate state for screen readers.
1 change: 1 addition & 0 deletions packages/components/checkbox/src/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ $variants: default, valid, invalid;
inline-size: 1px;
opacity: 0;
outline: 0;
pointer-events: none;
position: absolute;
}

Expand Down
28 changes: 19 additions & 9 deletions packages/components/checkbox/src/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@ describe('sl-checkbox', () => {
it('should not be checked', () => {
expect(el.checked).not.to.be.true;
expect(input.checked).not.to.be.true;
expect(input).not.to.have.attribute('checked');
expect(input).to.have.attribute('aria-checked', 'false');
expect(input).not.to.match(':checked');
expect(input.checked).to.be.false;
});

it('should be checked when set', async () => {
el.checked = true;
await el.updateComplete;

expect(el).to.have.attribute('checked');
expect(input).to.have.attribute('checked');
expect(input).to.have.attribute('aria-checked', 'true');
expect(input).to.match(':checked');
expect(input.checked).to.be.true;
});

Expand All @@ -57,15 +60,18 @@ describe('sl-checkbox', () => {
it('should not be indeterminate', () => {
expect(el).not.to.have.attribute('indeterminate');
expect(el.indeterminate).not.to.be.true;
expect(input).not.to.have.attribute('indeterminate');
expect(input).not.to.match(':indeterminate');
expect(input.indeterminate).to.be.false;
});

it('should be indeterminate when set', async () => {
el.indeterminate = true;
await el.updateComplete;

expect(el).to.have.attribute('indeterminate');
expect(input).to.have.attribute('indeterminate');
expect(input).to.have.attribute('aria-checked', 'mixed');
expect(input).to.match(':indeterminate');
expect(input.indeterminate).to.be.true;
});

it('should not be required', () => {
Expand Down Expand Up @@ -142,16 +148,18 @@ describe('sl-checkbox', () => {

expect(el).to.have.attribute('checked');
expect(el.checked).to.be.true;
expect(input).to.have.attribute('checked');
expect(input).to.have.attribute('aria-checked', 'true');
expect(input).to.match(':checked');
expect(input.checked).to.be.true;

el.click();
await el.updateComplete;

expect(el).not.to.have.attribute('checked');
expect(el.checked).to.be.false;
expect(input).not.to.have.attribute('checked');
expect(input.checked).not.to.be.true;
expect(input).to.have.attribute('aria-checked', 'false');
expect(input).not.to.match(':checked');
expect(input.checked).to.be.false;
});

it('should change the state to checked on when pressing enter', async () => {
Expand All @@ -161,7 +169,8 @@ describe('sl-checkbox', () => {

expect(el).to.have.attribute('checked');
expect(el.checked).to.be.true;
expect(input).to.have.attribute('checked');
expect(input).to.have.attribute('aria-checked', 'true');
expect(input).to.match(':checked');
expect(input.checked).to.be.true;
});

Expand All @@ -172,7 +181,8 @@ describe('sl-checkbox', () => {

expect(el).to.have.attribute('checked');
expect(el.checked).to.be.true;
expect(input).to.have.attribute('checked');
expect(input).to.have.attribute('aria-checked', 'true');
expect(input).to.match(':checked');
expect(input.checked).to.be.true;
});

Expand Down
15 changes: 10 additions & 5 deletions packages/components/checkbox/src/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ export class Checkbox<T = unknown> extends FormControlMixin(LitElement) {
this.append(style);
}

this.setAttribute('role', 'checkbox');
this.setFormControlElement(this.input);

this.#onLabelSlotChange();
Expand Down Expand Up @@ -180,10 +179,14 @@ export class Checkbox<T = unknown> extends FormControlMixin(LitElement) {
return;
}

event.preventDefault();
if (event.target instanceof HTMLLabelElement) {
this.input.click();
}

event.stopPropagation();

this.checked = !this.checked;
this.input.checked = this.checked;
this.changeEvent.emit(this.formValue);
this.updateState({ dirty: true });
this.updateValidity();
Expand All @@ -200,6 +203,8 @@ export class Checkbox<T = unknown> extends FormControlMixin(LitElement) {

#onKeydown(event: KeyboardEvent): void {
if (['Enter', ' '].includes(event.key)) {
event.preventDefault();
event.stopPropagation();
this.#onClick(event);
}
}
Expand Down Expand Up @@ -249,8 +254,8 @@ export class Checkbox<T = unknown> extends FormControlMixin(LitElement) {
input.id ||= `sl-checkbox-${nextUniqueId++}`;
input.required = !!this.required;

input.toggleAttribute('checked', !!this.checked);
input.toggleAttribute('indeterminate', !!this.indeterminate);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
input.checked = !!this.checked;
input.indeterminate = !!this.indeterminate;
input.setAttribute('aria-checked', this.indeterminate ? 'mixed' : this.checked ? 'true' : 'false');
}
}
Loading