Skip to content

Commit

Permalink
fix(checkbox): upgrade mdc-web to v1 (#769)
Browse files Browse the repository at this point in the history
  • Loading branch information
gugu authored and Matt Goo committed Apr 30, 2019
1 parent 1a04f3d commit 4f523e3
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 54 deletions.
72 changes: 52 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@material/base": "^1.0.0",
"@material/button": "^0.43.0",
"@material/card": "^0.41.0",
"@material/checkbox": "^0.41.0",
"@material/checkbox": "^1.0.0",
"@material/chips": "^1.0.0",
"@material/dialog": "^0.43.0",
"@material/dom": "^0.41.0",
Expand Down
35 changes: 22 additions & 13 deletions packages/checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@

import * as React from 'react';
import classnames from 'classnames';
// @ts-ignore no mdc .d.ts file
import {MDCCheckboxFoundation, MDCCheckboxAdapter} from '@material/checkbox/dist/mdc.checkbox';
import {MDCCheckboxFoundation} from '@material/checkbox/foundation';
import {MDCCheckboxAdapter} from '@material/checkbox/adapter';
import {cssClasses} from '@material/checkbox/constants';
import * as Ripple from '@material/react-ripple';

import NativeControl from './NativeControl';
Expand All @@ -45,20 +46,22 @@ interface CheckboxState {
checked?: boolean;
indeterminate?: boolean;
classList: Set<string>;
'aria-checked': boolean;
'aria-checked': string;
disabled: boolean;
};

export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
inputElement: React.RefObject<HTMLInputElement> = React.createRef();
foundation = MDCCheckboxFoundation;
foundation!: MDCCheckboxFoundation;

constructor(props: CheckboxProps) {
super(props);
this.state = {
'checked': props.checked,
'indeterminate': props.indeterminate,
'classList': new Set(),
'aria-checked': false,
'aria-checked': 'false',
'disabled': props.disabled!,
};
}

Expand All @@ -74,7 +77,7 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
componentDidMount() {
this.foundation = new MDCCheckboxFoundation(this.adapter);
this.foundation.init();
this.foundation.setDisabled(this.props.disabled);
this.foundation.setDisabled(this.props.disabled!);
// indeterminate property on checkboxes is not supported:
// https://github.com/facebook/react/issues/1798#issuecomment-333414857
if (this.inputElement.current) {
Expand All @@ -91,7 +94,7 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
this.handleChange(checked!, indeterminate!);
}
if (disabled !== prevProps.disabled) {
this.foundation.setDisabled(disabled);
this.foundation.setDisabled(disabled!);
}
}

Expand All @@ -118,7 +121,9 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
get classes(): string {
const {classList} = this.state;
const {className} = this.props;
return classnames('mdc-checkbox', Array.from(classList), className);
return classnames(
'mdc-checkbox', Array.from(classList),
this.state.disabled ? cssClasses.DISABLED : null, className);
}

updateState = (key: keyof CheckboxState, value: string | boolean) => {
Expand Down Expand Up @@ -146,10 +151,14 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
// isAttachedToDOM will likely be removed
// https://github.com/material-components/material-components-web/issues/3691
isAttachedToDOM: () => true,
isChecked: () => this.state.checked,
isIndeterminate: () => this.state.indeterminate,
isChecked: () => this.state.checked!,
isIndeterminate: () => this.state.indeterminate!,
setNativeControlAttr: this.updateState,
setNativeControlDisabled: (disabled) => {
this.updateState('disabled', disabled);
},
removeNativeControlAttr: this.removeState,
forceLayout: () => null,
};
}

Expand All @@ -169,8 +178,8 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
initRipple,
onChange,
unbounded,
/* eslint-enable no-unused-vars */
disabled,
/* eslint-enable no-unused-vars */
nativeControlId,
name,
...otherProps
Expand All @@ -186,8 +195,8 @@ export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
<NativeControl
id={nativeControlId}
checked={this.state.checked}
disabled={disabled}
aria-checked={this.state['aria-checked'] || this.state.checked}
disabled={this.state.disabled}
aria-checked={(this.state['aria-checked'] || this.state.checked!.toString()) as ('true' | 'false')}
name={name}
onChange={this.onChange}
rippleActivatorRef={this.inputElement}
Expand Down
2 changes: 1 addition & 1 deletion packages/checkbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"url": "https://github.com/material-components/material-components-web-react.git"
},
"dependencies": {
"@material/checkbox": "^0.41.0",
"@material/checkbox": "^1.1.0",
"@material/react-ripple": "^0.11.0",
"classnames": "^2.2.6",
"react": "^16.3.2"
Expand Down
57 changes: 38 additions & 19 deletions test/unit/checkbox/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import {assert} from 'chai';
import {shallow} from 'enzyme';
import * as td from 'testdouble';
import {Checkbox} from '../../../packages/checkbox/index';
import {MDCCheckboxAdapter} from '@material/checkbox/adapter';
import {coerceForTesting} from '../helpers/types';

suite('Checkbox');

const getAdapter = (instance: Checkbox): MDCCheckboxAdapter => {
// @ts-ignore adapter_ is a protected property, we need to override it
return instance.foundation.adapter_;
};

test('creates foundation', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
assert.exists(wrapper.instance().foundation);
Expand Down Expand Up @@ -34,12 +40,29 @@ test('has disabled class when props.disabled is true', () => {
);
});

test('has disabled class when foundation calls setDisabled is true', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
getAdapter(wrapper.instance()).setNativeControlDisabled(true);
wrapper.update();
assert.isTrue(
wrapper.find('.mdc-checkbox').hasClass('mdc-checkbox--disabled')
);
});

