Skip to content

Commit

Permalink
fix(switch): handle aria-checked correctly. (#5202)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yavanosta committed Dec 18, 2019
1 parent c483774 commit 98e2827
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 4 deletions.
7 changes: 4 additions & 3 deletions packages/mdc-switch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ npm install @material/switch
<div class="mdc-switch__track"></div>
<div class="mdc-switch__thumb-underlay">
<div class="mdc-switch__thumb"></div>
<input type="checkbox" id="basic-switch" class="mdc-switch__native-control" role="switch">
<input type="checkbox" id="basic-switch" class="mdc-switch__native-control" role="switch" aria-checked="false">
</div>
</div>
<label for="basic-switch">off/on</label>
Expand Down Expand Up @@ -78,7 +78,7 @@ Add the `mdc-switch--disabled` class to the `mdc-switch` element, and the `disab
<div class="mdc-switch__track"></div>
<div class="mdc-switch__thumb-underlay">
<div class="mdc-switch__thumb"></div>
<input type="checkbox" id="another-basic-switch" class="mdc-switch__native-control" role="switch" disabled>
<input type="checkbox" id="another-basic-switch" class="mdc-switch__native-control" role="switch" aria-checked="false" disabled>
</div>
</div>
<label for="another-basic-switch">off/on</label>
Expand All @@ -93,7 +93,7 @@ Add the `mdc-switch--checked` class to the `mdc-switch` element, and the `checke
<div class="mdc-switch__track"></div>
<div class="mdc-switch__thumb-underlay">
<div class="mdc-switch__thumb"></div>
<input type="checkbox" id="another-basic-switch" class="mdc-switch__native-control" role="switch" checked>
<input type="checkbox" id="another-basic-switch" class="mdc-switch__native-control" role="switch" aria-checked="false" checked>
</div>
</div>
<label for="another-basic-switch">off/on</label>
Expand Down Expand Up @@ -152,6 +152,7 @@ If you are using a JavaScript framework, such as React or Angular, you can creat
| `removeClass(className: string) => void` | Removes a class from the root element. |
| `setNativeControlChecked(checked: boolean)` | Sets the checked state of the native control. |
| `setNativeControlDisabled(disabled: boolean)` | Sets the disabled state of the native control. |
| `setNativeControlAttr(attr: string, value: string)` | Sets an HTML attribute to the given value on the native input element. |

### `MDCSwitchFoundation`

Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-switch/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ export interface MDCSwitchAdapter {
* Sets the disabled state of the native HTML control underlying the switch.
*/
setNativeControlDisabled(disabled: boolean): void;

/**
* Set an attribute value of the native HTML control underlying the switch.
*/
setNativeControlAttr(attr: string, value: string): void;
}
1 change: 1 addition & 0 deletions packages/mdc-switch/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class MDCSwitch extends MDCComponent<MDCSwitchFoundation> implements MDCR
removeClass: (className) => this.root_.classList.remove(className),
setNativeControlChecked: (checked) => this.nativeControl_.checked = checked,
setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled,
setNativeControlAttr: (attr, value) => this.nativeControl_.setAttribute(attr, value),
};
return new MDCSwitchFoundation(adapter);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/mdc-switch/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const cssClasses = {

/** String constants used by the switch. */
const strings = {
/** Aria attribute for checked or unchecked state of switch */
ARIA_CHECKED_ATTR: 'aria-checked',
/** Aria attribute value for checked state of switch */
ARIA_CHECKED_CHECKED_VALUE: 'true',
/** Aria attribute value for unchecked state of switch */
ARIA_CHECKED_UNCHECKED_VALUE: 'false',
/** A CSS selector used to locate the native HTML control for the switch. */
NATIVE_CONTROL_SELECTOR: '.mdc-switch__native-control',
/** A CSS selector used to locate the ripple surface element for the switch. */
Expand Down
11 changes: 11 additions & 0 deletions packages/mdc-switch/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class MDCSwitchFoundation extends MDCFoundation<MDCSwitchAdapter> {
removeClass: () => undefined,
setNativeControlChecked: () => undefined,
setNativeControlDisabled: () => undefined,
setNativeControlAttr: () => undefined,
};
}

Expand All @@ -53,6 +54,7 @@ export class MDCSwitchFoundation extends MDCFoundation<MDCSwitchAdapter> {
/** Sets the checked state of the switch. */
setChecked(checked: boolean) {
this.adapter_.setNativeControlChecked(checked);
this.updateAriaChecked_(checked);
this.updateCheckedStyling_(checked);
}

Expand All @@ -69,6 +71,7 @@ export class MDCSwitchFoundation extends MDCFoundation<MDCSwitchAdapter> {
/** Handles the change event for the switch native control. */
handleChange(evt: Event) {
const nativeControl = evt.target as HTMLInputElement;
this.updateAriaChecked_(nativeControl.checked);
this.updateCheckedStyling_(nativeControl.checked);
}

Expand All @@ -80,6 +83,14 @@ export class MDCSwitchFoundation extends MDCFoundation<MDCSwitchAdapter> {
this.adapter_.removeClass(cssClasses.CHECKED);
}
}

private updateAriaChecked_(checked: boolean) {
this.adapter_.setNativeControlAttr(
strings.ARIA_CHECKED_ATTR,
checked ?
strings.ARIA_CHECKED_CHECKED_VALUE :
strings.ARIA_CHECKED_UNCHECKED_VALUE);
}
}

// tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
Expand Down
42 changes: 41 additions & 1 deletion test/unit/mdc-switch/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ test('defaultAdapter returns a complete adapter implementation', () => {
const methods = Object.keys(defaultAdapter).filter((k) => typeof defaultAdapter[k] === 'function');

assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
assert.deepEqual(methods, ['addClass', 'removeClass', 'setNativeControlChecked', 'setNativeControlDisabled']);
assert.deepEqual(methods, [
'addClass',
'removeClass',
'setNativeControlChecked',
'setNativeControlDisabled',
'setNativeControlAttr',
]);
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
});

Expand Down Expand Up @@ -72,6 +78,22 @@ test('#setChecked removes mdc-switch--checked from the switch element when set t
td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#setChecked sets aria-checked to true when set to true', () => {
const {foundation, mockAdapter} = setupTest();
foundation.setChecked(true);
td.verify(mockAdapter.setNativeControlAttr(
MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR,
MDCSwitchFoundation.strings.ARIA_CHECKED_CHECKED_VALUE));
});

