diff --git a/.eslintrc.js b/.eslintrc.js index b355580c766d45..5bf4781d3a0baa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,7 @@ module.exports = { 'no-console': 'error', // airbnb is using warn 'no-param-reassign': 'off', 'no-prototype-builtins': 'off', + 'no-use-before-define': ['error', { 'functions': false }], // airbnb have functions: true, annoying 'object-curly-spacing': 'off', // use babel plugin rule 'operator-linebreak': ['error', 'after'], // aibnb is disabling this rule 'babel/object-curly-spacing': ['error', 'always'], diff --git a/docs/site/src/demos/dialogs/ConfirmationDialog.js b/docs/site/src/demos/dialogs/ConfirmationDialog.js index 91a963323886fb..a57423cae8dfa6 100644 --- a/docs/site/src/demos/dialogs/ConfirmationDialog.js +++ b/docs/site/src/demos/dialogs/ConfirmationDialog.js @@ -11,7 +11,7 @@ import { DialogActions, DialogTitle, } from 'material-ui/Dialog'; -import { Radio, RadioGroup } from 'material-ui/Radio'; +import { LabelRadio as Radio, RadioGroup } from 'material-ui/Radio'; const options = [ 'None', @@ -82,6 +82,7 @@ class ConfirmationDialog extends Component { { this.radioGroup = c; }} aria-label="Gender" + name="gender" selectedValue={this.state.selectedValue} onChange={this.handleChange} > diff --git a/docs/site/src/demos/selection-controls/Checkboxes.js b/docs/site/src/demos/selection-controls/Checkboxes.js index a04170616478e5..1f556d0b60dc1c 100644 --- a/docs/site/src/demos/selection-controls/Checkboxes.js +++ b/docs/site/src/demos/selection-controls/Checkboxes.js @@ -1,7 +1,7 @@ // @flow weak import React, { Component } from 'react'; -import Checkbox from 'material-ui/Checkbox'; +import { LabelCheckbox } from 'material-ui/Checkbox'; import { FormGroup } from 'material-ui/Form'; export default class Checkboxes extends Component { @@ -13,28 +13,28 @@ export default class Checkboxes extends Component { render() { return ( - this.setState({ checkedA: checked })} label="Option A" value="checkedA" /> - this.setState({ checkedB: checked })} label="Option B" value="checkedB" /> - - - ({ @@ -32,14 +32,15 @@ export default class RadioButtonsGroup extends Component { Gender - - - - + + + + ); diff --git a/docs/site/src/demos/selection-controls/SwitchLabels.js b/docs/site/src/demos/selection-controls/SwitchLabels.js index 8f93e88bf1b50c..aa97fb6b4bc71f 100644 --- a/docs/site/src/demos/selection-controls/SwitchLabels.js +++ b/docs/site/src/demos/selection-controls/SwitchLabels.js @@ -1,7 +1,7 @@ // @flow weak import React, { Component } from 'react'; -import Switch from 'material-ui/Switch'; +import { LabelSwitch } from 'material-ui/Switch'; export default class SwitchLabels extends Component { state = { @@ -12,18 +12,17 @@ export default class SwitchLabels extends Component { render() { return (
- this.setState({ checkedA: checked })} label="A" - labelReverse /> - this.setState({ checkedB: checked })} label="B" /> - +
); } diff --git a/src/Checkbox/Checkbox.js b/src/Checkbox/Checkbox.js index 33b21361323a65..e3752e0d1cdf77 100644 --- a/src/Checkbox/Checkbox.js +++ b/src/Checkbox/Checkbox.js @@ -1,10 +1,8 @@ // @flow weak -import React, { PropTypes } from 'react'; import { createStyleSheet } from 'jss-theme-reactor'; -import classNames from 'classnames'; -import SwitchBase from '../internal/SwitchBase'; -import SelectionLabel from '../internal/SelectionLabel'; +import { createSwitch } from '../internal/SwitchBase'; +import withSwitchLabel from '../internal/withSwitchLabel'; export const styleSheet = createStyleSheet('Checkbox', (theme) => { return { @@ -20,79 +18,12 @@ export const styleSheet = createStyleSheet('Checkbox', (theme) => { }; }); -export default function Checkbox(props, context) { - const { - className, - checkedClassName, - disabled, - disabledClassName, - label, - labelClassName, - labelReverse, - ...other - } = props; - const classes = context.styleManager.render(styleSheet); +const Checkbox = createSwitch({ styleSheet }); - const switchProps = { - className: classNames(classes.default, className), - checkedClassName: classNames(classes.checked, checkedClassName), - disabled, - disabledClassName: classNames(classes.disabled, disabledClassName), - ...other, - }; - - if (label) { - return ( - - - - ); - } - - return ; -} +Checkbox.displayName = 'Checkbox'; -Checkbox.propTypes = { - checkedClassName: PropTypes.string, - /** - * The CSS class name of the root element. - */ - className: PropTypes.string, - /** - * If `true`, the control will be disabled. - */ - disabled: PropTypes.bool, - /** - * The CSS class name of the switch element when disabled. - */ - disabledClassName: PropTypes.string, - /** - * The text to be used in an enclosing label element. - */ - label: PropTypes.node, - /** - * The className to be used in an enclosing label element. - */ - labelClassName: PropTypes.string, - /** - * Will reverse the order of the element and the label. - */ - labelReverse: PropTypes.bool, -}; +export default Checkbox; -Checkbox.defaultProps = { - disabled: false, - labelReverse: false, -}; +const LabelCheckbox = withSwitchLabel(Checkbox); -Checkbox.contextTypes = { - styleManager: PropTypes.object.isRequired, -}; +export { LabelCheckbox }; diff --git a/src/Checkbox/Checkbox.spec.js b/src/Checkbox/Checkbox.spec.js index fd7e6a3795adee..7c01873db54695 100644 --- a/src/Checkbox/Checkbox.spec.js +++ b/src/Checkbox/Checkbox.spec.js @@ -1,10 +1,9 @@ // @flow weak /* eslint-env mocha */ -import React from 'react'; import { assert } from 'chai'; import { createShallowWithContext } from 'test/utils'; -import Checkbox, { styleSheet } from './Checkbox'; +import Checkbox, { LabelCheckbox, styleSheet } from './Checkbox'; describe('', () => { let shallow; @@ -15,68 +14,25 @@ describe('', () => { classes = shallow.context.styleManager.render(styleSheet); }); - it('should render a SwitchBase when label not present', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('SwitchBase'), true, 'should be a SwitchBase'); - }); - - it('should render a label', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('SelectionLabel'), true, 'should be a SelectionLabel'); - }); - - it('should render with the default and checked classes', () => { - const wrapper = shallow( - , - ); - const switchBase = wrapper.find('SwitchBase'); - assert.strictEqual(wrapper.hasClass('foo'), true, 'should have the "foo" class'); - assert.strictEqual(switchBase.hasClass('woof'), true, 'should have the "woof" class'); - assert.strictEqual(switchBase.hasClass(classes.default), true, 'should have the default class'); - assert.strictEqual( - switchBase.prop('checkedClassName').indexOf('meow') !== -1, - true, - 'should have the "meow" class', - ); - assert.strictEqual( - switchBase.prop('checkedClassName').indexOf(classes.checked) !== -1, - true, - 'should have the checked class', - ); - }); - - it('should spread custom props on the switchBase node', () => { - const wrapper = shallow(); - const switchBase = wrapper.find('SwitchBase'); - assert.strictEqual(switchBase.prop('data-my-prop'), 'woof', 'custom prop should be woof'); + describe('styleSheet', () => { + it('should have the classes required for SwitchBase', () => { + assert.strictEqual(typeof classes.default, 'string'); + assert.strictEqual(typeof classes.checked, 'string'); + assert.strictEqual(typeof classes.disabled, 'string'); + }); }); - describe('prop: disabled', () => { - it('should disable the component', () => { - const wrapper = shallow(); - assert.strictEqual(wrapper.props().disabled, true, 'should pass the property down the tree'); + describe('default Checkbox export', () => { + it('should be a SwitchBase with the displayName set for debugging', () => { + assert.strictEqual(Checkbox.name, 'SwitchBase'); + assert.strictEqual(Checkbox.displayName, 'Checkbox'); }); }); - describe('prop: disabledClassName', () => { - it('should provide the class', () => { - const className = 'foo'; - const wrapper = shallow(); - assert.strictEqual( - wrapper.find('SwitchBase').props().disabledClassName.indexOf(className) !== -1, - true, - 'should have the custom disabled class', - ); + describe('named LabelCheckbox export', () => { + it('should be Checkbox wrapped with SwitchLabel', () => { + assert.strictEqual(LabelCheckbox.name, 'SwitchLabel'); + assert.strictEqual(LabelCheckbox.displayName, 'withSwitchLabel(Checkbox)'); }); }); }); diff --git a/src/Checkbox/index.js b/src/Checkbox/index.js index e343c47d91c063..92279045609180 100644 --- a/src/Checkbox/index.js +++ b/src/Checkbox/index.js @@ -1,4 +1,4 @@ /* eslint-disable flowtype/require-valid-file-annotation */ export default from './Checkbox'; -export Checkbox from './Checkbox'; +export Checkbox, { LabelCheckbox } from './Checkbox'; diff --git a/src/Form/FormGroup.js b/src/Form/FormGroup.js index 710478634d9579..91c67b41727399 100644 --- a/src/Form/FormGroup.js +++ b/src/Form/FormGroup.js @@ -21,14 +21,14 @@ export const styleSheet = createStyleSheet('FormGroup', () => { * awareness. Upon focusing on one of the child controls, it will propagate `focused` to the label. */ export default function FormGroup(props, context) { - const { className, children, row } = props; + const { className, children, row, ...other } = props; const classes = context.styleManager.render(styleSheet); const rootClassName = classNames(classes.root, { [classes.row]: row, }, className); return ( -
+
{children}
); diff --git a/src/IconButton/IconButton.js b/src/IconButton/IconButton.js index 832020b7267b9f..31da2c96e53eff 100644 --- a/src/IconButton/IconButton.js +++ b/src/IconButton/IconButton.js @@ -62,6 +62,7 @@ export const styleSheet = createStyleSheet('IconButton', (theme) => { export default function IconButton(props, context) { const { accent, + buttonRef, children, className, contrast, @@ -80,6 +81,7 @@ export default function IconButton(props, context) { centerRipple keyboardFocusedClassName={classes.keyboardFocused} disabled={disabled} + ref={buttonRef} {...other} > @@ -96,6 +98,10 @@ IconButton.propTypes = { * If true, will use the theme's accent color. */ accent: PropTypes.bool, + /** + * @ignore + */ + buttonRef: PropTypes.func, /** * The icon element. If a string is passed, * it will be used as a material icon font ligature. diff --git a/src/Radio/Radio.js b/src/Radio/Radio.js index e279bbe48fa140..4d209c9e3c227e 100644 --- a/src/Radio/Radio.js +++ b/src/Radio/Radio.js @@ -1,10 +1,8 @@ // @flow weak -import React, { PropTypes } from 'react'; import { createStyleSheet } from 'jss-theme-reactor'; -import classNames from 'classnames'; -import SwitchBase from '../internal/SwitchBase'; -import SelectionLabel from '../internal/SelectionLabel'; +import { createSwitch } from '../internal/SwitchBase'; +import withSwitchLabel from '../internal/withSwitchLabel'; export const styleSheet = createStyleSheet('Radio', (theme) => { return { @@ -20,90 +18,17 @@ export const styleSheet = createStyleSheet('Radio', (theme) => { }; }); -export default function Radio(props, context) { - const { - className, - checkedClassName, - disabled, - disabledClassName, - label, - labelClassName, - labelReverse, - onChange, - value, - ...other - } = props; - const classes = context.styleManager.render(styleSheet); - - const switchProps = { - className: classNames(classes.default, className), - checkedClassName: classNames(classes.checked, checkedClassName), - icon: 'radio_button_unchecked', - checkedIcon: 'radio_button_checked', - type: 'radio', - value, - onChange, - disabled, - disabledClassName: classNames(classes.disabled, disabledClassName), - ...other, - }; - - if (label) { - return ( - - - - ); - } - - return ; -} +const Radio = createSwitch({ + styleSheet, + inputType: 'radio', + defaultIcon: 'radio_button_unchecked', + defaultCheckedIcon: 'radio_button_checked', +}); +Radio.displayName = 'Radio'; -Radio.propTypes = { - checkedClassName: PropTypes.string, - /** - * The CSS class name of the root element. - */ - className: PropTypes.string, - /** - * If `true`, the control will be disabled. - */ - disabled: PropTypes.bool, - /** - * The CSS class name of the switch element when disabled. - */ - disabledClassName: PropTypes.string, - /** - * The text to be used in an enclosing label element. - */ - label: PropTypes.node, - /** - * The className to be used in an enclosing label element. - */ - labelClassName: PropTypes.string, - /** - * Will reverse the order of the element and the label. - */ - labelReverse: PropTypes.bool, - name: PropTypes.string, - onChange: PropTypes.func, - value: PropTypes.string, -}; +export default Radio; -Radio.defaultProps = { - disabled: false, - labelReverse: false, -}; +const LabelRadio = withSwitchLabel(Radio); -Radio.contextTypes = { - styleManager: PropTypes.object.isRequired, -}; +export { LabelRadio }; diff --git a/src/Radio/Radio.spec.js b/src/Radio/Radio.spec.js index ee0d5f8820336b..5d0d8c22abf7c0 100644 --- a/src/Radio/Radio.spec.js +++ b/src/Radio/Radio.spec.js @@ -1,10 +1,9 @@ // @flow weak /* eslint-env mocha */ -import React from 'react'; import { assert } from 'chai'; import { createShallowWithContext } from 'test/utils'; -import Radio, { styleSheet } from './Radio'; +import Radio, { LabelRadio, styleSheet } from './Radio'; describe('', () => { let shallow; @@ -15,58 +14,25 @@ describe('', () => { classes = shallow.context.styleManager.render(styleSheet); }); - it('should render a SwitchBase when label not present', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('SwitchBase'), true, 'should be a SwitchBase'); - }); - - it('should render a label', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('SelectionLabel'), true, 'should be a SelectionLabel'); - }); - - it('should render with the default and checked classes', () => { - const wrapper = shallow(); - assert.strictEqual(wrapper.hasClass('woof'), true, 'should have the "woof" class'); - assert.strictEqual(wrapper.hasClass(classes.default), true, 'should have the default class'); - assert.strictEqual( - wrapper.prop('checkedClassName').indexOf('meow') !== -1, - true, - 'should have the "meow" class', - ); - assert.strictEqual( - wrapper.prop('checkedClassName').indexOf(classes.checked) !== -1, - true, - 'should have the checked class', - ); - }); - - it('should spread custom props on the switchBase node', () => { - const wrapper = shallow(); - const switchBase = wrapper.find('SwitchBase'); - assert.strictEqual(switchBase.prop('data-my-prop'), 'woof', 'custom prop should be woof'); + describe('styleSheet', () => { + it('should have the classes required for SwitchBase', () => { + assert.strictEqual(typeof classes.default, 'string'); + assert.strictEqual(typeof classes.checked, 'string'); + assert.strictEqual(typeof classes.disabled, 'string'); + }); }); - describe('prop: disabled', () => { - it('should disable the component', () => { - const wrapper = shallow(); - assert.strictEqual(wrapper.props().disabled, true, 'should pass the property down the tree'); + describe('default Radio export', () => { + it('should be a SwitchBase with the displayName set for debugging', () => { + assert.strictEqual(Radio.name, 'SwitchBase'); + assert.strictEqual(Radio.displayName, 'Radio'); }); }); - describe('prop: disabledClassName', () => { - it('should provide the class', () => { - const className = 'foo'; - const wrapper = shallow(); - assert.strictEqual( - wrapper.find('SwitchBase').props().disabledClassName.indexOf(className) !== -1, - true, - 'should have the custom disabled class', - ); + describe('named LabelRadio export', () => { + it('should be Radio wrapped with SwitchLabel', () => { + assert.strictEqual(LabelRadio.name, 'SwitchLabel'); + assert.strictEqual(LabelRadio.displayName, 'withSwitchLabel(Radio)'); }); }); }); diff --git a/src/Radio/RadioGroup.js b/src/Radio/RadioGroup.js index 903e30225ecaec..866f1f01466e92 100644 --- a/src/Radio/RadioGroup.js +++ b/src/Radio/RadioGroup.js @@ -2,33 +2,9 @@ import React, { Component, Children, cloneElement, PropTypes } from 'react'; import { createStyleSheet } from 'jss-theme-reactor'; -import { findDOMNode } from 'react-dom'; import classNames from 'classnames'; -import keycode from 'keycode'; -import querySelectorAll from 'dom-helpers/query/querySelectorAll'; -import contains from 'dom-helpers/query/contains'; -import activeElement from 'dom-helpers/activeElement'; -import ownerDocument from 'dom-helpers/ownerDocument'; - -function changeFocus(currentFocusIndex = 0, event, radios) { - const key = keycode(event); - - if (key === 'down' || key === 'right') { - event.preventDefault(); - if (currentFocusIndex < radios.length - 1) { - radios[currentFocusIndex + 1].focus(); - } else { - radios[0].focus(); - } - } else if (key === 'up' || key === 'left') { - event.preventDefault(); - if (currentFocusIndex > 0) { - radios[currentFocusIndex - 1].focus(); - } else { - radios[radios.length - 1].focus(); - } - } -} +import FormGroup from '../Form/FormGroup'; +import { find } from '../utils/helpers'; export const styleSheet = createStyleSheet('RadioGroup', () => { return { @@ -40,19 +16,13 @@ export const styleSheet = createStyleSheet('RadioGroup', () => { }; }); -export default class RadioGroup extends Component { +class RadioGroup extends Component { static propTypes = { children: PropTypes.node, /** * The CSS class name of the root element. */ className: PropTypes.string, - component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * @ignore - * For uncontrolled support - */ - defaultValue: PropTypes.string, name: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, @@ -60,181 +30,73 @@ export default class RadioGroup extends Component { selectedValue: PropTypes.string, }; - static defaultProps = { - component: 'div', - }; - static contextTypes = { styleManager: PropTypes.object.isRequired, }; - static childContextTypes = { - radioGroup: PropTypes.object, - }; - - state = { - currentTabIndex: undefined, - focused: false, // used for optional label required/error - selectedValue: undefined, // used for uncontrolled support - }; - - componentWillMount() { - this.isControlled = this.props.selectedValue !== undefined; + radios = undefined; - // not controlled, use internal state - if (!this.isControlled && this.props.defaultValue) { - this.setState({ selectedValue: this.props.defaultValue }); + focus = () => { + if (!this.radios || !this.radios.length) { + return; } - } - - componentDidMount() { - this.resetTabIndex(); - } - componentWillUnmount() { - clearTimeout(this.blurTimer); - } + const focusRadios = this.radios.filter((n) => !n.props.disabled); - group = undefined; - blurTimer = undefined; - isControlled = undefined; - - handleBlur = (event) => { - this.blurTimer = setTimeout(() => { - if (this.group) { - const group = findDOMNode(this.group); - const currentFocus = activeElement(ownerDocument(group)); - if (!contains(group, currentFocus)) { - this.resetTabIndex(); - this.setState({ focused: false }); - } - } - }, 50); - - if (this.props.onBlur) { - this.props.onBlur(event); + if (!focusRadios.length) { + return; } - }; - - handleKeyDown = (event) => { - const group = findDOMNode(this.group); - - if (group) { - const currentFocus = activeElement(ownerDocument(group)); - const radios = querySelectorAll(group, '[role="radio"]'); - - let currentFocusIndex = -1; - for (let i = 0; i < radios.length; i += 1) { - if (radios[i] === currentFocus || contains(radios[i], currentFocus)) { - currentFocusIndex = i; - break; - } - } + const selectedRadio = find(focusRadios, (n) => n.props.checked); - changeFocus(currentFocusIndex, event, radios); + if (selectedRadio) { + selectedRadio.focus(); + return; } - if (this.props.onKeyDown) { - this.props.onKeyDown(event); - } + focusRadios[0].focus(); }; handleRadioChange = (event, checked) => { - if (checked) { - if (!this.isControlled) { - this.setState({ selectedValue: event.target.value }); - } - if (this.props.onChange) { - this.props.onChange(event, event.target.value); - } - } - }; - - handleRadioFocus = (event) => { - const group = findDOMNode(this.group); - if (group) { - this.setState({ focused: true }); - const radios = querySelectorAll(group, '[role="radio"]'); - for (let i = 0; i < radios.length; i += 1) { - if (radios[i] === event.currentTarget || contains(radios[i], event.currentTarget)) { - this.setTabIndex(i); - break; - } - } + if (checked && this.props.onChange) { + this.props.onChange(event, event.target.value); } }; - focus() { - const { currentTabIndex } = this.state; - if (currentTabIndex && currentTabIndex >= 0) { - const group = findDOMNode(this.group); - const radios = querySelectorAll(group, '[role="radio"]'); - if (radios && radios[currentTabIndex]) { - radios[currentTabIndex].focus(); - } - } - } - - resetTabIndex() { - const group = findDOMNode(this.group); - const currentFocus = activeElement(ownerDocument(group)); - const radios = querySelectorAll(group, '[role="radio"]'); - const currentFocusIndex = radios.indexOf(currentFocus); - - if (currentFocusIndex !== -1) { - return this.setTabIndex(currentFocusIndex); - } - - const selectedRadio = group.querySelector('[aria-checked="true"]'); - - if (selectedRadio) { - return this.setTabIndex(radios.indexOf(selectedRadio)); - } - - return this.setTabIndex(0); - } - - setTabIndex(n) { - this.setState({ currentTabIndex: n }); - } - render() { const { children, className: classNameProp, - component: ComponentProp, name, - selectedValue: selectedValueProp, + selectedValue, onChange, // eslint-disable-line no-unused-vars ...other } = this.props; - const selectedValue = this.isControlled ? selectedValueProp : this.state.selectedValue; const classes = this.context.styleManager.render(styleSheet); + this.radios = []; + return ( - { this.group = c; }} role="radiogroup" {...other} - onKeyDown={this.handleKeyDown} - onBlur={this.handleBlur} > {Children.map(children, (child, index) => { + const selected = selectedValue === child.props.value; return cloneElement(child, { key: index, name, - checked: selectedValue === child.props.value, - tabIndex: index === this.state.currentTabIndex ? '0' : '-1', + ref: (c) => { this.radios.push(c); }, + checked: selected, onChange: this.handleRadioChange, - onFocus: this.handleRadioFocus, }); })} - + ); } } +export default RadioGroup; diff --git a/src/Radio/RadioGroup.spec.js b/src/Radio/RadioGroup.spec.js index ea695a678bbfb1..47833a12324545 100644 --- a/src/Radio/RadioGroup.spec.js +++ b/src/Radio/RadioGroup.spec.js @@ -14,14 +14,14 @@ describe('', () => { shallow = createShallowWithContext(); }); - it('should render a radiogroup div', () => { + it('should render a FormGroup with the radiogroup role', () => { const wrapper = shallow( , ); assert.strictEqual( - wrapper.is('div[role="radiogroup"]'), + wrapper.is('FormGroup[role="radiogroup"]'), true, - 'should be a div with the correct role', + 'should be a FormGroup with the correct role', ); }); @@ -46,4 +46,72 @@ describe('', () => { assert.strictEqual(handleKeyDown.callCount, 1); assert.strictEqual(handleKeyDown.args[0][0], event); }); + + describe('imperative focus()', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('should focus the first non-disabled radio', () => { + const radios = [ + { props: { disabled: true }, focus: spy() }, + { props: { disabled: false }, focus: spy() }, + { props: { disabled: false }, focus: spy() }, + ]; + wrapper.instance().radios = radios; + wrapper.instance().focus(); + + assert.strictEqual(radios[1].focus.callCount, 1); + }); + + it('should not focus any radios if all are disabled', () => { + const radios = [ + { props: { disabled: true }, focus: spy() }, + { props: { disabled: true }, focus: spy() }, + { props: { disabled: true }, focus: spy() }, + ]; + wrapper.instance().radios = radios; + wrapper.instance().focus(); + + assert.strictEqual(radios[0].focus.callCount, 0); + assert.strictEqual(radios[1].focus.callCount, 0); + assert.strictEqual(radios[2].focus.callCount, 0); + }); + + it('should focus the selected radio', () => { + const radios = [ + { props: { disabled: true }, focus: spy() }, + { props: { disabled: false }, focus: spy() }, + { props: { disabled: false, checked: true }, focus: spy() }, + { props: { disabled: false }, focus: spy() }, + ]; + wrapper.instance().radios = radios; + wrapper.instance().focus(); + + assert.strictEqual(radios[0].focus.callCount, 0); + assert.strictEqual(radios[1].focus.callCount, 0); + assert.strictEqual(radios[2].focus.callCount, 1); + assert.strictEqual(radios[3].focus.callCount, 0); + }); + + it('should focus the non-disabled radio rather than the disabled selected radio', () => { + const radios = [ + { props: { disabled: true }, focus: spy() }, + { props: { disabled: true }, focus: spy() }, + { props: { disabled: true, checked: true }, focus: spy() }, + { props: { disabled: false }, focus: spy() }, + ]; + wrapper.instance().radios = radios; + wrapper.instance().focus(); + + assert.strictEqual(radios[0].focus.callCount, 0); + assert.strictEqual(radios[1].focus.callCount, 0); + assert.strictEqual(radios[2].focus.callCount, 0); + assert.strictEqual(radios[3].focus.callCount, 1); + }); + }); }); diff --git a/src/Radio/index.js b/src/Radio/index.js index 4e66bd389cefaf..ece22d8b8205bc 100644 --- a/src/Radio/index.js +++ b/src/Radio/index.js @@ -1,5 +1,5 @@ /* eslint-disable flowtype/require-valid-file-annotation */ export default from './Radio'; -export Radio from './Radio'; +export Radio, { LabelRadio } from './Radio'; export RadioGroup from './RadioGroup'; diff --git a/src/Switch/Switch.js b/src/Switch/Switch.js index 96731f3f0a6310..6ced943db12293 100644 --- a/src/Switch/Switch.js +++ b/src/Switch/Switch.js @@ -3,8 +3,8 @@ import React, { PropTypes } from 'react'; import { createStyleSheet } from 'jss-theme-reactor'; import classNames from 'classnames'; -import SwitchBase from '../internal/SwitchBase'; -import SelectionLabel from '../internal/SelectionLabel'; +import { createSwitch } from '../internal/SwitchBase'; +import withSwitchLabel from '../internal/withSwitchLabel'; export const styleSheet = createStyleSheet('Switch', (theme) => { const { palette } = theme; @@ -57,91 +57,36 @@ export const styleSheet = createStyleSheet('Switch', (theme) => { }; }); -export default function Switch(props, context) { +const SwitchBase = createSwitch({ styleSheet }); + +function Switch(props, context) { const { className, - checkedClassName, - disabled, - disabledClassName, - label, - labelClassName, - labelReverse, ...other } = props; - const classes = context.styleManager.render(styleSheet); + const classes = context.styleManager.render(styleSheet); const icon =
; - const switchProps = { - className: classes.default, - checkedClassName: classNames(classes.checked, checkedClassName), - disabledClassName: classNames(classes.disabled, disabledClassName), - icon, - checkedIcon: icon, - type: 'checkbox', - disabled, - ...other, - }; - - if (label) { - return ( - -
- -
-
- - ); - } return (
- +
); } Switch.propTypes = { - /** - * The CSS class name of the switch element when checked. - */ - checkedClassName: PropTypes.string, - /** - * The CSS class name of the root element. - */ className: PropTypes.string, - /** - * If `true`, the control will be disabled. - */ - disabled: PropTypes.bool, - /** - * The CSS class name of the switch element when disabled. - */ - disabledClassName: PropTypes.string, - /** - * The text to be used in an enclosing label element. - */ - label: PropTypes.node, - /** - * The className to be used in an enclosing label element. - */ - labelClassName: PropTypes.string, - /** - * Will reverse the order of the element and the label. - */ - labelReverse: PropTypes.bool, -}; - -Switch.defaultProps = { - disabled: false, - labelReverse: false, }; Switch.contextTypes = { styleManager: PropTypes.object.isRequired, }; + + +export default Switch; + +const LabelSwitch = withSwitchLabel(Switch); + +export { LabelSwitch }; diff --git a/src/Switch/Switch.spec.js b/src/Switch/Switch.spec.js index a45268adea4750..51d5fb1a9bbadd 100644 --- a/src/Switch/Switch.spec.js +++ b/src/Switch/Switch.spec.js @@ -4,7 +4,7 @@ import React from 'react'; import { assert } from 'chai'; import { createShallowWithContext } from 'test/utils'; -import Switch, { styleSheet } from './Switch'; +import Switch, { LabelSwitch, styleSheet } from './Switch'; describe('', () => { let shallow; @@ -15,60 +15,47 @@ describe('', () => { classes = shallow.context.styleManager.render(styleSheet); }); - it('should render a SwitchBase inside a div when label not present', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('div'), true, 'should be a div'); - assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the root class'); - assert.strictEqual(wrapper.childAt(0).is('SwitchBase'), true, 'should be a SwitchBase'); + describe('styleSheet', () => { + it('should have the classes required for SwitchBase', () => { + assert.strictEqual(typeof classes.default, 'string'); + assert.strictEqual(typeof classes.checked, 'string'); + assert.strictEqual(typeof classes.disabled, 'string'); + }); }); - it('should render a label', () => { - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.is('SelectionLabel'), true, 'should be a SelectionLabel'); - }); + describe('default Switch export', () => { + let wrapper; - it('should render with the default and checked classes', () => { - const wrapper = shallow(); - assert.strictEqual(wrapper.hasClass('woof'), true, 'should have the "woof" class'); - assert.strictEqual(wrapper.childAt(0).hasClass(classes.default), true, 'should have the default class'); - assert.strictEqual( - wrapper.childAt(0).prop('checkedClassName').indexOf('meow') !== -1, - true, - 'should have the "meow" class', - ); - assert.strictEqual( - wrapper.childAt(0).prop('checkedClassName').indexOf(classes.checked) !== -1, - true, - 'should have the checked class', - ); - }); + beforeEach(() => { + wrapper = shallow(); + }); - it('should spread custom props on the SwitchBase node', () => { - const wrapper = shallow(); - const switchBase = wrapper.find('SwitchBase'); - assert.strictEqual(switchBase.prop('data-my-prop'), 'woof', 'custom prop should be woof'); - }); + it('should render a div with the root and user classes', () => { + assert.strictEqual(wrapper.is('div'), true); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual(wrapper.hasClass('foo'), true); + }); + + it('should render SwitchBase with a custom div icon with the icon class', () => { + const switchBase = wrapper.childAt(0); + + assert.strictEqual(switchBase.is('SwitchBase'), true); + assert.strictEqual(switchBase.prop('icon').type, 'div'); + assert.strictEqual(switchBase.prop('icon').props.className, classes.icon); + }); + + it('should render the bar as the 2nd child', () => { + const bar = wrapper.childAt(1); - describe('prop: disabled', () => { - it('should disable the component', () => { - const wrapper = shallow(); - assert.strictEqual(wrapper.find('SwitchBase').props().disabled, true, 'should disable the switch node'); + assert.strictEqual(bar.is('div'), true); + assert.strictEqual(bar.hasClass(classes.bar), true); }); }); - describe('prop: disabledClassName', () => { - it('should provide the class', () => { - const className = 'foo'; - const wrapper = shallow(); - assert.strictEqual( - wrapper.find('SwitchBase').props().disabledClassName.indexOf(className) !== -1, - true, - 'should have the custom disabled class', - ); + describe('named LabelSwitch export', () => { + it('should be Switch wrapped with SwitchLabel', () => { + assert.strictEqual(LabelSwitch.name, 'SwitchLabel'); + assert.strictEqual(LabelSwitch.displayName, 'withSwitchLabel(Switch)'); }); }); }); diff --git a/src/Switch/index.js b/src/Switch/index.js index 25b128547f8937..d2a68d98ac9d39 100644 --- a/src/Switch/index.js +++ b/src/Switch/index.js @@ -1,4 +1,4 @@ /* eslint-disable flowtype/require-valid-file-annotation */ export default from './Switch'; -export Switch from './Switch'; +export Switch, { LabelSwitch } from './Switch'; diff --git a/src/internal/ButtonBase.js b/src/internal/ButtonBase.js index cd2e7bacecead5..2c8c176036fdbd 100644 --- a/src/internal/ButtonBase.js +++ b/src/internal/ButtonBase.js @@ -1,31 +1,13 @@ /* eslint-disable flowtype/require-valid-file-annotation */ import React, { Component, PropTypes } from 'react'; -import ReactDOM from 'react-dom'; +import { findDOMNode } from 'react-dom'; import { createStyleSheet } from 'jss-theme-reactor'; import classNames from 'classnames'; import keycode from 'keycode'; -import addEventListener from '../utils/addEventListener'; +import { listenForFocusKeys, detectKeyboardFocus, focusKeyPressed } from '../utils/keyboardFocus'; import { TouchRipple, createRippleHandler } from '../Ripple'; -let listening = false; -let focusKeyPressed = false; - -function isFocusKey(event) { - return ['tab', 'enter', 'space', 'esc', 'up', 'down', 'left', 'right'].indexOf(keycode(event)) !== -1; -} - -function listenForFocusKeys() { - if (!listening) { - addEventListener(window, 'keyup', (event) => { - if (isFocusKey(event)) { - focusKeyPressed = true; - } - }); - listening = true; - } -} - export const styleSheet = createStyleSheet('ButtonBase', () => { return { buttonBase: { @@ -121,6 +103,8 @@ export default class ButtonBase extends Component { button = null; keyboardFocusTimeout = undefined; + focus = () => this.button.focus(); + handleKeyDown = (event) => { const { component, focusRipple, onKeyDown, onClick } = this.props; const key = keycode(event); @@ -140,6 +124,7 @@ export default class ButtonBase extends Component { // Keyboard accessibility for non interactive elements if ( + event.target === this.button && onClick && component !== 'a' && component !== 'button' && @@ -163,7 +148,7 @@ export default class ButtonBase extends Component { handleMouseDown = createRippleHandler(this, 'MouseDown', 'start', () => { clearTimeout(this.keyboardFocusTimeout); - focusKeyPressed = false; + focusKeyPressed(false); if (this.state.keyboardFocused) { this.setState({ keyboardFocused: false }); } @@ -185,24 +170,23 @@ export default class ButtonBase extends Component { }); handleFocus = (event) => { - if (!this.props.disabled) { - // setTimeout is needed because the focus event fires - // first if focus was called programatically inside a keydown handler - event.persist(); - setTimeout(() => { - if (focusKeyPressed && document.activeElement === ReactDOM.findDOMNode(this.button)) { - this.keyDown = false; - focusKeyPressed = false; - this.setState({ keyboardFocused: true }); - if (this.props.onKeyboardFocus) { - this.props.onKeyboardFocus(event); - } - } - }, 150); - - if (this.props.onFocus) { - this.props.onFocus(event); + if (this.props.disabled) { + return; + } + + event.persist(); + + detectKeyboardFocus(this, findDOMNode(this.button), () => { + this.keyDown = false; + this.setState({ keyboardFocused: true }); + + if (this.props.onKeyboardFocus) { + this.props.onKeyboardFocus(event); } + }); + + if (this.props.onFocus) { + this.props.onFocus(event); } }; diff --git a/src/internal/SelectionLabel.js b/src/internal/SelectionLabel.js deleted file mode 100644 index 477b8c8c9e87e9..00000000000000 --- a/src/internal/SelectionLabel.js +++ /dev/null @@ -1,68 +0,0 @@ -// @flow weak - -import React, { PropTypes } from 'react'; -import { createStyleSheet } from 'jss-theme-reactor'; -import classNames from 'classnames'; - -export const styleSheet = createStyleSheet('SelectionLabel', (theme) => { - return { - root: { - marginLeft: -12, - marginRight: 16, // used for row presentation of radio/checkbox - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - }, - reverse: { - flexDirection: 'row-reverse', - }, - disabled: { - color: theme.palette.text.disabled, - cursor: 'not-allowed', - }, - }; -}); - -export default function SelectionLabel(props, context) { - const { - disabled, - label, - labelClassName: labelClassNameProp, - labelReverse, - children, - } = props; - const classes = context.styleManager.render(styleSheet); - const labelClassName = classNames(classes.root, { - [classes.reverse]: labelReverse, - }, labelClassNameProp); - - return ( - - ); -} - -SelectionLabel.propTypes = { - children: PropTypes.node, - /** - * If `true`, the control will be disabled. - */ - disabled: PropTypes.bool.isRequired, - label: PropTypes.node, - labelClassName: PropTypes.string, - labelReverse: PropTypes.bool.isRequired, -}; - -SelectionLabel.contextTypes = { - styleManager: PropTypes.object.isRequired, -}; diff --git a/src/internal/SelectionLabel.spec.js b/src/internal/SelectionLabel.spec.js deleted file mode 100644 index 04a0d884bac0f1..00000000000000 --- a/src/internal/SelectionLabel.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow weak -/* eslint-env mocha */ - -import React from 'react'; -import { assert } from 'chai'; -import { createShallowWithContext } from 'test/utils'; -import SelectionLabel, { styleSheet } from './SelectionLabel'; - -describe('', () => { - let shallow; - let classes; - - before(() => { - shallow = createShallowWithContext(); - classes = shallow.context.styleManager.render(styleSheet); - }); - - let wrapper; - - beforeEach(() => { - wrapper = shallow( - , - ); - }); - - it('should render a label', () => { - assert.strictEqual(wrapper.is('label'), true, 'should be a label'); - }); - - it('should render the label text inside an additional span', () => { - const span = wrapper.childAt(0); - assert.strictEqual(span.is('span'), true, 'should render a span'); - assert.strictEqual(span.childAt(0).node, 'Pizza', 'should be the label text'); - }); - - it('should render with accessibility attributes', () => { - assert.strictEqual( - wrapper.prop('role'), - 'presentation', - 'should set the role to presentation for screen readers', - ); - const span = wrapper.childAt(0); - assert.strictEqual( - span.prop('role'), - 'presentation', - 'should set the span role to presentation for screen readers', - ); - assert.strictEqual( - span.prop('aria-hidden'), - 'true', - 'should set to aria hidden for screen readers', - ); - }); - - it('should render with the default and custom classes', () => { - assert.strictEqual(wrapper.hasClass('foo'), true, 'should have the "foo" class'); - assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the "root" class'); - }); - - describe('prop: disabled', () => { - it('should disable the component', () => { - wrapper.setProps({ - disabled: true, - }); - - assert.strictEqual( - wrapper.childAt(0).hasClass(classes.disabled), - true, - 'should have the disabled class', - ); - }); - }); -}); diff --git a/src/internal/SwitchBase.js b/src/internal/SwitchBase.js index 8defd655fa54b3..4f8b90c7b83cad 100644 --- a/src/internal/SwitchBase.js +++ b/src/internal/SwitchBase.js @@ -10,8 +10,9 @@ export const styleSheet = createStyleSheet('SwitchBase', () => { root: { display: 'inline-flex', alignItems: 'center', + transition: 'none', }, - switch: { + input: { cursor: 'inherit', position: 'absolute', opacity: 0, @@ -25,155 +26,166 @@ export const styleSheet = createStyleSheet('SwitchBase', () => { }; }); -export default class SwitchBase extends Component { - static propTypes = { - /** - * SwitchBase is checked if true. - */ - checked: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - /** - * The CSS class name of the root element when checked. - */ - checkedClassName: PropTypes.string, - checkedIcon: PropTypes.node, - /** - * The CSS class name of the root element. - */ - className: PropTypes.string, - /** - * @ignore - */ - defaultChecked: PropTypes.bool, - /** - * If `true`, the switch will be disabled. - */ - disabled: PropTypes.bool, - /** - * The CSS class name of the root element when disabled. - */ - disabledClassName: PropTypes.string, - icon: PropTypes.node, - /** - * Callback function that is fired when the switch is changed. - * - * @param {object} event `change` event - * @param {boolean} checked The `checked` value of the switch - */ - onChange: PropTypes.func, - /** - * If false, the ripple effect will be disabled. - */ - ripple: PropTypes.bool, - type: PropTypes.oneOf(['checkbox', 'radio']), - value: PropTypes.string, - }; - - static defaultProps = { - icon: 'check_box_outline_blank', - checkedIcon: 'check_box', - type: 'checkbox', - }; - - static contextTypes = { - styleManager: PropTypes.object.isRequired, - }; - - state = {}; - - componentWillMount() { - const { props } = this; +export function createSwitch({ + defaultIcon = 'check_box_outline_blank', + defaultCheckedIcon = 'check_box', + inputType = 'checkbox', + styleSheet: switchStyleSheet, +} = {}) { + return class SwitchBase extends Component { + static propTypes = { + /** + * SwitchBase is checked if true. + */ + checked: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + /** + * The CSS class name of the root element when checked. + */ + checkedClassName: PropTypes.string, + checkedIcon: PropTypes.node, + /** + * The CSS class name of the root element. + */ + className: PropTypes.string, + /** + * @ignore + */ + defaultChecked: PropTypes.bool, + /** + * If `true`, the switch will be disabled. + */ + disabled: PropTypes.bool, + /** + * The CSS class name of the root element when disabled. + */ + disabledClassName: PropTypes.string, + icon: PropTypes.node, + /* + * @ignore + */ + name: PropTypes.string, + /** + * Callback function that is fired when the switch is changed. + * + * @param {object} event `change` event + * @param {boolean} checked The `checked` value of the switch + */ + onChange: PropTypes.func, + /** + * If false, the ripple effect will be disabled. + */ + ripple: PropTypes.bool, + /** + * @ignore + */ + tabIndex: PropTypes.string, + value: PropTypes.string, + }; + + static defaultProps = { + icon: defaultIcon, + checkedIcon: defaultCheckedIcon, + }; + + static contextTypes = { + styleManager: PropTypes.object.isRequired, + }; + + state = {}; + + componentWillMount() { + const { props } = this; + + this.isControlled = props.checked !== undefined; + + if (!this.isControlled) { // not controlled, use internal state + this.setState({ checked: props.defaultChecked !== undefined ? props.defaultChecked : false }); + } + } - this.isControlled = props.checked !== undefined; + input = undefined; + button = undefined; + isControlled = undefined; - if (!this.isControlled) { // not controlled, use internal state - this.setState({ checked: props.defaultChecked !== undefined ? props.defaultChecked : false }); - } - } + focus = () => this.input.focus(); - input = undefined; - isControlled = undefined; + handleInputChange = (event) => { + let newChecked; - handleInputChange = (event) => { - let newChecked; + if (this.isControlled) { + newChecked = !this.props.checked; + } else { + newChecked = !this.state.checked; + if (this.input && this.input.checked !== newChecked) { + this.input.checked = newChecked; + } + this.setState({ checked: !this.state.checked }); + } - if (this.isControlled) { - newChecked = !this.props.checked; - } else { - newChecked = !this.state.checked; - if (this.input && this.input.checked !== newChecked) { - this.input.checked = newChecked; + if (this.props.onChange) { + this.props.onChange(event, newChecked); + } + }; + + render() { + const { + checked: checkedProp, + className: classNameProp, + checkedClassName, + checkedIcon, + disabled, + disabledClassName, + icon: iconProp, + name, + onChange, // eslint-disable-line no-unused-vars + ripple, + tabIndex, + value, + ...other + } = this.props; + + + const checked = this.isControlled ? checkedProp : this.state.checked; + const classes = this.context.styleManager.render(styleSheet); + const switchClasses = switchStyleSheet ? this.context.styleManager.render(switchStyleSheet) : {}; + + const className = classNames(classes.root, switchClasses.default, classNameProp, { + [classNames(switchClasses.checked, checkedClassName)]: checked, + [classNames(switchClasses.disabled, disabledClassName)]: disabled, + }); + + let icon = checked ? checkedIcon : iconProp; + + if (typeof icon === 'string') { + icon = ; } - this.setState({ checked: !this.state.checked }); - } - if (this.props.onChange) { - this.props.onChange(event, newChecked); + return ( + { this.button = c; }} + className={className} + disabled={disabled} + ripple={ripple} + tabIndex={null} + role={undefined} + {...other} + > + {icon} + { this.input = c; }} + type={inputType} + name={name} + checked={this.isControlled ? checkedProp : undefined} + onChange={this.handleInputChange} + className={classes.input} + disabled={disabled} + tabIndex={tabIndex} + value={value} + /> + + ); } }; - - // Handle button interactions when - // IconButton is interacted with using space/enter - handleClick = (event) => { - if (event.target !== this.input) { - this.input.click(); - } - } - - render() { - const { - checked: checkedProp, - className: classNameProp, - checkedClassName, - checkedIcon, - icon: iconProp, - disabled, - disabledClassName, - onChange, // eslint-disable-line no-unused-vars - ripple, - type, - value, - ...other - } = this.props; - - const classes = this.context.styleManager.render(styleSheet); - const checked = this.isControlled ? checkedProp : this.state.checked; - - const className = classNames(classes.root, classNameProp, { - [checkedClassName]: checkedClassName && checked, - [disabledClassName]: disabledClassName && disabled, - }); - - let icon = checked ? checkedIcon : iconProp; - - if (typeof icon === 'string') { - icon = ; - } - - return ( - - {icon} - { this.input = c; }} - type={type} - checked={this.isControlled ? checkedProp : undefined} - onChange={this.handleInputChange} - className={classes.switch} - disabled={disabled} - value={value} - /> - - ); - } } diff --git a/src/internal/SwitchBase.spec.js b/src/internal/SwitchBase.spec.js index 9aecd593ec41c7..72e04ee07515a9 100644 --- a/src/internal/SwitchBase.spec.js +++ b/src/internal/SwitchBase.spec.js @@ -4,56 +4,16 @@ import React from 'react'; import { assert } from 'chai'; import { createShallowWithContext, createMountWithContext } from 'test/utils'; -import SwitchBase, { styleSheet } from './SwitchBase'; - -function assertIsChecked(classes, wrapper) { - const iconButton = wrapper.find('span').at(0); - - assert.strictEqual( - iconButton.hasClass('test-class-checked'), - true, - 'should have the checked class on the root node', - ); - assert.strictEqual( - iconButton.prop('aria-checked'), - true, - 'should pass aria-checked=true to the root node', - ); - - const input = wrapper.find('input'); - assert.strictEqual(input.node.checked, true, 'the DOM node should be checked'); - - const icon = wrapper.find('span.material-icons'); - assert.strictEqual(icon.text(), 'check_box', 'should be the check_box icon'); -} - -function assertIsNotChecked(classes, wrapper) { - const iconButton = wrapper.find('span').at(0); - - assert.strictEqual( - iconButton.hasClass('test-class-checked'), - false, - 'should not have the checked class on the root node', - ); - assert.strictEqual( - iconButton.prop('aria-checked'), - false, - 'should pass aria-checked=false to the root node', - ); - - const input = wrapper.find('input'); - assert.strictEqual(input.node.checked, false, 'the DOM node should not be checked'); - - const icon = wrapper.find('span.material-icons'); - assert.strictEqual(icon.text(), 'check_box_outline_blank', 'should be the check_box_outline_blank icon'); -} +import { createSwitch, styleSheet } from './SwitchBase'; describe('', () => { let shallow; let classes; let mount; + let SwitchBase; before(() => { + SwitchBase = createSwitch(); shallow = createShallowWithContext(); mount = createMountWithContext(); classes = shallow.context.styleManager.render(styleSheet); @@ -85,14 +45,33 @@ describe('', () => { assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the root class'); }); - // SHOULD APPLY CLASSNAME BASED ON STATUS - it('should spread custom props on the root node', () => { const wrapper = shallow(); assert.strictEqual(wrapper.prop('data-my-prop'), 'woof', 'custom prop should be woof'); }); - it('should set the icon to aria-hidden="true" to avoid the ligature being read by screenreaders', () => { + it('should pass tabIndex to the input so it can be taken out of focus rotation', () => { + const wrapper = shallow(); + const input = wrapper.find('input'); + assert.strictEqual(input.prop('tabIndex'), '-1'); + }); + + it('should pass value, disabled, checked, and name to the input', () => { + const props = { + name: 'gender', + disabled: true, + value: 'male', + }; + + const wrapper = shallow(); + const input = wrapper.find('input'); + + Object.keys(props).forEach((n) => { + assert.strictEqual(input.prop(n), props[n]); + }); + }); + + it('should set the icon to aria-hidden="true" to avoid being read by screenreaders', () => { const wrapper = shallow(); assert.strictEqual(wrapper.childAt(0).prop('aria-hidden'), 'true'); }); @@ -103,25 +82,25 @@ describe('', () => { assert.strictEqual(wrapper.childAt(1).prop('disabled'), true, 'should disable the input node'); }); - describe('prop: disabledClassName', () => { - it('should apply the custom disabled className when needed', () => { - const disabledClassName = 'foo'; - const wrapperA = shallow(); - assert.strictEqual(wrapperA.hasClass(disabledClassName), true, 'should have the custom disabled class'); - - const wrapperB = shallow(); - assert.strictEqual( - wrapperB.hasClass(disabledClassName), - false, - 'should not have the custom disabled class', - ); - }); + it('should apply the custom disabled className when disabled', () => { + const disabledClassName = 'foo'; + const wrapperA = shallow(); + + assert.strictEqual(wrapperA.hasClass(disabledClassName), true, 'should have the custom disabled class'); + + wrapperA.setProps({ disabled: false }); + + assert.strictEqual( + wrapperA.hasClass(disabledClassName), + false, + 'should not have the custom disabled class', + ); }); describe('controlled', () => { let wrapper; - before(() => { + beforeEach(() => { wrapper = mount( ', () => { }); it('should uncheck the checkbox', () => { + wrapper.setProps({ checked: true }); wrapper.setProps({ checked: false }); assertIsNotChecked(classes, wrapper); }); @@ -153,7 +133,7 @@ describe('', () => { describe('uncontrolled', () => { let wrapper; - before(() => { + beforeEach(() => { wrapper = mount( ', () => { }); it('should check the checkbox', () => { - const input = wrapper.find('input'); - input.node.checked = true; - wrapper.instance().handleInputChange({}); + wrapper.find('input').node.click(); assertIsChecked(classes, wrapper); }); it('should uncheck the checkbox', () => { - const input = wrapper.find('input'); - input.node.checked = false; - wrapper.instance().handleInputChange({}); + wrapper.find('input').node.click(); + wrapper.find('input').node.click(); assertIsNotChecked(classes, wrapper); }); }); }); + +function assertIsChecked(classes, wrapper) { + const iconButton = wrapper.find('span').at(0); + + assert.strictEqual( + iconButton.hasClass('test-class-checked'), + true, + 'should have the checked class on the root node', + ); + + const input = wrapper.find('input'); + assert.strictEqual(input.node.checked, true, 'the DOM node should be checked'); + + const icon = wrapper.find('span.material-icons'); + assert.strictEqual(icon.text(), 'check_box', 'should be the check_box icon'); +} + +function assertIsNotChecked(classes, wrapper) { + const iconButton = wrapper.find('span').at(0); + + assert.strictEqual( + iconButton.hasClass('test-class-checked'), + false, + 'should not have the checked class on the root node', + ); + + const input = wrapper.find('input'); + assert.strictEqual(input.node.checked, false, 'the DOM node should not be checked'); + + const icon = wrapper.find('span.material-icons'); + assert.strictEqual(icon.text(), 'check_box_outline_blank', 'should be the check_box_outline_blank icon'); +} diff --git a/src/internal/withSwitchLabel.js b/src/internal/withSwitchLabel.js new file mode 100644 index 00000000000000..3a4d5b418c04f7 --- /dev/null +++ b/src/internal/withSwitchLabel.js @@ -0,0 +1,94 @@ +// @flow weak +/* eslint-disable jsx-a11y/label-has-for */ + +import React, { Component, PropTypes } from 'react'; +import { createStyleSheet } from 'jss-theme-reactor'; +import createHelper from 'recompose/createHelper'; +import classNames from 'classnames'; + +export const styleSheet = createStyleSheet('SwitchLabel', (theme) => { + return { + root: { + display: 'inline-flex', + alignItems: 'center', + cursor: 'pointer', + }, + hasLabel: { + marginLeft: -12, + marginRight: 16, // used for row presentation of radio/checkbox + }, + disabled: { + color: theme.palette.text.disabled, + cursor: 'not-allowed', + }, + }; +}); + +function withSwitchLabel(SwitchComponent) { + return class SwitchLabel extends Component { + static propTypes = { + /** + * If `true`, the control will be disabled. + */ + disabled: PropTypes.bool, + /** + * The text to be used in an enclosing label element. + */ + label: PropTypes.node, + /** + * The className to be used in an enclosing label element. + */ + labelClassName: PropTypes.string, + }; + + static contextTypes = { + styleManager: PropTypes.object.isRequired, + }; + + switch = undefined; + + focus() { + if (this.switch && this.switch.focus) { + this.switch.focus(); + } + } + + render() { + const { + disabled, + label, + labelClassName: labelClassNameProp, + ...other + } = this.props; + + const classes = this.context.styleManager.render(styleSheet); + + const labelClassName = classNames(classes.root, { + [classes.hasLabel]: label && label.length, + }, labelClassNameProp); + + const switchElement = ( + { this.switch = c; }} + disabled={disabled} + {...other} + /> + ); + + if (!label) { + return switchElement; + } + + return ( + + ); + } + }; +} + +export default createHelper(withSwitchLabel, 'withSwitchLabel', true, true); diff --git a/src/internal/withSwitchLabel.spec.js b/src/internal/withSwitchLabel.spec.js new file mode 100644 index 00000000000000..eb85b0a6217094 --- /dev/null +++ b/src/internal/withSwitchLabel.spec.js @@ -0,0 +1,94 @@ +// @flow weak +/* eslint-env mocha */ + +import React from 'react'; +import { assert } from 'chai'; +import { spy } from 'sinon'; +import { createShallowWithContext } from 'test/utils'; +import withSwitchLabel, { styleSheet } from './withSwitchLabel'; + +describe('', () => { + let shallow; + let classes; + + before(() => { + shallow = createShallowWithContext(); + classes = shallow.context.styleManager.render(styleSheet); + }); + + describe('exports.withSwitchLabel', () => { + it('should be a function', () => { + assert.strictEqual(typeof withSwitchLabel, 'function'); + }); + + it('should return a SwitchLabel component with a wrapped displayName', () => { + const SwitchLabel = withSwitchLabel({ displayName: 'Foo' }); + assert.strictEqual(SwitchLabel.displayName, 'withSwitchLabel(Foo)'); + + const SwitchLabelFn = withSwitchLabel(function Foo() {}); // eslint-disable-line prefer-arrow-callback + assert.strictEqual(SwitchLabelFn.displayName, 'withSwitchLabel(Foo)'); + }); + }); + + describe('SwitchLabelFoo', () => { + let SwitchLabelFoo; + let wrapper; + + beforeEach(() => { + class Foo {} + SwitchLabelFoo = withSwitchLabel(Foo); + wrapper = shallow( + , + ); + }); + + it('should have the correct displayName', () => { + assert.strictEqual(SwitchLabelFoo.displayName, 'withSwitchLabel(Foo)'); + }); + + it('should render a label', () => { + assert.strictEqual(wrapper.is('label'), true, 'should be a label'); + }); + + it('should render the label text inside an additional span', () => { + const span = wrapper.childAt(1); + assert.strictEqual(span.is('span'), true, 'should render a span'); + assert.strictEqual(span.childAt(0).node, 'Pizza', 'should be the label text'); + }); + + it('should render with the default and custom classes', () => { + assert.strictEqual(wrapper.hasClass('foo'), true, 'should have the "foo" class'); + assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the "root" class'); + }); + + it('should render the switch element and no label if no label is provided', () => { + wrapper.setProps({ label: null }); + assert.strictEqual(wrapper.is('Foo'), true); + }); + + describe('imperative methods', () => { + it('should forward the focus method to the base component stored on switch', () => { + const focusSpy = spy(); + wrapper.instance().switch = { + focus: focusSpy, + }; + wrapper.instance().focus(); + assert.strictEqual(focusSpy.callCount, 1); + }); + }); + + describe('prop: disabled', () => { + it('should disable the component', () => { + wrapper.setProps({ + disabled: true, + }); + + assert.strictEqual( + wrapper.childAt(1).hasClass(classes.disabled), + true, + 'should have the disabled class', + ); + }); + }); + }); +}); diff --git a/src/styles/MuiThemeProvider.js b/src/styles/MuiThemeProvider.js index 536c99caa99c18..72751ac015d5cd 100644 --- a/src/styles/MuiThemeProvider.js +++ b/src/styles/MuiThemeProvider.js @@ -17,7 +17,6 @@ export const MUI_SHEET_ORDER = [ 'TouchRipple', 'ButtonBase', - 'SwitchBase', 'FormLabel', 'FormGroup', @@ -33,17 +32,19 @@ export const MUI_SHEET_ORDER = [ 'SvgIcon', + 'SwitchBase', + 'Switch', + 'Checkbox', + 'Radio', + 'RadioGroup', + 'SwitchLabel', + 'Dialog', 'DialogActions', 'DialogContent', 'DialogContentText', 'DialogTitle', - 'Switch', - 'Checkbox', - 'Radio', - 'RadioGroup', - 'TabIndicator', 'Tab', 'Tabs', diff --git a/src/utils/keyboardFocus.js b/src/utils/keyboardFocus.js new file mode 100644 index 00000000000000..d9b065685e5dc0 --- /dev/null +++ b/src/utils/keyboardFocus.js @@ -0,0 +1,48 @@ +// @flow weak + +import keycode from 'keycode'; +import contains from 'dom-helpers/query/contains'; +import addEventListener from '../utils/addEventListener'; + +const FOCUS_KEYS = ['tab', 'enter', 'space', 'esc', 'up', 'down', 'left', 'right']; + +const internal = { + listening: false, + focusKeyPressed: false, +}; + +function isFocusKey(event) { + return FOCUS_KEYS.indexOf(keycode(event)) !== -1; +} + +export function detectKeyboardFocus(instance, element, cb, attempt = 1) { + instance.keyboardFocusTimeout = setTimeout(() => { + if ( + focusKeyPressed() && + (document.activeElement === element || contains(element, document.activeElement)) + ) { + cb(); + } else if (attempt < 5) { + detectKeyboardFocus(instance, element, cb, attempt + 1); + } + }, 40); +} + +export function listenForFocusKeys() { + if (!internal.listening) { + addEventListener(window, 'keyup', (event) => { + if (isFocusKey(event)) { + internal.focusKeyPressed = true; + } + }); + internal.listening = true; + } +} + +export function focusKeyPressed(pressed) { + if (typeof pressed !== 'undefined') { + internal.focusKeyPressed = Boolean(pressed); + } + + return internal.focusKeyPressed; +} diff --git a/test/integration/RadioGroup.test.js b/test/integration/RadioGroup.test.js deleted file mode 100644 index 41768b47fa461f..00000000000000 --- a/test/integration/RadioGroup.test.js +++ /dev/null @@ -1,276 +0,0 @@ -// @flow weak -/* eslint-env mocha */ - -import React from 'react'; -import keycode from 'keycode'; -import { assert } from 'chai'; -import { spy } from 'sinon'; -import RadioGroup from 'src/Radio/RadioGroup'; -import Radio from 'src/Radio'; -import { createMountWithContext } from 'test/utils'; - -function assertRadioSelected(wrapper, selectedIndex) { - const radios = wrapper.find('Radio'); - - radios.forEach((radio, index) => { - if (index === selectedIndex) { - assert.strictEqual(radio.prop('checked'), true, 'selected radio should be checked'); - assert.strictEqual(radio.find('[role="radio"]').prop('aria-checked'), true, - 'radio should be aria-checked'); - assert.strictEqual(radio.find('input').prop('checked'), true, 'selected radio should be checked'); - } else { - assert.strictEqual(radio.prop('checked'), false, 'radio should not be checked'); - assert.strictEqual(radio.find('[role="radio"]').prop('aria-checked'), false, - 'radio should not be aria-checked'); - assert.strictEqual(radio.find('input').prop('checked'), false, 'radio should not be checked'); - } - }); -} - -function assertRadioTabIndexed(wrapper, tabIndexed) { - const radios = wrapper.find('Radio'); - - radios.forEach((radio, index) => { - if (index === tabIndexed) { - assert.strictEqual(radio.prop('tabIndex'), '0', 'selected radio should have the tab index'); - } else { - assert.strictEqual(radio.prop('tabIndex'), '-1', `radio at index ${index} should not be tab focusable`); - } - }); -} - -function assertRadioFocused(wrapper, tabIndexed) { - const radios = wrapper.find('Radio'); - - radios.forEach((radio, index) => { - if (index === tabIndexed) { - assert.strictEqual(radio.find('[role="radio"]').get(0), document.activeElement, 'should be focused'); - } - }); -} - -describe(' integration', () => { - let mount; - - before(() => { - mount = createMountWithContext(); - }); - after(() => { - mount.cleanUp(); - }); - - describe('controlled radio interaction', () => { - let handleChange; - let wrapper; - - before(() => { - handleChange = spy(); - wrapper = mount( - - - - - - , - ); - }); - - it('should detect controlled use', () => { - assert.strictEqual(wrapper.instance().isControlled, true); - }); - - it('should mark up the root node with role and our supplied aria-label', () => { - const rootDiv = wrapper.find('[data-mui-test="RadioGroup"]'); - assert.strictEqual(rootDiv.length, 1, 'should exist'); - assert.strictEqual(rootDiv.prop('aria-label'), 'Cheese', true); - assert.strictEqual(rootDiv.prop('role'), 'radiogroup', true); - }); - - it('should have radios inside the group', () => { - const radios = wrapper.find('[role="radio"]'); - assert.strictEqual(radios.length, 4, 'should have 4 radios'); - }); - - it('should have nothing selected but the first item tabIndexed', () => { - assertRadioSelected(wrapper, -1); - assertRadioTabIndexed(wrapper, 0); - }); - - it('should focus the second value', () => { - wrapper.find('[role="radio"]').first().get(0).focus(); - wrapper.simulate('keyDown', { which: keycode('down') }); - assertRadioTabIndexed(wrapper, 1); - assertRadioFocused(wrapper, 1); - }); - - it('should reset the tabIndex to the first item after blur', (done) => { - document.activeElement.blur(); - setTimeout(() => { - assertRadioTabIndexed(wrapper, 0); - done(); - }, 110); - }); - - it('should select/choose the first value', () => { - wrapper.find('[role="radio"]').first().simulate('click'); - wrapper.find('[role="radio"]').first().get(0).focus(); - assert.strictEqual(handleChange.callCount, 1, 'should have called handleChange'); - assert.strictEqual(handleChange.args[0][1], '0x', 'should pass 0x as the 2nd arg'); - - // controlled! - // this is our pseudo managed state in action - wrapper.setProps({ selectedValue: '0x' }); - assertRadioSelected(wrapper, 0); - assertRadioTabIndexed(wrapper, 0); - }); - - it('should focus the second value', () => { - wrapper.simulate('keyDown', { which: keycode('down') }); - assertRadioTabIndexed(wrapper, 1); - assertRadioFocused(wrapper, 1); - }); - - it('should focus the third value', () => { - wrapper.simulate('keyDown', { which: keycode('down') }); - assertRadioTabIndexed(wrapper, 2); - assertRadioFocused(wrapper, 2); - }); - - it('should select the third value', () => { - wrapper.find('[role="radio"]').at(2).simulate('click'); - assert.strictEqual(handleChange.callCount, 2, 'should have called handleChange'); - assert.strictEqual(handleChange.args[1][1], '2x', 'should pass 2x as the 2nd arg'); - }); - - it('should focus the fourth value', () => { - wrapper.simulate('keyDown', { which: keycode('down') }); - assertRadioTabIndexed(wrapper, 3); - assertRadioFocused(wrapper, 3); - }); - - it('should focus the second value', () => { - wrapper.simulate('keyDown', { which: keycode('up') }); - wrapper.simulate('keyDown', { which: keycode('up') }); - assertRadioTabIndexed(wrapper, 1); - assertRadioFocused(wrapper, 1); - }); - - it('should select the second value', () => { - wrapper.find('[role="radio"]').at(1).simulate('click'); - assert.strictEqual(handleChange.callCount, 3, 'should have called handleChange'); - assert.strictEqual(handleChange.args[2][1], '1x', 'should pass 1x as the 2nd arg'); - }); - }); - - describe('initially selected value', () => { - let wrapper; - - before(() => { - wrapper = mount( - - - - - - , - ); - }); - - it('should set the tabIndex to the third radio', () => { - assertRadioTabIndexed(wrapper, 2); - }); - - it('should set focus and tabIndex to the second radio', () => { - wrapper.find('[role="radio"]').at(2).get(0).focus(); - wrapper.simulate('keyDown', { which: keycode('up') }); - assertRadioTabIndexed(wrapper, 1); - assertRadioFocused(wrapper, 1); - }); - - it('should reset the tabIndex to the selected item after blur', (done) => { - document.activeElement.blur(); - setTimeout(() => { - assertRadioTabIndexed(wrapper, 2); - done(); - }, 110); - }); - }); - - describe('uncontrolled interaction', () => { - let handleChange; - let wrapper; - - before(() => { - handleChange = spy(); - wrapper = mount( - - - - - - , - ); - }); - - it('should detect uncontrolled use', () => { - assert.strictEqual(wrapper.instance().isControlled, false); - }); - - it('should have nothing selected but the first item tabIndexed', () => { - assertRadioSelected(wrapper, -1); - assertRadioTabIndexed(wrapper, 0); - }); - - it('should select/choose the first value', () => { - wrapper.find('[role="radio"]').first().simulate('click'); - wrapper.find('[role="radio"]').first().get(0).focus(); - assert.strictEqual(handleChange.callCount, 1, 'should have called handleChange'); - assert.strictEqual(handleChange.args[0][1], '0x', 'should pass 0x as the 2nd arg'); - assertRadioSelected(wrapper, 0); - assertRadioTabIndexed(wrapper, 0); - }); - - it('should select/choose the third value', () => { - wrapper.find('[role="radio"]').at(2).simulate('click'); - wrapper.find('[role="radio"]').at(2).get(0).focus(); - assert.strictEqual(handleChange.callCount, 2, 'should have called handleChange'); - assert.strictEqual(handleChange.args[1][1], '2x', 'should pass 0x as the 2nd arg'); - assertRadioSelected(wrapper, 2); - assertRadioTabIndexed(wrapper, 2); - }); - - it('should set focus and tabIndex to the fourth radio', () => { - wrapper.simulate('keyDown', { which: keycode('right') }); - assertRadioTabIndexed(wrapper, 3); - assertRadioFocused(wrapper, 3); - }); - - it('should set focus and tabIndex to the first radio', () => { - wrapper.simulate('keyDown', { which: keycode('down') }); - assertRadioTabIndexed(wrapper, 0); - assertRadioFocused(wrapper, 0); - }); - - it('should set focus and tabIndex to the fourth radio', () => { - wrapper.simulate('keyDown', { which: keycode('left') }); - assertRadioTabIndexed(wrapper, 3); - assertRadioFocused(wrapper, 3); - }); - - it('should set focus and tabIndex to the third radio', () => { - wrapper.simulate('keyDown', { which: keycode('up') }); - assertRadioTabIndexed(wrapper, 2); - assertRadioFocused(wrapper, 2); - }); - }); -}); diff --git a/test/regressions/screenshots/baseline/Checkbox/DisabledCheckbox/chrome-53.0.2785.143-linux.png b/test/regressions/screenshots/baseline/Checkbox/DisabledCheckbox/chrome-53.0.2785.143-linux.png index 1d0dae5c336c37..b6b5822dd1802b 100644 Binary files a/test/regressions/screenshots/baseline/Checkbox/DisabledCheckbox/chrome-53.0.2785.143-linux.png and b/test/regressions/screenshots/baseline/Checkbox/DisabledCheckbox/chrome-53.0.2785.143-linux.png differ diff --git a/test/regressions/screenshots/baseline/Radio/DisabledRadio/chrome-53.0.2785.143-linux.png b/test/regressions/screenshots/baseline/Radio/DisabledRadio/chrome-53.0.2785.143-linux.png index fcf58e4ef7af68..ce639bdc71ea7f 100644 Binary files a/test/regressions/screenshots/baseline/Radio/DisabledRadio/chrome-53.0.2785.143-linux.png and b/test/regressions/screenshots/baseline/Radio/DisabledRadio/chrome-53.0.2785.143-linux.png differ diff --git a/test/regressions/screenshots/baseline/Switch/DisabledSwitch/chrome-53.0.2785.143-linux.png b/test/regressions/screenshots/baseline/Switch/DisabledSwitch/chrome-53.0.2785.143-linux.png index 48a60810b9bc9c..751ac747c85cf1 100644 Binary files a/test/regressions/screenshots/baseline/Switch/DisabledSwitch/chrome-53.0.2785.143-linux.png and b/test/regressions/screenshots/baseline/Switch/DisabledSwitch/chrome-53.0.2785.143-linux.png differ diff --git a/test/regressions/site/src/tests/Checkbox/DisabledCheckbox.js b/test/regressions/site/src/tests/Checkbox/DisabledCheckbox.js index 1fe82d4f4c46e5..112e86650b1d52 100644 --- a/test/regressions/site/src/tests/Checkbox/DisabledCheckbox.js +++ b/test/regressions/site/src/tests/Checkbox/DisabledCheckbox.js @@ -1,12 +1,12 @@ // @flow weak import React from 'react'; -import Checkbox from 'material-ui/Checkbox'; +import Checkbox, { LabelCheckbox } from 'material-ui/Checkbox'; export default function DisabledCheckbox() { return (
- +
diff --git a/test/regressions/site/src/tests/Radio/DisabledRadio.js b/test/regressions/site/src/tests/Radio/DisabledRadio.js index 8cc412e1e55554..667b62a36e23c1 100644 --- a/test/regressions/site/src/tests/Radio/DisabledRadio.js +++ b/test/regressions/site/src/tests/Radio/DisabledRadio.js @@ -1,12 +1,12 @@ // @flow weak import React from 'react'; -import Radio from 'material-ui/Radio'; +import Radio, { LabelRadio } from 'material-ui/Radio'; export default function DisabledRadio() { return (
- +
diff --git a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabel.js b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabel.js index 04bc0484001b5f..f8b522a2c5b32a 100644 --- a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabel.js +++ b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabel.js @@ -2,7 +2,7 @@ import React from 'react'; import { FormLabel } from 'material-ui/Form'; -import { RadioGroup, Radio } from 'material-ui/Radio'; +import { RadioGroup, LabelRadio as Radio } from 'material-ui/Radio'; export default function RadioGroupWithLabel() { return ( diff --git a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelError.js b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelError.js index 52ee4810574e69..eab862c7c1a969 100644 --- a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelError.js +++ b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelError.js @@ -2,7 +2,7 @@ import React from 'react'; import { FormLabel } from 'material-ui/Form'; -import { RadioGroup, Radio } from 'material-ui/Radio'; +import { RadioGroup, LabelRadio as Radio } from 'material-ui/Radio'; export default function RadioGroupWithLabelError() { return ( diff --git a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelRequired.js b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelRequired.js index d900ec4c050a41..fbd725bec4d28b 100644 --- a/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelRequired.js +++ b/test/regressions/site/src/tests/RadioGroup/RadioGroupWithLabelRequired.js @@ -2,7 +2,7 @@ import React from 'react'; import { FormLabel } from 'material-ui/Form'; -import { RadioGroup, Radio } from 'material-ui/Radio'; +import { RadioGroup, LabelRadio as Radio } from 'material-ui/Radio'; export default function RadioGroupWithLabelRequired() { return ( diff --git a/test/regressions/site/src/tests/RadioGroup/SimpleRadioGroup.js b/test/regressions/site/src/tests/RadioGroup/SimpleRadioGroup.js index 1b43d0705d4681..12b49327c926a9 100644 --- a/test/regressions/site/src/tests/RadioGroup/SimpleRadioGroup.js +++ b/test/regressions/site/src/tests/RadioGroup/SimpleRadioGroup.js @@ -1,7 +1,7 @@ // @flow weak import React from 'react'; -import { RadioGroup, Radio } from 'material-ui/Radio'; +import { RadioGroup, LabelRadio as Radio } from 'material-ui/Radio'; export default function SimpleRadioGroup() { return ( diff --git a/test/regressions/site/src/tests/Switch/DisabledSwitch.js b/test/regressions/site/src/tests/Switch/DisabledSwitch.js index 8ab92bd7f0eca1..6d93c52878a3c8 100644 --- a/test/regressions/site/src/tests/Switch/DisabledSwitch.js +++ b/test/regressions/site/src/tests/Switch/DisabledSwitch.js @@ -1,12 +1,12 @@ // @flow weak import React from 'react'; -import Switch from 'material-ui/Switch'; +import Switch, { LabelSwitch } from 'material-ui/Switch'; export default function DisabledSwitch() { return (
- +