Skip to content

Commit

Permalink
feat(switch): Move component specific logic out of foundation (#3342)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: We've removed the `isChecked` and `isDisabled` methods from `MDCSwitchFoundation`. Please update any call to `MDCSwitchFoundation.handleChange` so it passes in the change event. And note that `isNativeControlChecked` and `isNativeControlDisabled` are no longer required methods in `MDCSwitchAdapter`
  • Loading branch information
lynnmercier authored Aug 15, 2018
1 parent dd20ea8 commit e1e4465
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 88 deletions.
30 changes: 22 additions & 8 deletions packages/mdc-switch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ const switchControl = new MDCSwitch(document.querySelector('.mdc-switch'));
## Variant

### Disabled Switch
### Initially Disabled Switch

Users can add the class 'mdc-switch--disabled' to the 'mdc-switch' element to disable the switch.
Add the `mdc-switch--disabled` class to the `mdc-switch` element, and the `disabled` attribute to the `mdc-switch__native-control` element to disable the switch. This logic is handled by the `MDCSwitchFoundation.setDisabled` method, but you'll want to avoid a FOUC by initially adding this class and attribute.

```html
<div class="mdc-switch mdc-switch--disabled">
Expand All @@ -86,13 +86,31 @@ Users can add the class 'mdc-switch--disabled' to the 'mdc-switch' element to di
<label for="another-basic-switch">off/on</label>
```

### Initially "On" Switch

Add the `mdc-switch--checked` class to the `mdc-switch` element, and the `checked` attribute to the `mdc-switch__native-control` element to toggle the switch to "on". This logic is handled by the `MDCSwitchFoundation.setChecked` method, but you'll want to avoid a FOUC by initially adding this class and attribute.

```html
<div class="mdc-switch mdc-switch--checked">
<div class="mdc-switch__track"></div>
<div class="mdc-switch__thumb-underlay">
<div class="mdc-switch__thumb">
<input type="checkbox" id="another-basic-switch" class="mdc-switch__native-control" role="switch" checked>
</div>
</div>
</div>
<label for="another-basic-switch">off/on</label>
```

## Style Customization

### CSS Classes

CSS Class | Description
--- | ---
`mdc-switch` | Mandatory, for the parent element.
`mdc-switch--disabled` | Optional, styles the switch as disabled
`mdc-switch--checked` | Optional, styles the switch as checked ("on")
`mdc-switch__track` | Mandatory, for the track element.
`mdc-switch__thumb-underlay` | Mandatory, for the ripple effect.
`mdc-switch__thumb` | Mandatory, for the thumb element.
Expand Down Expand Up @@ -133,23 +151,19 @@ If you are using a JavaScript framework, such as React or Angular, you can creat
| `addClass(className: string) => void` | Adds a class to the root element. |
| `removeClass(className: string) => void` | Removes a class from the root element. |
| `setNativeControlChecked(checked: boolean)` | Sets the checked state of the native control. |
| `isNativeControlChecked() => boolean` | Returns the checked state of the native control. |
| `setNativeControlDisabled(disabled: boolean)` | Sets the disabled state of the native control. |
| `isNativeControlDisabled() => boolean` | Returns the disabled state of the native control. |

### `MDCSwitchFoundation`

| Method Signature | Description |
| --- | --- |
| `isChecked() => boolean` | Returns whether the native control is checked. |
| `setChecked(checked: boolean) => void` | Sets the checked value of the native control and updates styling to reflect the checked state. |
| `isDisabled() => boolean` | Returns whether the native control is disabled. |
| `setDisabled(disabled: boolean) => void` | Sets the disabled value of the native control and updates styling to reflect the disabled state. |
| `handleChange() => void` | Handles a change event from the native control. |
| `handleChange(evt: Event) => void` | Handles a change event from the native control. |

### `MDCSwitchFoundation` Event Handlers
If wrapping the switch component it is necessary to add an event handler for native control change events that calls the `handleChange` foundation method. For an example of this, see the [MDCSwitch](index.js) component `initialSyncWithDOM` method.

| Event | Element Selector | Foundation Handler |
| --- | --- | --- |
| `change` | `.mdc-switch__native-control` | `handleChange()` |
| `change` | `.mdc-switch__native-control` | `handleChange()` |
6 changes: 0 additions & 6 deletions packages/mdc-switch/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,8 @@ class MDCSwitchAdapter {
/** @param {boolean} checked */
setNativeControlChecked(checked) {}

/** @return {boolean} checked */
isNativeControlChecked() {}

/** @param {boolean} disabled */
setNativeControlDisabled(disabled) {}

/** @return {boolean} disabled */
isNativeControlDisabled() {}
}

export default MDCSwitchAdapter;
23 changes: 3 additions & 20 deletions packages/mdc-switch/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,38 +40,20 @@ class MDCSwitchFoundation extends MDCFoundation {
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
setNativeControlChecked: (/* checked: boolean */) => {},
isNativeControlChecked: () => /* boolean */ {},
setNativeControlDisabled: (/* disabled: boolean */) => {},
isNativeControlDisabled: () => /* boolean */ {},
});
}

constructor(adapter) {
super(Object.assign(MDCSwitchFoundation.defaultAdapter, adapter));
}

/** @override */
init() {
// Do an initial state update based on the state of the native control.
this.handleChange();
}

/** @return {boolean} */
isChecked() {
return this.adapter_.isNativeControlChecked();
}

