diff --git a/src/components/checkboxes/_checkbox-macro.njk b/src/components/checkboxes/_checkbox-macro.njk index dfe4ad400e..d1ed8a3a7b 100644 --- a/src/components/checkboxes/_checkbox-macro.njk +++ b/src/components/checkboxes/_checkbox-macro.njk @@ -3,10 +3,11 @@ {% if params.other %} - + {% from "components/input/_macro.njk" import onsInput %} {{ onsInput({ diff --git a/src/components/checkboxes/checkboxes.dom.js b/src/components/checkboxes/checkboxes.dom.js new file mode 100644 index 0000000000..18f3cb6d0b --- /dev/null +++ b/src/components/checkboxes/checkboxes.dom.js @@ -0,0 +1,4 @@ +import domready from 'js/domready'; +import Checkboxes from './checkboxes'; + +domready(() => new Checkboxes('js-checkbox')); diff --git a/src/components/checkboxes/checkboxes.js b/src/components/checkboxes/checkboxes.js new file mode 100644 index 0000000000..d5d40ef027 --- /dev/null +++ b/src/components/checkboxes/checkboxes.js @@ -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)); + } +} diff --git a/src/components/radios/_macro.njk b/src/components/radios/_macro.njk index 5d374d8a3f..fea9d0809a 100644 --- a/src/components/radios/_macro.njk +++ b/src/components/radios/_macro.njk @@ -16,10 +16,11 @@ {% if radio.other %} - + {% from "components/input/_macro.njk" import onsInput %} {{ onsInput({ diff --git a/src/components/radios/_test-template.njk b/src/components/radios/_test-template.njk new file mode 100644 index 0000000000..922663fb7c --- /dev/null +++ b/src/components/radios/_test-template.njk @@ -0,0 +1,3 @@ +{% from "components/radios/_macro.njk" import onsRadios %} + +{{ onsRadios(params) }} diff --git a/src/components/radios/radios.dom.js b/src/components/radios/radios.dom.js new file mode 100644 index 0000000000..015e11c795 --- /dev/null +++ b/src/components/radios/radios.dom.js @@ -0,0 +1,4 @@ +import domready from 'js/domready'; +import Radios from 'components/checkboxes/checkboxes'; + +domready(() => new Radios('js-radio')); diff --git a/src/js/index.js b/src/js/index.js index a1de0614c5..4a7223521d 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -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'; diff --git a/src/tests/spec/checkboxes/checkboxes.spec.js b/src/tests/spec/checkboxes/checkboxes.spec.js new file mode 100644 index 0000000000..cd3b9c55ee --- /dev/null +++ b/src/tests/spec/checkboxes/checkboxes.spec.js @@ -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 + }; +} diff --git a/src/tests/spec/radios/radios.spec.js b/src/tests/spec/radios/radios.spec.js new file mode 100644 index 0000000000..72353edb67 --- /dev/null +++ b/src/tests/spec/radios/radios.spec.js @@ -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 + }; +}