test('native control props.disabled is true when props.disabled is true', () => {
const wrapper = shallow(<Checkbox disabled />);
const nativeControl = wrapper.childAt(0);
assert.isTrue(nativeControl.props().disabled);
});

test('native control props.disabled when foundation calls setDisabled is true', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
getAdapter(wrapper.instance()).setNativeControlDisabled(true);
wrapper.update();
const nativeControl = wrapper.childAt(0);
assert.isTrue(nativeControl.props().disabled);
});

test('native control props.checked is true when props.checked is true', () => {
const wrapper = shallow(<Checkbox checked />);
const nativeControl = wrapper.childAt(0);
Expand All @@ -48,84 +71,80 @@ test('native control props.checked is true when props.checked is true', () => {

test('#foundation.handleChange gets called when prop.checked updates', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.instance().foundation.handleChange = td.func();
wrapper.instance().foundation.handleChange = td.func<() => null>();
wrapper.setProps({checked: true});
td.verify(wrapper.instance().foundation.handleChange(), {times: 1});
});

test('#foundation.handleChange gets called when prop.indeterminate updates', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.instance().foundation.handleChange = td.func();
wrapper.instance().foundation.handleChange = td.func<() => null>();
wrapper.setProps({indeterminate: true});
td.verify(wrapper.instance().foundation.handleChange(), {times: 1});
});

test('#foundation.setDisabled gets called when prop.disabled updates', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.instance().foundation.setDisabled = td.func();
wrapper.instance().foundation.setDisabled = td.func<(disabled: boolean) => null>();
wrapper.setProps({disabled: true});
td.verify(wrapper.instance().foundation.setDisabled(true), {times: 1});
});

test('#componentWillUnmount destroys foundation', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
const foundation = wrapper.instance().foundation;
foundation.destroy = td.func();
foundation.destroy = td.func<() => void>();
wrapper.unmount();
td.verify(foundation.destroy(), {times: 1});
});

test('#adapter.addClass adds class to state.classList', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.instance().foundation.adapter_.addClass('test-class-name');
getAdapter(wrapper.instance()).addClass('test-class-name');
assert.isTrue(wrapper.state().classList.has('test-class-name'));
});

test('#adapter.removeClass removes class from state.classList', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({classList: new Set(['test-class-name'])});
wrapper.instance().foundation.adapter_.removeClass('test-class-name');
getAdapter(wrapper.instance()).removeClass('test-class-name');
assert.isFalse(wrapper.state().classList.has('test-class-name'));
});

test('#adapter.isChecked returns state.checked if true', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({checked: true});
assert.isTrue(wrapper.instance().foundation.adapter_.isChecked());
assert.isTrue(getAdapter(wrapper.instance()).isChecked());
});

test('#adapter.isChecked returns state.checked if false', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({checked: false});
assert.isFalse(wrapper.instance().foundation.adapter_.isChecked());
assert.isFalse(getAdapter(wrapper.instance()).isChecked());
});

test('#adapter.isIndeterminate returns state.indeterminate if true', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({indeterminate: true});
assert.isTrue(wrapper.instance().foundation.adapter_.isIndeterminate());
assert.isTrue(getAdapter(wrapper.instance()).isIndeterminate());
});

test('#adapter.isIndeterminate returns state.indeterminate if false', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({indeterminate: false});
assert.isFalse(wrapper.instance().foundation.adapter_.isIndeterminate());
assert.isFalse(getAdapter(wrapper.instance()).isIndeterminate());
});

test('#adapter.setNativeControlAttr sets aria-checked state', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper
.instance()
.foundation.adapter_.setNativeControlAttr('aria-checked', true);
assert.isTrue(wrapper.state()['aria-checked']);
getAdapter(wrapper.instance()).setNativeControlAttr('aria-checked', 'true');
assert.equal(wrapper.state()['aria-checked'], 'true');
});

test('#adapter.removeNativeControlAttr sets aria-checked state as false', () => {
const wrapper = shallow<Checkbox>(<Checkbox />);
wrapper.setState({'aria-checked': true});
wrapper
.instance()
.foundation.adapter_.removeNativeControlAttr('aria-checked');
wrapper.setState({'aria-checked': 'true'});
getAdapter(wrapper.instance()).removeNativeControlAttr('aria-checked');
assert.isFalse(wrapper.state()['aria-checked']);
});

Expand All @@ -148,7 +167,7 @@ test('calls foundation.handleChange in native control props.onChange', () => {
indeterminate: false,
},
};
wrapper.instance().foundation.handleChange = td.func();
wrapper.instance().foundation.handleChange = td.func<() => void>();
nativeControl.simulate('change', mockEvt);
td.verify(wrapper.instance().foundation.handleChange(), {times: 1});
});
Expand Down

0 comments on commit 4f523e3

Please sign in to comment.