test('#setChecked sets aria-checked to false when set to false', () => {
const {foundation, mockAdapter} = setupTest();
foundation.setChecked(false);
td.verify(mockAdapter.setNativeControlAttr(
MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR,
MDCSwitchFoundation.strings.ARIA_CHECKED_UNCHECKED_VALUE));
});

test('#setDisabled updates the disabled state', () => {
const {foundation, mockAdapter} = setupTest();
foundation.setDisabled(true);
Expand Down Expand Up @@ -106,3 +128,21 @@ test('#handleChange removes mdc-switch--checked from the switch when it is an un
foundation.handleChange({target: {checked: false}});
td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#handleChange sets aria-checked to true when the swith is a checked state', () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleChange({target: {checked: true}});
td.verify(mockAdapter.setNativeControlAttr(
MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR,
MDCSwitchFoundation.strings.ARIA_CHECKED_CHECKED_VALUE));
});

test('#handleChange sets aria-checked to false when the swith is a checked state', () => {
const {foundation, mockAdapter} = setupTest();

foundation.handleChange({target: {checked: false}});
td.verify(mockAdapter.setNativeControlAttr(
MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR,
MDCSwitchFoundation.strings.ARIA_CHECKED_UNCHECKED_VALUE));
});
13 changes: 13 additions & 0 deletions test/unit/mdc-switch/mdc-switch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ test('get/set disabled updates the component styles', () => {
assert.isNotOk(root.classList.contains(MDCSwitchFoundation.cssClasses.DISABLED));
});

test('get/set checked updates the aria-checked of the native switch input element', () => {
const {root, component} = setupTest();
const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR);
component.checked = true;
assert.equal(
inputEl.getAttribute(MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR),
MDCSwitchFoundation.strings.ARIA_CHECKED_CHECKED_VALUE);
component.checked = false;
assert.equal(
inputEl.getAttribute(MDCSwitchFoundation.strings.ARIA_CHECKED_ATTR),
MDCSwitchFoundation.strings.ARIA_CHECKED_UNCHECKED_VALUE);
});

test('get ripple returns a MDCRipple instance', () => {
const {component} = setupTest();
assert.isOk(component.ripple instanceof MDCRipple);
Expand Down

0 comments on commit 98e2827

Please sign in to comment.