Skip to content

Commit

Permalink
Checkboxes and radios with revealed option accessibility (#478)
Browse files Browse the repository at this point in the history
* Added accessibity helpers for radios and checkboxes

* Added unit tests for radios and checkboxes with other input accessibility
  • Loading branch information
bameyrick authored Jul 1, 2019
1 parent 461cd3a commit d0c2de1
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 4 deletions.
5 changes: 3 additions & 2 deletions src/components/checkboxes/_checkbox-macro.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<input
type="checkbox"
id="{{ params.id }}"
class="checkbox__input {{ params.inputClasses }}"
class="checkbox__input js-checkbox {{ params.inputClasses }}"
value="{{ params.value }}"
{% if params.name %} name="{{ params.name }}"{% endif %}
{% if params.checked %} checked{% endif %}
{% if params.other %} aria-controls="{{ params.id }}-other-wrap" aria-haspopup="true"{% endif %}
{% if params.attributes %}{% for attribute, value in (params.attributes.items() if params.attributes is mapping and params.attributes.items else params.attributes) %}{{ attribute }}{% if value %}="{{ value }}"{% endif %} {% endfor %}{% endif %}
>
<label id="{{ params.id }}-label" class="checkbox__label {{ params.label.classes }}" for="{{ params.id }}">
Expand All @@ -17,7 +18,7 @@
{% endif %}
</label>
{% if params.other %}
<span class="checkbox__other">
<span class="checkbox__other" id="{{ params.id }}-other-wrap">
{% from "components/input/_macro.njk" import onsInput %}
{{
onsInput({
Expand Down
4 changes: 4 additions & 0 deletions src/components/checkboxes/checkboxes.dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import domready from 'js/domready';
import Checkboxes from './checkboxes';

domready(() => new Checkboxes('js-checkbox'));
11 changes: 11 additions & 0 deletions src/components/checkboxes/checkboxes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default class Checkboxes {
constructor(inputCls) {
this.inputs = [...document.querySelectorAll(`.${inputCls}`)];
this.inputs.forEach(input => input.addEventListener('change', this.setExpandedAttributes.bind(this)));
this.setExpandedAttributes();
}

setExpandedAttributes() {
this.inputs.filter(input => input.hasAttribute('aria-haspopup')).forEach(input => input.setAttribute('aria-expanded', input.checked));
}
}
5 changes: 3 additions & 2 deletions src/components/radios/_macro.njk
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
<input
type="radio"
id="{{ radio.id }}"
class="radio__input {{ radio.classes }}"
class="radio__input js-radio {{ radio.classes }}"
value="{{ radio.value }}"
name="{{ params.name }}"
{% if radio.checked or (params.value and params.value == radio.value) %} checked{% endif %}
{% if radio.other %} aria-controls="{{ radio.id }}-other-wrap" aria-haspopup="true"{% endif %}
{% if radio.attributes %}{% for attribute, value in (radio.attributes.items() if radio.attributes is mapping and radio.attributes.items else radio.attributes) %}{{ attribute }}{% if value %}="{{ value }}"{% endif %} {% endfor %}{% endif %}
>
<label class="radio__label {{ radio.label.classes }}" for="{{ radio.id }}">
Expand All @@ -30,7 +31,7 @@
{% endif %}
</label>
{% if radio.other %}
<span class="radio__other">
<span class="radio__other" id="{{ radio.id }}-other-wrap">
{% from "components/input/_macro.njk" import onsInput %}
{{
onsInput({
Expand Down
3 changes: 3 additions & 0 deletions src/components/radios/_test-template.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% from "components/radios/_macro.njk" import onsRadios %}

{{ onsRadios(params) }}
4 changes: 4 additions & 0 deletions src/components/radios/radios.dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import domready from 'js/domready';
import Radios from 'components/checkboxes/checkboxes';

domready(() => new Radios('js-radio'));
2 changes: 2 additions & 0 deletions src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ import 'components/header/header-nav';
import 'components/feedback/feedback.dom';
import 'components/uac/uac.dom';
import 'components/relationships/relationships.dom';
import 'components/checkboxes/checkboxes.dom';
import 'components/radios/radios.dom';
125 changes: 125 additions & 0 deletions src/tests/spec/checkboxes/checkboxes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { awaitPolyfills } from 'js/polyfills/await-polyfills';
import template from 'components/checkboxes/_test-template.njk';
import Checkboxes from 'components/checkboxes/checkboxes';

const params = {
legend: 'What are your favourite pizza toppings?',
dontVisuallyHideLegend: true,
checkboxesLabel: 'Select all that apply',
name: 'food-other',
checkboxes: [
{
id: 'bacon-other',
label: {
text: 'Bacon'
},
value: 'bacon'
},
{
id: 'olives-other',
label: {
text: 'Olives'
},
value: 'olives'
},
{
id: 'other-checkbox',
label: {
text: 'Other',
description: 'An answer is required'
},
value: 'other',
other: {
id: 'other-textbox',
name: 'other-answer',
label: {
text: 'Please specify other'
}
}
}
]
};

describe('Component: Checkboxes', function() {
before(() => awaitPolyfills);

beforeEach(function() {
const component = renderComponent(params);

Object.keys(component).forEach(key => {
this[key] = component[key];
});
});

describe('Before the component initialises', function() {
it('if a checkbox has an other option, it should be given the correct aria-attributes', function() {
expect(this.checkboxWithOther.hasAttribute('aria-haspopup')).to.equal(true);
expect(this.checkboxWithOther.getAttribute('aria-haspopup')).to.equal('true');
expect(this.checkboxWithOther.hasAttribute('aria-controls')).to.equal(true);
expect(this.checkboxWithOther.getAttribute('aria-controls')).to.equal(
`${params.checkboxes[params.checkboxes.length - 1].id}-other-wrap`
);
});
});

describe('When the component initialises', function() {
beforeEach(function() {
new Checkboxes('js-checkbox');
});

it('checkboxes with other options should be given aria-expanded attributes', function() {
expect(this.checkboxWithOther.hasAttribute('aria-expanded')).to.equal(true);
expect(this.checkboxWithOther.getAttribute('aria-expanded')).to.equal('false');
});

describe('and a checkbox with an other input is checked', function() {
beforeEach(function() {
this.checkboxWithOther.click();
});

// eslint-disable-next-line prettier/prettier
it('it\'s aria-expanded attribute should be set to true', function() {
expect(this.checkboxWithOther.getAttribute('aria-expanded')).to.equal('true');
});

describe('and any other checkbox is changed', function() {
beforeEach(function() {
this.checkboxes[0].click();
});

// eslint-disable-next-line prettier/prettier
it('the checkbox with an other input\'s aria-expanded attribute not change', function() {
expect(this.checkboxWithOther.getAttribute('aria-expanded')).to.equal('true');
});
});

describe('and a checkbox with an other input is unchecked', function() {
beforeEach(function() {
this.checkboxWithOther.click();
});

// eslint-disable-next-line prettier/prettier
it('it\'s aria-expanded attribute should be set to false', function() {
expect(this.checkboxWithOther.getAttribute('aria-expanded')).to.equal('false');
});
});
});
});
});

function renderComponent(params) {
const html = template.render({ params });

const wrapper = document.createElement('div');
wrapper.innerHTML = html;
document.body.appendChild(wrapper);

const checkboxes = [...wrapper.querySelectorAll('.js-checkbox')];
const checkboxWithOther = checkboxes[checkboxes.length - 1];

return {
wrapper,
checkboxes,
checkboxWithOther
};
}
139 changes: 139 additions & 0 deletions src/tests/spec/radios/radios.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { awaitPolyfills } from 'js/polyfills/await-polyfills';
import template from 'components/radios/_test-template.njk';
import Radios from 'components/checkboxes/checkboxes';

const params = {
name: 'contact-preference',
radios: [
{
id: 'email',
value: 'email',
label: {
text: 'Email'
},
other: {
type: 'email',
label: {
text: 'Enter your email address'
}
}
},
{
id: 'phone',
value: 'phone',
label: {
text: 'Phone'
},
other: {
type: 'tel',
label: {
text: 'Enter your phone number'
}
}
},
{
id: 'text',
value: 'text',
label: {
text: 'Text'
},
other: {
type: 'tel',
label: {
text: 'Enter your phone number'
}
}
}
]
};

describe('Component: Radios', function() {
before(() => awaitPolyfills);

beforeEach(function() {
const component = renderComponent(params);

Object.keys(component).forEach(key => {
this[key] = component[key];
});
});

describe('Before the component initialises', function() {
it('if a checkbox has an other option, it should be given the correct aria-attributes', function() {
this.radios.forEach(radio => {
expect(radio.hasAttribute('aria-haspopup')).to.equal(true);
expect(radio.getAttribute('aria-haspopup')).to.equal('true');
expect(radio.hasAttribute('aria-controls')).to.equal(true);
expect(radio.getAttribute('aria-controls')).to.equal(`${radio.id}-other-wrap`);
});
});
});

describe('When the component initialises', function() {
beforeEach(function() {
new Radios('js-radio');
});

it('checkboxes with other options should be given aria-expanded attributes', function() {
this.radios.forEach(radio => {
expect(radio.hasAttribute('aria-expanded')).to.equal(true);
expect(radio.getAttribute('aria-expanded')).to.equal('false');
});
});

describe('and a radio checked', function() {
beforeEach(function() {
this.radios[0].click();
});

// eslint-disable-next-line prettier/prettier
it('then the checked radio\'s aria-expanded attribute should be set to true', function() {
expect(this.radios[0].getAttribute('aria-expanded')).to.equal('true');
});

// eslint-disable-next-line prettier/prettier
it('then the unchecked radios\' aria-expanded attribute should be set to false', function() {
this.radios
.filter(radio => !radio.checked)
.forEach(radio => {
expect(radio.getAttribute('aria-expanded')).to.equal('false');
});
});

describe('and the radio selection is changed', function() {
beforeEach(function() {
this.radios[1].click();
});

// eslint-disable-next-line prettier/prettier
it('then the checked radio\'s aria-expanded attribute should be set to true', function() {
expect(this.radios[1].getAttribute('aria-expanded')).to.equal('true');
});

// eslint-disable-next-line prettier/prettier
it('then the unchecked radios\' aria-expanded attribute should be set to false', function() {
this.radios
.filter(radio => !radio.checked)
.forEach(radio => {
expect(radio.getAttribute('aria-expanded')).to.equal('false');
});
});
});
});
});
});

function renderComponent(params) {
const html = template.render({ params });

const wrapper = document.createElement('div');
wrapper.innerHTML = html;
document.body.appendChild(wrapper);

const radios = [...wrapper.querySelectorAll('.js-radio')];

return {
wrapper,
radios
};
}

0 comments on commit d0c2de1

Please sign in to comment.