diff --git a/.changeset/flat-flowers-approve.md b/.changeset/flat-flowers-approve.md new file mode 100644 index 0000000000..ae421bde69 --- /dev/null +++ b/.changeset/flat-flowers-approve.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/checkbox': patch +--- + +Fix NVDA accessibility issues, fix indeterminate state for screen readers. diff --git a/packages/components/checkbox/src/checkbox.scss b/packages/components/checkbox/src/checkbox.scss index d7e1d3f128..98df79f45f 100644 --- a/packages/components/checkbox/src/checkbox.scss +++ b/packages/components/checkbox/src/checkbox.scss @@ -117,6 +117,7 @@ $variants: default, valid, invalid; inline-size: 1px; opacity: 0; outline: 0; + pointer-events: none; position: absolute; } diff --git a/packages/components/checkbox/src/checkbox.spec.ts b/packages/components/checkbox/src/checkbox.spec.ts index f6acbe6669..8030be0c9c 100644 --- a/packages/components/checkbox/src/checkbox.spec.ts +++ b/packages/components/checkbox/src/checkbox.spec.ts @@ -29,7 +29,9 @@ 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 () => { @@ -37,7 +39,8 @@ describe('sl-checkbox', () => { 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; }); @@ -57,7 +60,8 @@ 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 () => { @@ -65,7 +69,9 @@ describe('sl-checkbox', () => { 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', () => { @@ -142,7 +148,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; el.click(); @@ -150,8 +157,9 @@ describe('sl-checkbox', () => { 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 () => { @@ -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; }); @@ -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; }); diff --git a/packages/components/checkbox/src/checkbox.ts b/packages/components/checkbox/src/checkbox.ts index eb075363a1..40a285cf72 100644 --- a/packages/components/checkbox/src/checkbox.ts +++ b/packages/components/checkbox/src/checkbox.ts @@ -114,7 +114,6 @@ export class Checkbox extends FormControlMixin(LitElement) { this.append(style); } - this.setAttribute('role', 'checkbox'); this.setFormControlElement(this.input); this.#onLabelSlotChange(); @@ -180,10 +179,14 @@ export class Checkbox 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(); @@ -200,6 +203,8 @@ export class Checkbox extends FormControlMixin(LitElement) { #onKeydown(event: KeyboardEvent): void { if (['Enter', ' '].includes(event.key)) { + event.preventDefault(); + event.stopPropagation(); this.#onClick(event); } } @@ -249,8 +254,8 @@ export class Checkbox 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'); } }