Skip to content

Commit

Permalink
fix(checkbox): Implement component/adapter APIs to sync aria-checked (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
kfranqueiro authored Apr 17, 2018
1 parent 60c0204 commit 30710a4
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 18 deletions.
23 changes: 14 additions & 9 deletions packages/mdc-checkbox/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */) => {},
Expand Down Expand Up @@ -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_();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/mdc-checkbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions test/unit/mdc-checkbox/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 19 additions & 9 deletions test/unit/mdc-checkbox/mdc-checkbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand All @@ -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');
Expand Down

0 comments on commit 30710a4

Please sign in to comment.