Skip to content

Commit

Permalink
feat(checkbox): improve accessibility for screen readers (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
wsuwt authored Oct 25, 2021
1 parent 7554471 commit 93509d0
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 21 deletions.
14 changes: 14 additions & 0 deletions packages/elements/src/checkbox/__demo__/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
.ellipsis {
width: 200px;
}
.group {
display: flex;
flex-direction: column;
}
</style>
<script type="module">
import '@refinitiv-ui/elements/checkbox';
Expand All @@ -24,6 +28,16 @@
<ef-checkbox></ef-checkbox>
</demo-block>

<demo-block header="Group Checkbox" tags="accessibility">
<div role="group" aria-labelledby="header" class="group">
<h6 id="header">Sandwich Condiments</h6>
<ef-checkbox>Lettuce</ef-checkbox>
<ef-checkbox>Tomato</ef-checkbox>
<ef-checkbox checked>Mustard</ef-checkbox>
<ef-checkbox>Sprouts</ef-checkbox>
</div>
</demo-block>

<!-- TRUNCATE CHECKBOX -->
<demo-block header="Truncate Checkbox">
<ef-checkbox class="ellipsis">Checkbox with very long long long text</ef-checkbox>
Expand Down
63 changes: 63 additions & 0 deletions packages/elements/src/checkbox/__test__/checkbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,69 @@ describe('checkbox/Checkbox', () => {
const readonly = `<ef-checkbox readonly>${label}</ef-checkbox>`;
const indeterminate = `<ef-checkbox indeterminate>${label}</ef-checkbox>`;


describe('Accessiblity', () => {
it('should fail without label', async () => {
const el = await fixture(noLabel);
await expect(el).not.to.be.accessible();
});
it('should pass a11y test with aria-label', async () => {
const el = await fixture(`<ef-checkbox aria-label="Checkbox without label"></ef-checkbox>`);
await expect(el).to.be.accessible();
});
it('should pass a11y test with slotted label', async () => {
const el = await fixture(unchecked);
await expect(el).to.be.accessible();
await expect(el.ariaChecked).to.equal(String(el.checked));
});
it('should pass a11y test when in checked state', async () => {
const el = await fixture(checked);

await expect(el).to.be.accessible();
await expect(el.ariaChecked).to.equal(String(el.checked));
});
it('should pass a11y test when in indeterminate state and has aria-checked="mixed"', async () => {
const el = await fixture(indeterminate);

await expect(el).to.be.accessible();
await expect(el.ariaChecked).to.equal('mixed');
});
it('should have aria-checked equals to false when indeterminate changes to false', async () => {
const el = await fixture(indeterminate);
el.indeterminate = false;
await elementUpdated(el);

await expect(el).to.be.accessible();
await expect(el.checked).to.equal(false);
await expect(el.ariaChecked).to.equal(String(el.checked));
});
it('should have aria-checked equals to false when checked is set to indeterminate checkbox', async () => {
const el = await fixture(indeterminate);
el.checked = true;
await elementUpdated(el);

await expect(el).to.be.accessible();
await expect(el.checked).to.equal(true);
await expect(el.ariaChecked).to.equal(String(el.checked));
});
it('should have aria-checked equals to mixed when indeterminate is set to checked checkbox', async () => {
const el = await fixture(checked);
el.indeterminate = true;
await elementUpdated(el);

await expect(el).to.be.accessible();
await expect(el.ariaChecked).to.equal('mixed');
});
it('should pass a11y test when disabled', async () => {
const el = await fixture(disabled);
await expect(el).to.be.accessible();
});
it('should pass a11y test when readonly', async () => {
const el = await fixture(readonly);
await expect(el).to.be.accessible();
});
})

describe('Basic Structure And State', () => {
it('DOM structure is correct', async () => {
el = await fixture(unchecked);
Expand Down
69 changes: 48 additions & 21 deletions packages/elements/src/checkbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import '../icon/index.js';
alias: 'coral-checkbox'
})
export class Checkbox extends ControlElement {

/**
* Element version number
* @returns version number
Expand All @@ -37,6 +36,8 @@ export class Checkbox extends ControlElement {
return VERSION;
}

protected readonly defaultRole = 'checkbox';

/**
* A `CSSResultGroup` that will be used
* to style the host, slotted children
Expand Down Expand Up @@ -71,41 +72,66 @@ export class Checkbox extends ControlElement {
`;
}

private _checked = false;
/**
* Value of checkbox
* @param value new checked value
*/
@property({ type: Boolean, reflect: true })
public checked = false;
public set checked (value: boolean) {
const oldValue = this._checked;
if (oldValue !== value) {
this._checked = value;

// remove indeterminate if change state to checked
if (this._checked) {
this.indeterminate = false;
}

this.ariaChecked = String(value);
void this.requestUpdate('checked', oldValue);
}
}
public get checked (): boolean {
return this._checked;
}

private _indeterminate = false;
/**
* Set state to indeterminate
* @param value new indeterminate value
*/
@property({ type: Boolean, reflect: true })
public indeterminate = false;
public set indeterminate (value: boolean) {
const oldValue = this._indeterminate;
if (oldValue !== value) {
this._indeterminate = value;

// remove checked if change state to indeterminate
if (value) {
this.checked = false;
}

this.ariaChecked = value ? 'mixed' : String(this.checked);
void this.requestUpdate('indeterminate', oldValue);
}
}
public get indeterminate (): boolean {
return this._indeterminate;
}

/**
* Getter for label
* Indicates current state of checkbox
* @ignore
*/
@query('[part=label]', true)
private labelEl!: HTMLElement;
@property({ type: String, reflect: true, attribute: 'aria-checked' })
public ariaChecked = String(this.checked);

/**
* Updates the element
* @param changedProperties Properties that has changed
* @returns {void}
* Getter for label
*/
protected update (changedProperties: PropertyValues): void {
// remove indeterminate if change state to checked
if(changedProperties.get('checked') === false && this.checked && this.indeterminate) {
this.indeterminate = false;
}
// remove checked if change state to indeterminate
if(changedProperties.get('indeterminate') === false && this.indeterminate && this.checked) {
this.checked = false;
}

super.update(changedProperties);
}
@query('[part=label]', true)
private labelEl!: HTMLElement;

/**
* Called once after the component is first rendered
Expand All @@ -114,6 +140,7 @@ export class Checkbox extends ControlElement {
*/
protected firstUpdated (changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);

this.addEventListener('tap', this.onTap);
this.addEventListener('keydown', this.onKeyDown);

Expand Down
4 changes: 4 additions & 0 deletions packages/elements/src/item/__snapshots__/Item.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@
```html
<div part="left">
<ef-checkbox
aria-checked="false"
aria-disabled="false"
aria-readonly="false"
part="checkbox"
role="checkbox"
tabindex="-1"
>
</ef-checkbox>
Expand All @@ -272,10 +274,12 @@
```html
<div part="left">
<ef-checkbox
aria-checked="true"
aria-disabled="false"
aria-readonly="false"
checked=""
part="checkbox"
role="checkbox"
tabindex="-1"
>
</ef-checkbox>
Expand Down

0 comments on commit 93509d0

Please sign in to comment.