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
+ };
+}