Skip to content
This repository was archived by the owner on Jan 14, 2025. It is now read-only.

fix(checkbox): upgrade mdc-web to v1 #769

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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