Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(switch): Move component specific logic out of foundation #3342

Merged
merged 8 commits into from
Aug 15, 2018
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 `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 `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