diff --git a/docs/src/pages/component-api/MobileStepper/MobileStepper.md b/docs/src/pages/component-api/MobileStepper/MobileStepper.md new file mode 100644 index 00000000000000..a81d53f58dcc9c --- /dev/null +++ b/docs/src/pages/component-api/MobileStepper/MobileStepper.md @@ -0,0 +1,39 @@ +# MobileStepper + + + +## Props +| Name | Type | Default | Description | +|:-----|:-----|:--------|:------------| +| activeStep | number | `0` | Specifies the currently active step. | +| disableBack | bool | `false` | Set to disable the back button. | +| disableNext | bool | `false` | Set to disable the next button. | +| kind | `text`, `dots` or `progress` | `dots` | Defines the kind of mobile stepper to use. | +| onBack | function | | Supplied to the onClick attribute of the back button. | +| onNext | function | | Supplied to the onClick attribute of the next button. | +| steps | number | | The total amount of steps. | +| buttonClassName | string | | Specify an extra class to be put on back/next buttons | +| className | string | | Specify an extra class to be put on the root element | +| dotClassName | string | | Specify an extra class to be put on each dot element | +| dotsClassName | string | | Specify an extra class to be put the container that holds the dots | +| progressClassname | string | | Specify an extra class to be put the container that holds the component. | + +Any other properties supplied will be spread to the root element. + +## Classes + +You can overrides all the class names injected by Material-UI thanks to the `classes` property. +This property accepts the following keys: +- `mobileStepper` +- `button` +- `dots` +- `dot` +- `dotActive` +- `progress` + +Have a look at [overriding with class names](/customization/overrides#overriding-with-class-names) +section 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: `MuiAppBar`. diff --git a/docs/src/pages/component-demos/mobile-stepper/DotsMobileStepper.js b/docs/src/pages/component-demos/mobile-stepper/DotsMobileStepper.js new file mode 100644 index 00000000000000..c6e8b2ee35f2f2 --- /dev/null +++ b/docs/src/pages/component-demos/mobile-stepper/DotsMobileStepper.js @@ -0,0 +1,61 @@ +// @flow + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, createStyleSheet } from 'material-ui/styles'; +import MobileStepper from 'material-ui/MobileStepper'; + +const styleSheet = createStyleSheet('DotsMobileStepper', { + root: { + position: 'relative', + marginTop: 30, + width: '100%', + }, + mobileStepper: { + position: 'relative', + }, +}); + +class DotsMobileStepper extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + }; + + state = { + activeStep: 0, + }; + + handleOnNext = () => { + const activeStep = this.state.activeStep === 5 ? 5 : this.state.activeStep + 1; + this.setState({ + activeStep, + }); + }; + + handleOnBack = () => { + const activeStep = this.state.activeStep === 0 ? 0 : this.state.activeStep - 1; + this.setState({ + activeStep, + }); + }; + + render() { + const classes = this.props.classes; + return ( +
+ +
+ ); + } +} + +export default withStyles(styleSheet)(DotsMobileStepper); diff --git a/docs/src/pages/component-demos/mobile-stepper/ProgressMobileStepper.js b/docs/src/pages/component-demos/mobile-stepper/ProgressMobileStepper.js new file mode 100644 index 00000000000000..7e8994266acf5f --- /dev/null +++ b/docs/src/pages/component-demos/mobile-stepper/ProgressMobileStepper.js @@ -0,0 +1,61 @@ +// @flow + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, createStyleSheet } from 'material-ui/styles'; +import MobileStepper from 'material-ui/MobileStepper'; + +const styleSheet = createStyleSheet('ProgressMobileStepper', { + root: { + position: 'relative', + marginTop: 30, + width: '100%', + }, + mobileStepper: { + position: 'relative', + }, +}); + +class ProgressMobileStepper extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + }; + + state = { + activeStep: 0, + }; + + handleOnNext = () => { + const activeStep = this.state.activeStep === 5 ? 5 : this.state.activeStep + 1; + this.setState({ + activeStep, + }); + }; + + handleOnBack = () => { + const activeStep = this.state.activeStep === 0 ? 0 : this.state.activeStep - 1; + this.setState({ + activeStep, + }); + }; + + render() { + const classes = this.props.classes; + return ( +
+ +
+ ); + } +} + +export default withStyles(styleSheet)(ProgressMobileStepper); diff --git a/docs/src/pages/component-demos/mobile-stepper/TextMobileStepper.js b/docs/src/pages/component-demos/mobile-stepper/TextMobileStepper.js new file mode 100644 index 00000000000000..9cbc3776ace59b --- /dev/null +++ b/docs/src/pages/component-demos/mobile-stepper/TextMobileStepper.js @@ -0,0 +1,78 @@ +// @flow + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, createStyleSheet } from 'material-ui/styles'; +import MobileStepper from 'material-ui/MobileStepper'; +import Paper from 'material-ui/Paper'; + +const styleSheet = createStyleSheet('TextMobileStepper', { + root: { + position: 'relative', + marginTop: 30, + width: '100%', + }, + mobileStepper: { + position: 'relative', + }, + textualDescription: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + position: 'relative', + height: '50px', + left: 0, + fontSize: '14px', + paddingLeft: '28px', + marginBottom: '20px', + }, +}); + +class TextMobileStepper extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + }; + + state = { + activeStep: 0, + }; + + handleOnNext = () => { + const activeStep = this.state.activeStep === 5 ? 5 : this.state.activeStep + 1; + this.setState({ + activeStep, + }); + }; + + handleOnBack = () => { + const activeStep = this.state.activeStep === 0 ? 0 : this.state.activeStep - 1; + this.setState({ + activeStep, + }); + }; + + render() { + const classes = this.props.classes; + return ( +
+ + Step {this.state.activeStep + 1} of 6 + + +
+ ); + } +} + +export default withStyles(styleSheet)(TextMobileStepper); diff --git a/docs/src/pages/component-demos/mobile-stepper/mobile-stepper.md b/docs/src/pages/component-demos/mobile-stepper/mobile-stepper.md new file mode 100644 index 00000000000000..1d3436262e5719 --- /dev/null +++ b/docs/src/pages/component-demos/mobile-stepper/mobile-stepper.md @@ -0,0 +1,22 @@ +--- +components: MobileStepper +--- + +# MobileStepper + +The [MobileStepper](https://material.io/guidelines/layout/structure.html#structure-mobile-stepper) implements a compact stepper suitable for a mobile device. + +## Mobile Stepper - Dots + +{{demo='pages/component-demos/mobile-stepper/DotsMobileStepper.js'}} + +## Mobile Stepper - Text + +This is essentially a back/next button positioned correctly. You must implement the textual description yourself however an example is provided below for reference. + +{{demo='pages/component-demos/mobile-stepper/TextMobileStepper.js'}} + +## Mobile Stepper - Progress + +{{demo='pages/component-demos/mobile-stepper/ProgressMobileStepper.js'}} + diff --git a/docs/src/pages/getting-started/supported-components.md b/docs/src/pages/getting-started/supported-components.md index dc239579be2e2a..e25578f7f8b37f 100644 --- a/docs/src/pages/getting-started/supported-components.md +++ b/docs/src/pages/getting-started/supported-components.md @@ -96,6 +96,7 @@ to discuss the approach before submitting a PR. - [Steppers](https://www.google.com/design/spec/components/steppers.html) - [Horizontal](https://www.google.com/design/spec/components/steppers.html#steppers-types-of-steppers) - [Vertical](https://www.google.com/design/spec/components/steppers.html#steppers-types-of-steppers) + - [MobileStepper](https://www.google.com/design/spec/components/mobile-stepper.html) - **[Tabs](https://www.google.com/design/spec/components/tabs.html) ✓** - Usage - **[Mobile (Full width)](https://www.google.com/design/spec/components/tabs.html#tabs-usage) ✓** diff --git a/src/MobileStepper/MobileStepper.js b/src/MobileStepper/MobileStepper.js new file mode 100644 index 00000000000000..658a5bf23b7892 --- /dev/null +++ b/src/MobileStepper/MobileStepper.js @@ -0,0 +1,165 @@ +// @flow weak + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { createStyleSheet } from 'jss-theme-reactor'; +import withStyles from '../styles/withStyles'; +import Paper from '../Paper'; +import Button from '../Button'; +import KeyboardArrowLeft from '../svg-icons/keyboard-arrow-left'; +import KeyboardArrowRight from '../svg-icons/keyboard-arrow-right'; +import { LinearProgress } from '../Progress'; + +export const styleSheet = createStyleSheet('MuiMobileStepper', theme => ({ + mobileStepper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + position: 'fixed', + bottom: 0, + left: 0, + zIndex: theme.zIndex.mobileStepper, + backgroundColor: theme.palette.background.paper, + padding: '6px', + }, + button: {}, + dots: { + display: 'flex', + flexDirection: 'row', + }, + dot: { + backgroundColor: theme.palette.action.disabled, + borderRadius: '50%', + width: '10px', + height: '10px', + margin: '0 2px', + }, + dotActive: { + backgroundColor: theme.palette.primary[500], + }, + progress: { + width: '50%', + }, +})); + +function MobileStepper(props) { + const { + activeStep, + buttonClassName: buttonClassNameProp, + classes, + className: classNameProp, + disableBack, + disableNext, + dotClassName: dotClassNameProp, + dotsClassName: dotsClassNameProp, + kind, + onBack, + onNext, + progressClassName: progressClassNameProp, + steps, + ...other + } = props; + + const className = classNames(classes.mobileStepper, classNameProp); + const dotsClassName = classNames(classes.dots, dotsClassNameProp); + const buttonClassName = classNames(classes.button, buttonClassNameProp); + const progressClassName = classNames(classes.progress, progressClassNameProp); + + return ( + + + {kind === 'dots' && +
+ {Array.from(Array(steps)).map((_, step) => { + const dotClassName = classNames( + { + [classes.dot]: true, + [classes.dotActive]: step === activeStep, + }, + dotClassNameProp, + ); + return
; // eslint-disable-line react/no-array-index-key,max-len + })} +
} + {kind === 'progress' && +
+ +
} + + + ); +} + +MobileStepper.propTypes = { + /** + * Set the active step (zero based index). This will enable `Step` control helpers. + */ + activeStep: PropTypes.number, + /** + * @ignore + */ + buttonClassName: PropTypes.string, + /** + * Useful to extend the style applied to components. + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * Set to true to disable the back button. + */ + disableBack: PropTypes.bool, + /** + * Set to true to disable the next button. + */ + disableNext: PropTypes.bool, + /** + * @ignore + */ + dotClassName: PropTypes.string, + /** + * @ignore + */ + dotsClassName: PropTypes.string, + /** + * The kind of mobile stepper to use. + */ + kind: PropTypes.oneOf(['text', 'dots', 'progress']), + /** + * Passed into the onTouchTap prop of the Back button. + */ + onBack: PropTypes.func.isRequired, + /** + * Passed into the onTouchTap prop of the Next button. + */ + onNext: PropTypes.func.isRequired, + /** + * @ignore + */ + progressClassName: PropTypes.string, + /** + * The total steps. + */ + steps: PropTypes.number.isRequired, +}; + +MobileStepper.defaultProps = { + activeStep: 0, + kind: 'dots', + disableBack: false, + disableNext: false, +}; + +export default withStyles(styleSheet)(MobileStepper); diff --git a/src/MobileStepper/MobileStepper.spec.js b/src/MobileStepper/MobileStepper.spec.js new file mode 100644 index 00000000000000..b5e896cb8e6bbf --- /dev/null +++ b/src/MobileStepper/MobileStepper.spec.js @@ -0,0 +1,138 @@ +// @flow + +import React from 'react'; +import { assert } from 'chai'; +import { createShallow } from '../test-utils'; +import MobileStepper, { styleSheet } from './MobileStepper'; +import Button from '../Button/Button'; +import KeyboardArrowLeft from '../svg-icons/keyboard-arrow-left'; +import KeyboardArrowRight from '../svg-icons/keyboard-arrow-right'; +import { LinearProgress } from '../Progress'; + +describe('', () => { + let shallow; + let classes; + const defaultProps = { + steps: 2, + onBack: () => {}, + onNext: () => {}, + }; + + before(() => { + shallow = createShallow({ dive: true }); + classes = shallow.context.styleManager.render(styleSheet); + }); + + it('should render a Paper component', () => { + const wrapper = shallow(); + assert.strictEqual(wrapper.name(), 'withStyles(Paper)'); + assert.strictEqual(wrapper.props().elevation, 0, 'should have no elevation'); + }); + it('should render with the mobileStepper class', () => { + const wrapper = shallow(); + assert.strictEqual( + wrapper.hasClass(classes.mobileStepper), + true, + 'should have the mobileStepper class', + ); + }); + it('should render the custom className and the mobileStepper class', () => { + const wrapper = shallow(); + assert.strictEqual(wrapper.is('.test-class-name'), true, 'should pass the test className'); + assert.strictEqual( + wrapper.hasClass(classes.mobileStepper), + true, + 'should have the mobileStepper class', + ); + }); + it('should render two buttons', () => { + const wrapper = shallow(); + assert.lengthOf(wrapper.find(Button), 2, 'should render two buttons'); + }); + it('should render a