diff --git a/.size-limit.js b/.size-limit.js index f707fa5eef8add..96333c79f22769 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -33,7 +33,7 @@ module.exports = [ name: 'The main bundle of the docs', webpack: false, path: getMainFile().path, - limit: '177 KB', + limit: '177.1 KB', }, { name: 'The home page of the docs', diff --git a/docs/src/modules/components/withRoot.js b/docs/src/modules/components/withRoot.js index 763cf600ace49c..cdb4bb4bb0e79a 100644 --- a/docs/src/modules/components/withRoot.js +++ b/docs/src/modules/components/withRoot.js @@ -185,11 +185,14 @@ const pages = [ pathname: '/lab/about', title: 'About The Lab', }, + { + pathname: '/lab/slider', + }, { pathname: '/lab/speed-dial', }, { - pathname: '/lab/slider', + pathname: '/lab/toggle-button', }, findPages[2].children[1], ], diff --git a/docs/src/pages/lab/toggle-button/ToggleButtons.js b/docs/src/pages/lab/toggle-button/ToggleButtons.js new file mode 100644 index 00000000000000..fbf55b7cf37855 --- /dev/null +++ b/docs/src/pages/lab/toggle-button/ToggleButtons.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import FormatAlignLeftIcon from '@material-ui/icons/FormatAlignLeft'; +import FormatAlignCenterIcon from '@material-ui/icons/FormatAlignCenter'; +import FormatAlignRightIcon from '@material-ui/icons/FormatAlignRight'; +import FormatAlignJustifyIcon from '@material-ui/icons/FormatAlignJustify'; +import FormatBoldIcon from '@material-ui/icons/FormatBold'; +import FormatItalicIcon from '@material-ui/icons/FormatItalic'; +import FormatUnderlinedIcon from '@material-ui/icons/FormatUnderlined'; +import FormatColorFillIcon from '@material-ui/icons/FormatColorFill'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import ToggleButton, { ToggleButtonGroup } from '@material-ui/lab/ToggleButton'; + +const styles = theme => ({ + toggleContainer: { + height: 56, + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + margin: `${theme.spacing.unit}px 0`, + background: theme.palette.background.default, + }, +}); + +class ToggleButtons extends React.Component { + state = { + alignment: 'left', + formats: ['bold'], + }; + + handleFormat = formats => this.setState({ formats }); + + handleAlignment = alignment => this.setState({ alignment }); + + render() { + const { classes } = this.props; + const { alignment, formats } = this.state; + + return ( + + +
+ + + + + + + + + + + + + + +
+ + Exclusive Selection + + + Text justification toggle buttons present options for left, right, center, full, and + justified text with only one item available for selection at a time. Selecting one + option deselects any other. + +
+ +
+ + + + + + + + + + + + + + + +
+ + Multiple Selection + + + Logically-grouped options, like Bold, Italic, and Underline, allow multiple options to + be selected. + +
+
+ ); + } +} + +ToggleButtons.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(ToggleButtons); diff --git a/docs/src/pages/lab/toggle-button/toggle-button.md b/docs/src/pages/lab/toggle-button/toggle-button.md new file mode 100644 index 00000000000000..31ee3eebcf1d98 --- /dev/null +++ b/docs/src/pages/lab/toggle-button/toggle-button.md @@ -0,0 +1,16 @@ +--- +title: Toggle Button React component +components: ToggleButton, ToggleButtonGroup +--- + +# Toggle Buttons + +

Toggle buttons can be used to group related options.

