From 30710a43688a60a45eca5af6e9a80beb8c103c14 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 17 Apr 2018 16:43:39 -0400 Subject: [PATCH] fix(checkbox): Implement component/adapter APIs to sync aria-checked (#2580) --- packages/mdc-checkbox/foundation.js | 23 ++++++++++------- packages/mdc-checkbox/index.js | 2 ++ test/unit/mdc-checkbox/foundation.test.js | 22 ++++++++++++++++ test/unit/mdc-checkbox/mdc-checkbox.test.js | 28 ++++++++++++++------- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/mdc-checkbox/foundation.js b/packages/mdc-checkbox/foundation.js index f300b3126c1..7664aca45f2 100644 --- a/packages/mdc-checkbox/foundation.js +++ b/packages/mdc-checkbox/foundation.js @@ -49,8 +49,8 @@ class MDCCheckboxFoundation extends MDCFoundation { return /** @type {!MDCCheckboxAdapter} */ ({ addClass: (/* className: string */) => {}, removeClass: (/* className: string */) => {}, - setNativeControlAttr: () => {}, - removeNativeControlAttr: () => {}, + setNativeControlAttr: (/* attr: string, value: string */) => {}, + removeNativeControlAttr: (/* attr: string */) => {}, registerAnimationEndHandler: (/* handler: EventListener */) => {}, deregisterAnimationEndHandler: (/* handler: EventListener */) => {}, registerChangeHandler: (/* handler: EventListener */) => {}, @@ -82,6 +82,7 @@ class MDCCheckboxFoundation extends MDCFoundation { init() { this.currentCheckState_ = this.determineCheckState_(this.getNativeControl_()); + this.updateAriaChecked_(); this.adapter_.addClass(cssClasses.UPGRADED); this.adapter_.registerChangeHandler(this.changeHandler_); this.installPropertyChangeHooks_(); @@ -205,13 +206,7 @@ class MDCCheckboxFoundation extends MDCFoundation { return; } - // Ensure aria-checked is set to mixed if checkbox is in indeterminate state. - if (this.isIndeterminate()) { - this.adapter_.setNativeControlAttr( - strings.ARIA_CHECKED_ATTR, strings.ARIA_CHECKED_INDETERMINATE_VALUE); - } else { - this.adapter_.removeNativeControlAttr(strings.ARIA_CHECKED_ATTR); - } + this.updateAriaChecked_(); // Check to ensure that there isn't a previously existing animation class, in case for example // the user interacted with the checkbox before the animation was finished. @@ -288,6 +283,16 @@ class MDCCheckboxFoundation extends MDCFoundation { } } + updateAriaChecked_() { + // Ensure aria-checked is set to mixed if checkbox is in indeterminate state. + if (this.isIndeterminate()) { + this.adapter_.setNativeControlAttr( + strings.ARIA_CHECKED_ATTR, strings.ARIA_CHECKED_INDETERMINATE_VALUE); + } else { + this.adapter_.removeNativeControlAttr(strings.ARIA_CHECKED_ATTR); + } + } + /** * @return {!MDCSelectionControlState} * @private diff --git a/packages/mdc-checkbox/index.js b/packages/mdc-checkbox/index.js index 715a48f2c12..18d591c874c 100644 --- a/packages/mdc-checkbox/index.js +++ b/packages/mdc-checkbox/index.js @@ -73,6 +73,8 @@ class MDCCheckbox extends MDCComponent { return new MDCCheckboxFoundation({ addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), + setNativeControlAttr: (attr, value) => this.nativeCb_.setAttribute(attr, value), + removeNativeControlAttr: (attr) => this.nativeCb_.removeAttribute(attr), registerAnimationEndHandler: (handler) => this.root_.addEventListener(getCorrectEventName(window, 'animationend'), handler), deregisterAnimationEndHandler: diff --git a/test/unit/mdc-checkbox/foundation.test.js b/test/unit/mdc-checkbox/foundation.test.js index 0629553a6d2..0ca0a0b3066 100644 --- a/test/unit/mdc-checkbox/foundation.test.js +++ b/test/unit/mdc-checkbox/foundation.test.js @@ -123,6 +123,14 @@ test('#init adds the upgraded class to the root element', () => { td.verify(mockAdapter.addClass(cssClasses.UPGRADED)); }); +test('#init adds aria-checked="mixed" if checkbox is initially indeterminate', () => { + const {foundation, mockAdapter, nativeControl} = setupTest(); + nativeControl.indeterminate = true; + + foundation.init(); + td.verify(mockAdapter.setNativeControlAttr('aria-checked', strings.ARIA_CHECKED_INDETERMINATE_VALUE)); +}); + test('#init calls adapter.registerChangeHandler() with a change handler function', () => { const {foundation, mockAdapter} = setupTest(); const {isA} = td.matchers; @@ -216,6 +224,20 @@ test('#setIndeterminate updates the value of nativeControl.indeterminate', () => assert.isNotOk(nativeControl.indeterminate); }); +test('#setIndeterminate adds aria-checked="mixed" when indeterminate is true', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.init(); + foundation.setIndeterminate(true); + td.verify(mockAdapter.setNativeControlAttr('aria-checked', strings.ARIA_CHECKED_INDETERMINATE_VALUE)); +}); + +test('#setIndeterminate removes aria-checked when indeterminate is false', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.init(); + foundation.setIndeterminate(false); + td.verify(mockAdapter.removeNativeControlAttr('aria-checked')); +}); + test('#setIndeterminate works when no native control is returned', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.getNativeControl()).thenReturn(null); diff --git a/test/unit/mdc-checkbox/mdc-checkbox.test.js b/test/unit/mdc-checkbox/mdc-checkbox.test.js index 19e3de9c6de..c519f177924 100644 --- a/test/unit/mdc-checkbox/mdc-checkbox.test.js +++ b/test/unit/mdc-checkbox/mdc-checkbox.test.js @@ -51,8 +51,9 @@ function getFixture() { function setupTest() { const root = getFixture(); + const cb = root.querySelector(strings.NATIVE_CONTROL_SELECTOR); const component = new MDCCheckbox(root); - return {root, component}; + return {root, cb, component}; } suite('MDCCheckbox'); @@ -100,32 +101,28 @@ test('attachTo initializes and returns a MDCCheckbox instance', () => { }); test('get/set checked updates the checked property on the native checkbox element', () => { - const {root, component} = setupTest(); - const cb = root.querySelector(strings.NATIVE_CONTROL_SELECTOR); + const {cb, component} = setupTest(); component.checked = true; assert.isOk(cb.checked); assert.equal(component.checked, cb.checked); }); test('get/set indeterminate updates the indeterminate property on the native checkbox element', () => { - const {root, component} = setupTest(); - const cb = root.querySelector(strings.NATIVE_CONTROL_SELECTOR); + const {cb, component} = setupTest(); component.indeterminate = true; assert.isOk(cb.indeterminate); assert.equal(component.indeterminate, cb.indeterminate); }); test('get/set disabled updates the indeterminate property on the native checkbox element', () => { - const {root, component} = setupTest(); - const cb = root.querySelector(strings.NATIVE_CONTROL_SELECTOR); + const {cb, component} = setupTest(); component.disabled = true; assert.isOk(cb.disabled); assert.equal(component.disabled, cb.disabled); }); test('get/set value updates the value of the native checkbox element', () => { - const {root, component} = setupTest(); - const cb = root.querySelector(strings.NATIVE_CONTROL_SELECTOR); + const {cb, component} = setupTest(); component.value = 'new value'; assert.equal(cb.value, 'new value'); assert.equal(component.value, cb.value); @@ -149,6 +146,19 @@ test('adapter#removeClass removes a class from the root element', () => { assert.isNotOk(root.classList.contains('foo')); }); +test('adapter#setNativeControlAttr sets an attribute on the input element', () => { + const {cb, component} = setupTest(); + component.getDefaultFoundation().adapter_.setNativeControlAttr('aria-checked', 'mixed'); + assert.equal(cb.getAttribute('aria-checked'), 'mixed'); +}); + +test('adapter#removeNativeControlAttr removes an attribute from the input element', () => { + const {cb, component} = setupTest(); + cb.setAttribute('aria-checked', 'mixed'); + component.getDefaultFoundation().adapter_.removeNativeControlAttr('aria-checked'); + assert.isFalse(cb.hasAttribute('aria-checked')); +}); + test('adapter#registerAnimationEndHandler adds an animation end event listener on the root element', () => { const {root, component} = setupTest(); const handler = td.func('animationEndHandler');