diff --git a/packages/checkbox/NativeControl.js b/packages/checkbox/NativeControl.tsx similarity index 71% rename from packages/checkbox/NativeControl.js rename to packages/checkbox/NativeControl.tsx index 082cf9dd6..dbb6e4b16 100644 --- a/packages/checkbox/NativeControl.js +++ b/packages/checkbox/NativeControl.tsx @@ -19,17 +19,25 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import * as React from 'react'; -import React from 'react'; -import PropTypes from 'prop-types'; +export interface NativeControlProps extends React.HTMLProps{ + checked: boolean; + disabled: boolean; + id: string; + rippleActivatorRef: React.RefObject; + onChange: (evt: React.ChangeEvent) => void; +}; -export class NativeControl extends React.Component { - render() { - const { - rippleActivatorRef, - ...otherProps - } = this.props; +export class NativeControl extends React.Component { + static defaultProps: Partial = { + checked: false, + disabled: false, + onChange: () => {}, + }; + render() { + const {rippleActivatorRef, ...otherProps} = this.props; return ( ); } -}; - -NativeControl.propTypes = { - checked: PropTypes.bool, - disabled: PropTypes.bool, - id: PropTypes.string, - rippleActivatorRef: PropTypes.object, -}; - -NativeControl.defaultProps = { - checked: false, - disabled: false, - id: null, - rippleActivatorRef: null, -}; +} export default NativeControl; diff --git a/packages/checkbox/index.js b/packages/checkbox/index.tsx similarity index 60% rename from packages/checkbox/index.js rename to packages/checkbox/index.tsx index a264a2c96..5dc2d7953 100644 --- a/packages/checkbox/index.js +++ b/packages/checkbox/index.tsx @@ -19,28 +19,57 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - -import React, {Component} from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import {MDCCheckboxFoundation} from '@material/checkbox/dist/mdc.checkbox'; +import * as React from 'react'; +import * as classnames from 'classnames'; +// @ts-ignore +import {MDCCheckboxFoundation, MDCCheckboxAdapter} from '@material/checkbox/dist/mdc.checkbox'; +// TODO: fix with #528 +// @ts-ignore import withRipple from '@material/react-ripple'; - import NativeControl from './NativeControl'; -export class Checkbox extends Component { - constructor(props) { +export interface CheckboxProps { + checked: boolean; + className: string; + disabled: boolean; + indeterminate: boolean; + nativeControlId?: string; + onChange: (evt: React.ChangeEvent) => void; + initRipple: (surface: HTMLDivElement, activator: HTMLInputElement) => void; + unbounded: boolean; +}; + +interface CheckboxState { + checked: boolean; + indeterminate: boolean; + classList: Set; + 'aria-checked': boolean; +}; + +export class Checkbox extends React.Component { + inputElement_: React.RefObject = React.createRef(); + foundation_ = MDCCheckboxFoundation; + + constructor(props: CheckboxProps) { super(props); - this.inputElement_ = React.createRef(); - this.foundation_ = null; this.state = { - checked: props.checked, - indeterminate: props.indeterminate, - classList: new Set(), - ['aria-checked']: false, + 'checked': props.checked, + 'indeterminate': props.indeterminate, + 'classList': new Set(), + 'aria-checked': false, }; } + static defaultProps: Partial = { + checked: false, + className: '', + disabled: false, + indeterminate: false, + onChange: () => {}, + initRipple: () => {}, + unbounded: true, + }; + componentDidMount() { this.foundation_ = new MDCCheckboxFoundation(this.adapter); this.foundation_.init(); @@ -52,14 +81,12 @@ export class Checkbox extends Component { } } - componentDidUpdate(prevProps) { - const { - checked, - indeterminate, - disabled, - } = this.props; - - if (checked !== prevProps.checked || indeterminate !== prevProps.indeterminate) { + componentDidUpdate(prevProps: CheckboxProps) { + const {checked, indeterminate, disabled} = this.props; + if ( + checked !== prevProps.checked || + indeterminate !== prevProps.indeterminate + ) { this.handleChange(checked, indeterminate); } if (disabled !== prevProps.disabled) { @@ -73,33 +100,43 @@ export class Checkbox extends Component { } } - init = (el) => { + init = (el: HTMLDivElement) => { + if (!this.inputElement_.current) return; this.props.initRipple(el, this.inputElement_.current); - } + }; - handleChange = (checked, indeterminate) => { + handleChange = (checked: boolean, indeterminate: boolean) => { this.setState({checked, indeterminate}, () => { this.foundation_.handleChange(); if (this.inputElement_.current) { this.inputElement_.current.indeterminate = indeterminate; } }); - } + }; - get classes() { + get classes(): string { const {classList} = this.state; const {className} = this.props; return classnames('mdc-checkbox', Array.from(classList), className); } - get adapter() { + updateState = (key: keyof CheckboxState, value: string | boolean) => { + this.setState((prevState) => ({ + ...prevState, + [key]: value, + })); + } + + removeState = (key: keyof CheckboxState) => this.updateState(key, false); + + get adapter(): MDCCheckboxAdapter { return { - addClass: (className) => { + addClass: (className: string) => { const {classList} = this.state; classList.add(className); this.setState({classList}); }, - removeClass: (className) => { + removeClass: (className: string) => { const {classList} = this.state; classList.delete(className); this.setState({classList}); @@ -110,11 +147,18 @@ export class Checkbox extends Component { isAttachedToDOM: () => true, isChecked: () => this.state.checked, isIndeterminate: () => this.state.indeterminate, - setNativeControlAttr: (attr, value) => this.setState({[attr]: value}), - removeNativeControlAttr: (attr) => this.setState({[attr]: false}), + setNativeControlAttr: this.updateState, + removeNativeControlAttr: this.removeState, }; } + onChange = (evt: React.ChangeEvent) => { + const {onChange} = this.props; + const {checked, indeterminate} = evt.target; + this.handleChange(checked, indeterminate); + onChange(evt); + } + render() { const { /* eslint-disable no-unused-vars */ @@ -122,11 +166,11 @@ export class Checkbox extends Component { checked, indeterminate, initRipple, + onChange, unbounded, /* eslint-enable no-unused-vars */ disabled, nativeControlId, - onChange, ...otherProps } = this.props; @@ -142,49 +186,26 @@ export class Checkbox extends Component { checked={this.state.checked} disabled={disabled} aria-checked={this.state['aria-checked']} - onChange={(evt) => { - const {checked, indeterminate} = evt.target; - this.handleChange(checked, indeterminate); - onChange(evt); - }} + onChange={this.onChange} rippleActivatorRef={this.inputElement_} />
- - + d='M1.73,12.91 8.1,19.28 22.79,4.59' + /> -
+
); } } -Checkbox.propTypes = { - checked: PropTypes.bool, - className: PropTypes.string, - disabled: PropTypes.bool, - indeterminate: PropTypes.bool, - nativeControlId: PropTypes.string, - onChange: PropTypes.func, - initRipple: PropTypes.func, - unbounded: PropTypes.bool, -}; - -Checkbox.defaultProps = { - checked: false, - className: '', - disabled: false, - indeterminate: false, - nativeControlId: null, - onChange: () => {}, - initRipple: () => {}, - unbounded: true, -}; - export default withRipple(Checkbox); diff --git a/test/screenshot/checkbox/index.js b/test/screenshot/checkbox/index.tsx similarity index 72% rename from test/screenshot/checkbox/index.js rename to test/screenshot/checkbox/index.tsx index cc9b6cd45..9185441fc 100644 --- a/test/screenshot/checkbox/index.js +++ b/test/screenshot/checkbox/index.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import * as React from 'react'; import './index.scss'; - import Checkbox from '../../../packages/checkbox/index'; -const CheckboxScreenshotTest = () => { +const CheckboxScreenshotTest: React.FunctionComponent = () => { return (
@@ -18,10 +17,13 @@ const CheckboxScreenshotTest = () => {
- + ); }; - export default CheckboxScreenshotTest; diff --git a/test/unit/checkbox/NativeControl.test.js b/test/unit/checkbox/NativeControl.test.tsx similarity index 78% rename from test/unit/checkbox/NativeControl.test.js rename to test/unit/checkbox/NativeControl.test.tsx index 43471ffaf..b5887707b 100644 --- a/test/unit/checkbox/NativeControl.test.js +++ b/test/unit/checkbox/NativeControl.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; import NativeControl from '../../../packages/checkbox/NativeControl'; @@ -6,6 +6,6 @@ import NativeControl from '../../../packages/checkbox/NativeControl'; suite('Checkbox Native Control'); test('has mdc-checkbox__native-control class', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.isTrue(wrapper.hasClass('mdc-checkbox__native-control')); }); diff --git a/test/unit/checkbox/index.test.js b/test/unit/checkbox/index.test.tsx similarity index 76% rename from test/unit/checkbox/index.test.js rename to test/unit/checkbox/index.test.tsx index 05cd2d4a5..cc33555fb 100644 --- a/test/unit/checkbox/index.test.js +++ b/test/unit/checkbox/index.test.tsx @@ -1,13 +1,13 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import td from 'testdouble'; +import * as td from 'testdouble'; import {Checkbox} from '../../../packages/checkbox/index'; suite('Checkbox'); test('creates foundation', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.exists(wrapper.instance().foundation_); }); @@ -28,7 +28,9 @@ test('classNames adds classes', () => { test('has disabled class when props.disabled is true', () => { const wrapper = shallow(); - assert.isTrue(wrapper.find('.mdc-checkbox').hasClass('mdc-checkbox--disabled')); + assert.isTrue( + wrapper.find('.mdc-checkbox').hasClass('mdc-checkbox--disabled') + ); }); test('native control props.disabled is true when props.disabled is true', () => { @@ -44,28 +46,28 @@ test('native control props.checked is true when props.checked is true', () => { }); test('#foundation_.handleChange gets called when prop.checked updates', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().foundation_.handleChange = td.func(); wrapper.setProps({checked: true}); td.verify(wrapper.instance().foundation_.handleChange(), {times: 1}); }); test('#foundation_.handleChange gets called when prop.indeterminate updates', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().foundation_.handleChange = td.func(); wrapper.setProps({indeterminate: true}); td.verify(wrapper.instance().foundation_.handleChange(), {times: 1}); }); test('#foundation_.setDisabled gets called when prop.disabled updates', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().foundation_.setDisabled = td.func(); wrapper.setProps({disabled: true}); td.verify(wrapper.instance().foundation_.setDisabled(true), {times: 1}); }); test('#componentWillUnmount destroys foundation', () => { - const wrapper = shallow(); + const wrapper = shallow(); const foundation = wrapper.instance().foundation_; foundation.destroy = td.func(); wrapper.unmount(); @@ -73,62 +75,66 @@ test('#componentWillUnmount destroys foundation', () => { }); test('#adapter.addClass adds class to state.classList', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().foundation_.adapter_.addClass('test-class-name'); assert.isTrue(wrapper.state().classList.has('test-class-name')); }); test('#adapter.removeClass removes class from state.classList', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({classList: new Set(['test-class-name'])}); wrapper.instance().foundation_.adapter_.removeClass('test-class-name'); assert.isFalse(wrapper.state().classList.has('test-class-name')); }); test('#adapter.isChecked returns state.checked if true', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({checked: true}); assert.isTrue(wrapper.instance().foundation_.adapter_.isChecked()); }); test('#adapter.isChecked returns state.checked if false', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({checked: false}); assert.isFalse(wrapper.instance().foundation_.adapter_.isChecked()); }); test('#adapter.isIndeterminate returns state.indeterminate if true', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({indeterminate: true}); assert.isTrue(wrapper.instance().foundation_.adapter_.isIndeterminate()); }); test('#adapter.isIndeterminate returns state.indeterminate if false', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({indeterminate: false}); assert.isFalse(wrapper.instance().foundation_.adapter_.isIndeterminate()); }); test('#adapter.setNativeControlAttr sets aria-checked state', () => { - const wrapper = shallow(); - wrapper.instance().foundation_.adapter_.setNativeControlAttr('aria-checked', true); + const wrapper = shallow(); + wrapper + .instance() + .foundation_.adapter_.setNativeControlAttr('aria-checked', true); assert.isTrue(wrapper.state()['aria-checked']); }); test('#adapter.removeNativeControlAttr sets aria-checked state as false', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setState({'aria-checked': true}); - wrapper.instance().foundation_.adapter_.removeNativeControlAttr('aria-checked'); + wrapper + .instance() + .foundation_.adapter_.removeNativeControlAttr('aria-checked'); assert.isFalse(wrapper.state()['aria-checked']); }); test('passes nativeControlId to NativeControl through props', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.equal(wrapper.childAt(0).props().id, 'test-id'); }); test('calls foundation.handleChange in native control props.onChange', () => { - const wrapper = shallow(); + const wrapper = shallow(); const nativeControl = wrapper.childAt(0); const mockEvt = { target: { @@ -142,15 +148,15 @@ test('calls foundation.handleChange in native control props.onChange', () => { }); test('calls props.onChange in native control props.onChange', () => { - const onChange = td.func(); - const wrapper = shallow(); + const onChange = td.func() as (evt: React.ChangeEvent) => void; + const wrapper = shallow(); const nativeControl = wrapper.childAt(0); - const mockEvt = { - target: { + const mockEvt = ({ + target: ({ checked: true, indeterminate: false, - }, - }; + } as HTMLInputElement), + } as React.ChangeEvent); nativeControl.simulate('change', mockEvt); td.verify(onChange(mockEvt), {times: 1}); }); diff --git a/tsconfig.json b/tsconfig.json index 3eb6f6f0b..bc8214fd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,10 @@ "jsx": "react", "target": "es5", "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true }, + "compileOnSave": true, "exclude": [ "node_modules", "build",