+ +To emphasize groups of related [Toggle buttons](https://material.io/design/components/buttons.html#toggle-button), +a group should share a common container. + +The `ToggleButtonGroup` will control the selected of its child buttons when +given its own `value` prop. + +{{"demo": "pages/lab/toggle-button/ToggleButtons.js"}} diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButton.d.ts b/packages/material-ui-lab/src/ToggleButton/ToggleButton.d.ts new file mode 100644 index 00000000000000..ee74772c8d4db1 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButton.d.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { StandardProps, PropTypes } from '..'; +import { ButtonBaseProps, ButtonBaseClassKey } from '../ButtonBase'; + +export interface ToggleButtonProps + extends StandardProps { + component?: React.ReactType; + disabled?: boolean; + disableFocusRipple?: boolean; + disableRipple?: boolean; + selected?: boolean; + type?: string; + value?: any; +} + +export type ToggleButtonClassKey = ButtonBaseClassKey | 'label' | 'selected'; + +declare const ToggleButton: React.ComponentType; + +export default ToggleButton; diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButton.js b/packages/material-ui-lab/src/ToggleButton/ToggleButton.js new file mode 100644 index 00000000000000..16634e43e5e9bf --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButton.js @@ -0,0 +1,189 @@ +// @inheritedComponent ButtonBase + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import ButtonBase from '@material-ui/core/ButtonBase'; + +export const styles = theme => ({ + root: { + ...theme.typography.button, + height: 32, + minWidth: 48, + margin: 0, + padding: `${theme.spacing.unit - 4}px ${theme.spacing.unit * 1.5}px`, + borderRadius: 2, + willChange: 'opacity', + color: fade(theme.palette.action.active, 0.38), + '&:hover': { + textDecoration: 'none', + // Reset on mouse devices + backgroundColor: fade(theme.palette.text.primary, 0.12), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + '&$disabled': { + backgroundColor: 'transparent', + }, + }, + + '&:not(:first-child)': { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }, + + '&:not(:last-child)': { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + }, + label: { + width: '100%', + display: 'inherit', + alignItems: 'inherit', + justifyContent: 'inherit', + }, + disabled: { + color: fade(theme.palette.action.disabled, 0.12), + }, + selected: { + color: theme.palette.action.active, + '&:after': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + borderRadius: 'inherit', + width: '100%', + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.38, + }, + + '& + &:before': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + width: 1, + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.12, + }, + }, +}); + +class ToggleButton extends React.Component { + handleChange = event => { + const { onChange, onClick, value } = this.props; + + if (onClick) { + onClick(event); + if (event.isDefaultPrevented()) { + return; + } + } + + if (onChange) { + onChange(value); + } + }; + + render() { + const { + children, + className: classNameProp, + classes, + disableFocusRipple, + disabled, + selected, + ...other + } = this.props; + + const className = classNames( + classes.root, + { + [classes.disabled]: disabled, + [classes.selected]: selected, + }, + classNameProp, + ); + + return ( + + {children} + + ); + } +} + +ToggleButton.propTypes = { + /** + * The content of the button. + */ + children: PropTypes.node.isRequired, + /** + * Useful to extend the style applied to components. + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * If `true`, the button will be disabled. + */ + disabled: PropTypes.bool, + /** + * If `true`, the keyboard focus ripple will be disabled. + * `disableRipple` must also be true. + */ + disableFocusRipple: PropTypes.bool, + /** + * If `true`, the ripple effect will be disabled. + */ + disableRipple: PropTypes.bool, + /** + * @ignore + */ + onChange: PropTypes.func, + /** + * @ignore + */ + onClick: PropTypes.func, + /** + * If `true`, the button will be rendered in an active state. + */ + selected: PropTypes.bool, + /** + * The value to associate with the button when selected in a + * ToggleButtonGroup. + */ + value: PropTypes.any.isRequired, +}; + +ToggleButton.defaultProps = { + disabled: false, + disableFocusRipple: false, + disableRipple: false, +}; + +ToggleButton.muiName = 'ToggleButton'; + +export default withStyles(styles, { name: 'MuiToggleButton' })(ToggleButton); diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButton.test.js b/packages/material-ui-lab/src/ToggleButton/ToggleButton.test.js new file mode 100644 index 00000000000000..c38690c1474104 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButton.test.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { assert } from 'chai'; +import { spy } from 'sinon'; +import { createRender, createShallow, getClasses } from '@material-ui/core/test-utils'; +import ButtonBase from '@material-ui/core/ButtonBase'; +import ToggleButton from './ToggleButton'; + +describe('', () => { + let shallow; + let render; + let classes; + + before(() => { + shallow = createShallow({ dive: true }); + render = createRender(); + classes = getClasses(Hello World); + }); + + it('should render a element', () => { + const wrapper = shallow(Hello World); + assert.strictEqual(wrapper.type(), ButtonBase); + }); + + it('should render the custom className and the root class', () => { + const wrapper = shallow( + + Hello World + , + ); + assert.strictEqual(wrapper.is('.test-class-name'), true, 'should pass the test className'); + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); + + it('should render a selected button', () => { + const wrapper = shallow( + + Hello World + , + ); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual(wrapper.hasClass(classes.selected), true, 'should have the selected class'); + }); + + it('should render a disabled button', () => { + const wrapper = shallow( + + Hello World + , + ); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual(wrapper.hasClass(classes.disabled), true, 'should have the disabled class'); + }); + + describe('prop: onChange', () => { + it('should be called when clicked', () => { + const handleChange = spy(); + const wrapper = shallow( + + Hello + , + ); + const event = {}; + + wrapper.simulate('click', event); + + assert.strictEqual(handleChange.callCount, 1); + }); + + it('should be called with the button value', () => { + const handleChange = spy(); + const wrapper = shallow( + + Hello + , + ); + const event = {}; + + wrapper.simulate('click', event); + + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChange.args[0][0], 'one'); + }); + + it('should not be called if the click is prevented', () => { + const handleChange = spy(); + const wrapper = shallow( + e.preventDefault()}> + Hello + , + ); + const event = { + preventDefault: () => {}, + isDefaultPrevented: () => true, + }; + + wrapper.simulate('click', event); + + assert.strictEqual(handleChange.callCount, 0); + }); + }); + + describe('server side', () => { + // Only run the test on node. + if (!/jsdom/.test(window.navigator.userAgent)) { + return; + } + + it('should server side render', () => { + const markup = render(Hello World); + assert.strictEqual(markup.text(), 'Hello World'); + }); + }); +}); diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.d.ts b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.d.ts new file mode 100644 index 00000000000000..4a74da15806b3e --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.d.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { StandardProps } from '..'; + +export interface ToggleButtonGroupProps + extends StandardProps, ToggleButtonGroupClassKey> { + selected?: boolean; + exclusive?: boolean; + value?: any; +} + +export type ToggleButtonGroupClassKey = 'root' | 'selected'; + +declare const ToggleButtonGroup: React.ComponentType; + +export default ToggleButtonGroup; diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.js b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.js new file mode 100644 index 00000000000000..361af634f13ffb --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.js @@ -0,0 +1,142 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import hasValue from './hasValue'; +import isValueSelected from './isValueSelected'; + +export const styles = theme => ({ + root: { + transition: theme.transitions.create('background,box-shadow'), + background: 'transparent', + borderRadius: 2, + overflow: 'hidden', + }, + + selected: { + background: theme.palette.background.paper, + boxShadow: theme.shadows[2], + }, +}); + +class ToggleButtonGroup extends React.Component { + handleChange = buttonValue => { + const { onChange, value } = this.props; + + if (!onChange) { + return; + } + + const index = value && value.indexOf(buttonValue); + let newValue; + + if (value && index >= 0) { + newValue = [...value]; + newValue.splice(index, 1); + if (newValue.length === 0) newValue = null; + } else { + newValue = value ? [...value, buttonValue] : [buttonValue]; + } + + onChange(newValue); + }; + + handleExclusiveChange = buttonValue => { + const { onChange, value } = this.props; + + if (!onChange) { + return; + } + + onChange(value === buttonValue ? null : buttonValue); + }; + + render() { + const { + children: childrenProp, + className: classNameProp, + classes, + exclusive, + onChange, + selected: selectedProp, + value, + ...other + } = this.props; + + const children = React.Children.map(childrenProp, child => { + if (!React.isValidElement(child)) { + return null; + } + + const { selected: buttonSelected, value: buttonValue } = child.props; + + const selected = + buttonSelected === undefined ? isValueSelected(buttonValue, value) : buttonSelected; + + return React.cloneElement(child, { + selected, + onChange: exclusive ? this.handleExclusiveChange : this.handleChange, + }); + }); + + const groupSelected = selectedProp === 'auto' ? hasValue(value) : selectedProp; + const className = classNames( + classes.root, + { + [classes.selected]: groupSelected, + }, + classNameProp, + ); + + return ( +
+ {children} +
+ ); + } +} + +ToggleButtonGroup.propTypes = { + /** + * The content of the button. + */ + children: PropTypes.node.isRequired, + /** + * Useful to extend the style applied to components. + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * If `true` only allow one of the child ToggleButton values to be selected. + */ + exclusive: PropTypes.bool, + /** + * Callback fired when the value changes. + * + * @param {object} event The event source of the callback + * @param {object} value of the selected buttons. When `exclusive` is true + * this is a single value; when false an array of selected values. + */ + onChange: PropTypes.func, + /** + * If `true` render the group in a selected state. If `auto` (the default) + * render in a selected state if `value` is not empty. + */ + selected: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['auto'])]), + /** + * The currently selected value within the group or an array of selected + * values when `exclusive` is false. + */ + value: PropTypes.any, +}; + +ToggleButtonGroup.defaultProps = { + exclusive: false, + selected: 'auto', + value: null, +}; + +export default withStyles(styles, { name: 'MuiToggleButtonGroup' })(ToggleButtonGroup); diff --git a/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.test.js b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.test.js new file mode 100644 index 00000000000000..8afc0311f3a7bd --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.test.js @@ -0,0 +1,260 @@ +import React from 'react'; +import { assert } from 'chai'; +import { spy } from 'sinon'; +import { createMount, createShallow, getClasses } from '@material-ui/core/test-utils'; +import ToggleButtonGroup from './ToggleButtonGroup'; +import ToggleButton from './ToggleButton'; + +describe('', () => { + let shallow; + let mount; + let classes; + + before(() => { + mount = createMount(); + shallow = createShallow({ dive: true }); + classes = getClasses( + + + , + ); + }); + + it('should render a
element', () => { + const wrapper = shallow( + + + , + ); + assert.strictEqual(wrapper.type(), 'div'); + }); + + it('should render the custom className and the root class', () => { + const wrapper = shallow( + + + , + ); + assert.strictEqual(wrapper.is('.test-class-name'), true, 'should pass the test className'); + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); + + it('should render a selected div', () => { + const wrapper = shallow( + + + , + ); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual(wrapper.hasClass(classes.selected), true, 'should have the selected class'); + }); + + it('should render a selected div when selected is "auto" and a value is present', () => { + const wrapper = shallow( + + + , + ); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual(wrapper.hasClass(classes.selected), true, 'should have the selected class'); + }); + + it('should not render a selected div when selected is "auto" and a value is missing', () => { + const wrapper = shallow( + + + , + ); + assert.strictEqual(wrapper.hasClass(classes.root), true); + assert.strictEqual( + wrapper.hasClass(classes.selected), + false, + 'should not have the selected class', + ); + }); + + describe('exclusive', () => { + it('should render a selected ToggleButton if value is selected', () => { + const wrapper = shallow( + + + , + ); + const buttonWrapper = wrapper.find(ToggleButton); + + assert.strictEqual(buttonWrapper.props().selected, true); + }); + + it('should not render a selected ToggleButton when its value is not selected', () => { + const wrapper = shallow( + + + + , + ); + const buttonWrapper = wrapper.find(ToggleButton).at(1); + + assert.strictEqual(buttonWrapper.props().selected, false); + }); + }); + + describe('non exclusive', () => { + it('should render a selected ToggleButton if value is selected', () => { + const wrapper = shallow( + + + + , + ); + + assert.strictEqual( + wrapper + .find(ToggleButton) + .at(0) + .props().selected, + true, + 'should be selected', + ); + assert.strictEqual( + wrapper + .find(ToggleButton) + .at(1) + .props().selected, + false, + 'should not be selected', + ); + }); + }); + + describe('prop: onChange', () => { + describe('exclusive', () => { + it('should be null when current value is toggled off', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(0) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChange.args[0][0], null); + }); + + it('should be a single value when value is toggled on', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(0) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChange.args[0][0], 'one'); + }); + + it('should be a single value when a new value is toggled on', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(1) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChange.args[0][0], 'two'); + }); + }); + + describe('non exclusive', () => { + it('should be null when current value is toggled off', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(0) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.strictEqual(handleChange.args[0][0], null); + }); + + it('should be an array with a single value when value is toggled on', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(0) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.deepEqual(handleChange.args[0][0], ['one']); + }); + + it('should be an array with a single value when a secondary value is toggled off', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(0) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.deepEqual(handleChange.args[0][0], ['two']); + }); + + it('should be an array of all selected values when a second value is toggled on', () => { + const handleChange = spy(); + const wrapper = mount( + + One + Two + , + ); + + wrapper + .find(ToggleButton) + .at(1) + .simulate('click'); + + assert.strictEqual(handleChange.callCount, 1); + assert.deepEqual(handleChange.args[0][0], ['one', 'two']); + }); + }); + }); +}); diff --git a/packages/material-ui-lab/src/ToggleButton/hasValue.js b/packages/material-ui-lab/src/ToggleButton/hasValue.js new file mode 100644 index 00000000000000..68504ba0e561c8 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/hasValue.js @@ -0,0 +1,8 @@ +// Determines if the given toggle group value is present. +export default function hasValue(value) { + if (Array.isArray(value)) { + return value.length > 0; + } + + return !!value; +} diff --git a/packages/material-ui-lab/src/ToggleButton/hasValue.test.js b/packages/material-ui-lab/src/ToggleButton/hasValue.test.js new file mode 100644 index 00000000000000..2ab9b6f35f51cb --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/hasValue.test.js @@ -0,0 +1,24 @@ +import { assert } from 'chai'; +import hasValue from './hasValue'; + +describe(' hasValue', () => { + it('should be true for a scalar value', () => { + assert.strictEqual(hasValue('yep'), true); + }); + + it('should be true for a non-empty array', () => { + assert.strictEqual(hasValue(['got one']), true); + }); + + it('should be false for an empty array', () => { + assert.strictEqual(hasValue([]), false); + }); + + it('should be false for undefined', () => { + assert.strictEqual(hasValue(undefined), false); + }); + + it('should be false for null', () => { + assert.strictEqual(hasValue(null), false); + }); +}); diff --git a/packages/material-ui-lab/src/ToggleButton/index.d.ts b/packages/material-ui-lab/src/ToggleButton/index.d.ts new file mode 100644 index 00000000000000..a7a15e9cd24fe3 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/index.d.ts @@ -0,0 +1,3 @@ +export { default } from './ToggleButton'; +export { default as ToggleButtonGroup } from './ToggleButtonGroup'; +export * from './ToggleButton'; diff --git a/packages/material-ui-lab/src/ToggleButton/index.js b/packages/material-ui-lab/src/ToggleButton/index.js new file mode 100644 index 00000000000000..a7a15e9cd24fe3 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/index.js @@ -0,0 +1,3 @@ +export { default } from './ToggleButton'; +export { default as ToggleButtonGroup } from './ToggleButtonGroup'; +export * from './ToggleButton'; diff --git a/packages/material-ui-lab/src/ToggleButton/isValueSelected.js b/packages/material-ui-lab/src/ToggleButton/isValueSelected.js new file mode 100644 index 00000000000000..068c06c00173f4 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/isValueSelected.js @@ -0,0 +1,13 @@ +// Determine if the toggle button value matches, or is contained in, the +// candidate group value. +export default function isValueSelected(value, candidate) { + if (candidate === undefined || value === undefined) { + return false; + } + + if (Array.isArray(candidate)) { + return candidate.indexOf(value) >= 0; + } + + return value === candidate; +} diff --git a/packages/material-ui-lab/src/ToggleButton/isValueSelected.test.js b/packages/material-ui-lab/src/ToggleButton/isValueSelected.test.js new file mode 100644 index 00000000000000..143b7fbe8e3278 --- /dev/null +++ b/packages/material-ui-lab/src/ToggleButton/isValueSelected.test.js @@ -0,0 +1,40 @@ +import { assert } from 'chai'; +import isValueSelected from './isValueSelected'; + +describe(' isValueSelected', () => { + it('is false when value is undefined', () => { + assert.strictEqual(isValueSelected(undefined, [undefined]), false); + }); + + it('is false when candidate is undefined', () => { + assert.strictEqual(isValueSelected('example', undefined), false); + }); + + describe('non exclusive', () => { + it('is true if candidate is contained in value', () => { + assert.strictEqual(isValueSelected('one', ['one']), true); + }); + + it('is false if value is not contained in candidate', () => { + assert.strictEqual(isValueSelected('one', ['two']), false); + }); + + it('is false if value is loosely contained in candidate', () => { + assert.strictEqual(isValueSelected('3', [3]), false); + }); + }); + + describe('exclusive', () => { + it('is true if candidate strictly equals value', () => { + assert.strictEqual(isValueSelected('one', 'one'), true); + }); + + it('is false if candidate does not equal value', () => { + assert.strictEqual(isValueSelected('two', 'one'), false); + }); + + it('is false if candidate loosely equals value', () => { + assert.strictEqual(isValueSelected('3', 3), false); + }); + }); +}); diff --git a/packages/material-ui-lab/src/index.js b/packages/material-ui-lab/src/index.js index 4bd3a6e8c7fd2f..209e6bf1aac574 100644 --- a/packages/material-ui-lab/src/index.js +++ b/packages/material-ui-lab/src/index.js @@ -1,3 +1,6 @@ export { default as SpeedDial } from './SpeedDial'; export { default as SpeedDialAction } from './SpeedDialAction'; export { default as SpeedDialIcon } from './SpeedDialIcon'; + +export { default as ToggleButton } from './ToggleButton/ToggleButton'; +export { default as ToggleButtonGroup } from './ToggleButton/ToggleButtonGroup'; diff --git a/pages/lab/api/toggle-button-group.js b/pages/lab/api/toggle-button-group.js new file mode 100644 index 00000000000000..3c86f55502a6f0 --- /dev/null +++ b/pages/lab/api/toggle-button-group.js @@ -0,0 +1,10 @@ +import React from 'react'; +import withRoot from 'docs/src/modules/components/withRoot'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import markdown from './toggle-button-group.md'; + +function Page() { + return ; +} + +export default withRoot(Page); diff --git a/pages/lab/api/toggle-button-group.md b/pages/lab/api/toggle-button-group.md new file mode 100644 index 00000000000000..b68355d2134920 --- /dev/null +++ b/pages/lab/api/toggle-button-group.md @@ -0,0 +1,45 @@ +--- +filename: /packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.js +title: ToggleButtonGroup API +--- + + + +# ToggleButtonGroup + +

The API documentation of the ToggleButtonGroup React component.

+ + + +## Props + +| Name | Type | Default | Description | +|:-----|:-----|:--------|:------------| +| children * | node |   | The content of the button. | +| classes | object |   | Useful to extend the style applied to components. | +| exclusive | bool | false | If `true` only allow one of the child ToggleButton values to be selected. | +| onChange | func |   | Callback fired when the value changes.

**Signature:**
`function(event: object, value: object) => void`
*event:* The event source of the callback
*value:* of the selected buttons. When `exclusive` is true this is a single value; when false an array of selected values. | +| selected | union: bool |
 enum: 'auto'

| 'auto' | If `true` render the group in a selected state. If `auto` (the default) render in a selected state if `value` is not empty. | +| value | any | null | The currently selected value within the group or an array of selected values when `exclusive` is false. | + +Any other properties supplied will be spread to the root element (native element). + +## CSS API + +You can override all the class names injected by Material-UI thanks to the `classes` property. +This property accepts the following keys: +- `root` +- `selected` + +Have a look at [overriding with classes](/customization/overrides#overriding-with-classes) section +and the [implementation of the component](https://github.com/mui-org/material-ui/tree/master/packages/material-ui-lab/src/ToggleButton/ToggleButtonGroup.js) +for more detail. + +If using the `overrides` key of the theme as documented +[here](/customization/themes#customizing-all-instances-of-a-component-type), +you need to use the following style sheet name: `MuiToggleButtonGroup`. + +## Demos + +- [Toggle Button](/lab/toggle-button) + diff --git a/pages/lab/api/toggle-button.js b/pages/lab/api/toggle-button.js new file mode 100644 index 00000000000000..a8419ce2e4f01b --- /dev/null +++ b/pages/lab/api/toggle-button.js @@ -0,0 +1,10 @@ +import React from 'react'; +import withRoot from 'docs/src/modules/components/withRoot'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import markdown from './toggle-button.md'; + +function Page() { + return ; +} + +export default withRoot(Page); diff --git a/pages/lab/api/toggle-button.md b/pages/lab/api/toggle-button.md new file mode 100644 index 00000000000000..8ab6917600947e --- /dev/null +++ b/pages/lab/api/toggle-button.md @@ -0,0 +1,53 @@ +--- +filename: /packages/material-ui-lab/src/ToggleButton/ToggleButton.js +title: ToggleButton API +--- + + + +# ToggleButton + +

The API documentation of the ToggleButton React component.

+ + + +## Props + +| Name | Type | Default | Description | +|:-----|:-----|:--------|:------------| +| children * | node |   | The content of the button. | +| classes | object |   | Useful to extend the style applied to components. | +| disabled | bool | false | If `true`, the button will be disabled. | +| disableFocusRipple | bool | false | If `true`, the keyboard focus ripple will be disabled. `disableRipple` must also be true. | +| disableRipple | bool | false | If `true`, the ripple effect will be disabled. | +| selected | bool |   | If `true`, the button will be rendered in an active state. | +| value * | any |   | The value to associate with the button when selected in a ToggleButtonGroup. | + +Any other properties supplied will be spread to the root element ([ButtonBase](/api/button-base)). + +## CSS API + +You can override all the class names injected by Material-UI thanks to the `classes` property. +This property accepts the following keys: +- `root` +- `label` +- `disabled` +- `selected` + +Have a look at [overriding with classes](/customization/overrides#overriding-with-classes) section +and the [implementation of the component](https://github.com/mui-org/material-ui/tree/master/packages/material-ui-lab/src/ToggleButton/ToggleButton.js) +for more detail. + +If using the `overrides` key of the theme as documented +[here](/customization/themes#customizing-all-instances-of-a-component-type), +you need to use the following style sheet name: `MuiToggleButton`. + +## Inheritance + +The properties of the [ButtonBase](/api/button-base) component are also available. +You can take advantage of this behavior to [target nested components](/guides/api#spread). + +## Demos + +- [Toggle Button](/lab/toggle-button) + diff --git a/pages/lab/toggle-button.js b/pages/lab/toggle-button.js new file mode 100644 index 00000000000000..b10bf37e8dfd61 --- /dev/null +++ b/pages/lab/toggle-button.js @@ -0,0 +1,23 @@ +import React from 'react'; +import withRoot from 'docs/src/modules/components/withRoot'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import markdown from 'docs/src/pages/lab/toggle-button/toggle-button.md'; + +function Page() { + return ( + + ); +} + +export default withRoot(Page);