/** @param {boolean} checked */
setChecked(checked) {
this.adapter_.setNativeControlChecked(checked);
this.updateCheckedStyling_(checked);
}

/** @return {boolean} */
isDisabled() {
return this.adapter_.isNativeControlDisabled();
}

/** @param {boolean} disabled */
setDisabled(disabled) {
this.adapter_.setNativeControlDisabled(disabled);
Expand All @@ -84,9 +66,10 @@ class MDCSwitchFoundation extends MDCFoundation {

/**
* Handles the change event for the switch native control.
* @param {!Event} evt
*/
handleChange() {
this.updateCheckedStyling_(this.isChecked());
handleChange(evt) {
this.updateCheckedStyling_(evt.target.checked);
}

/**
Expand Down
11 changes: 7 additions & 4 deletions packages/mdc-switch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class MDCSwitch extends MDCComponent {
initialSyncWithDOM() {
this.changeHandler_ = this.foundation_.handleChange.bind(this.foundation_);
this.nativeControl_.addEventListener('change', this.changeHandler_);

// Sometimes the checked state of the input element is saved in the history.
// The switch styling should match the checked state of the input element.
// Do an initial sync between the native control and the foundation.
this.checked = this.checked;
}

/**
Expand Down Expand Up @@ -94,9 +99,7 @@ class MDCSwitch extends MDCComponent {
addClass: (className) => this.root_.classList.add(className),
removeClass: (className) => this.root_.classList.remove(className),
setNativeControlChecked: (checked) => this.nativeControl_.checked = checked,
isNativeControlChecked: () => this.nativeControl_.checked,
setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled,
isNativeControlDisabled: () => this.nativeControl_.disabled,
});
}

Expand All @@ -107,7 +110,7 @@ class MDCSwitch extends MDCComponent {

/** @return {boolean} */
get checked() {
return this.foundation_.isChecked();
return this.nativeControl_.checked;
}

/** @param {boolean} checked */
Expand All @@ -117,7 +120,7 @@ class MDCSwitch extends MDCComponent {

/** @return {boolean} */
get disabled() {
return this.foundation_.isDisabled();
return this.nativeControl_.disabled;
}

/** @param {boolean} disabled */
Expand Down
53 changes: 3 additions & 50 deletions test/unit/mdc-switch/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ 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', 'isNativeControlChecked',
'setNativeControlDisabled', 'isNativeControlDisabled']);
assert.deepEqual(methods, ['addClass', 'removeClass', 'setNativeControlChecked', 'setNativeControlDisabled']);
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
});

Expand All @@ -45,34 +44,6 @@ function setupTest() {
return {foundation, mockAdapter};
}

test('#init adds mdc-switch--checked to the switch element if the switch is initially checked', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(true);

foundation.init();
td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#init removes mdc-switch--checked from the switch element if the switch is initially unchecked', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(false);

foundation.init();
td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#isChecked returns true when the value of adapter.isChecked() is true', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(true);
assert.isOk(foundation.isChecked());
});

test('#isChecked returns false when the value of adapter.isChecked() is false', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(false);
assert.isNotOk(foundation.isChecked());
});

test('#setChecked updates the checked state', () => {
const {foundation, mockAdapter} = setupTest();
foundation.setChecked(true);
Expand All @@ -84,30 +55,16 @@ test('#setChecked updates the checked state', () => {

test('#setChecked adds mdc-switch--checked to the switch element when set to true', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(true);
foundation.setChecked(true);
td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#setChecked removes mdc-switch--checked from the switch element when set to false', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlChecked()).thenReturn(false);
foundation.setChecked(false);
td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#isDisabled returns true when adapter.isDisabled() is true', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlDisabled()).thenReturn(true);
assert.isOk(foundation.isDisabled());
});

test('#isDisabled returns false when adapter.isDisabled() is false', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.isNativeControlDisabled()).thenReturn(false);
assert.isNotOk(foundation.isDisabled());
});

test('#setDisabled updates the disabled state', () => {
const {foundation, mockAdapter} = setupTest();
foundation.setDisabled(true);
Expand All @@ -132,17 +89,13 @@ test('#setDisabled removes mdc-switch--disabled from the switch element when set
test('#handleChange adds mdc-switch--checked to the switch when it is a checked state', () => {
const {foundation, mockAdapter} = setupTest();

td.when(mockAdapter.isNativeControlChecked()).thenReturn(true);

foundation.handleChange();
foundation.handleChange({target: {checked: true}});
td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED));
});

test('#handleChange removes mdc-switch--checked from the switch when it is an unchecked state', () => {
const {foundation, mockAdapter} = setupTest();

td.when(mockAdapter.isNativeControlChecked()).thenReturn(false);

foundation.handleChange();
foundation.handleChange({target: {checked: false}});
td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED));
});
8 changes: 8 additions & 0 deletions test/unit/mdc-switch/mdc-switch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ function setupMockFoundationTest(root = getFixture()) {
return {root, component, mockFoundation};
}

test('#initialSyncWithDom calls foundation.setChecked', () => {
const root = getFixture();
const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR);
inputEl.checked = true;
const {mockFoundation} = setupMockFoundationTest(root);
td.verify(mockFoundation.setChecked(true), {times: 1});
});

test('change handler is added to the native control element', () => {
const {root, mockFoundation} = setupMockFoundationTest();

Expand Down

0 comments on commit e1e4465

Please sign in to comment.