From d5e4b1993a0a26e84611ed764eb93ab08e64402f Mon Sep 17 00:00:00 2001 From: Alex Hayes Date: Sat, 3 Jun 2017 23:56:36 +1000 Subject: [PATCH 1/9] [Stepper] Mobile version - fixes #7033 --- .../MobileStepper/MobileStepper.md | 39 +++++ .../mobile-stepper/DotsMobileStepper.js | 61 +++++++ .../mobile-stepper/ProgressMobileStepper.js | 61 +++++++ .../mobile-stepper/TextMobileStepper.js | 78 +++++++++ .../mobile-stepper/mobile-stepper.md | 22 +++ .../getting-started/supported-components.md | 1 + src/MobileStepper/MobileStepper.js | 165 ++++++++++++++++++ src/MobileStepper/MobileStepper.spec.js | 138 +++++++++++++++ src/MobileStepper/index.js | 3 + src/styles/muiThemeProviderFactory.js | 2 + src/styles/zIndex.js | 1 + 11 files changed, 571 insertions(+) create mode 100644 docs/src/pages/component-api/MobileStepper/MobileStepper.md create mode 100644 docs/src/pages/component-demos/mobile-stepper/DotsMobileStepper.js create mode 100644 docs/src/pages/component-demos/mobile-stepper/ProgressMobileStepper.js create mode 100644 docs/src/pages/component-demos/mobile-stepper/TextMobileStepper.js create mode 100644 docs/src/pages/component-demos/mobile-stepper/mobile-stepper.md create mode 100644 src/MobileStepper/MobileStepper.js create mode 100644 src/MobileStepper/MobileStepper.spec.js create mode 100644 src/MobileStepper/index.js 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 {kind === 'dots' &&
@@ -94,7 +96,7 @@ function MobileStepper(props) { />
} ); @@ -105,6 +107,10 @@ MobileStepper.propTypes = { * Set the active step (zero based index). This will enable `Step` control helpers. */ activeStep: PropTypes.number, + /** + * Set the text that appears for the back button. + */ + backButtonText: PropTypes.string, /** * @ignore */ @@ -137,6 +143,10 @@ MobileStepper.propTypes = { * The kind of mobile stepper to use. */ kind: PropTypes.oneOf(['text', 'dots', 'progress']), + /** + * Set the text that appears for the next button. + */ + nextButtonText: PropTypes.string, /** * Passed into the onTouchTap prop of the Back button. */ @@ -160,6 +170,8 @@ MobileStepper.defaultProps = { kind: 'dots', disableBack: false, disableNext: false, + backButtonText: 'Back', + nextButtonText: 'Next', }; export default withStyles(styleSheet)(MobileStepper); diff --git a/src/MobileStepper/MobileStepper.spec.js b/src/MobileStepper/MobileStepper.spec.js index c86af38b88fe2a..20fbc7dcadc264 100644 --- a/src/MobileStepper/MobileStepper.spec.js +++ b/src/MobileStepper/MobileStepper.spec.js @@ -49,24 +49,38 @@ describe('', () => { const wrapper = shallow(); assert.lengthOf(wrapper.find(Button), 2, 'should render two buttons'); }); - it('should render a - {kind === 'dots' && -
+ {type === 'dots' && +
{Array.from(Array(steps)).map((_, step) => { const dotClassName = classNames( { [classes.dotActive]: step === activeStep, }, classes.dot, - dotClassNameProp, ); - return
; // eslint-disable-line react/no-array-index-key,max-len + // eslint-disable-next-line react/no-array-index-key + return
; })}
} - {kind === 'progress' && -
- + {type === 'progress' && +
+
} - @@ -126,11 +111,7 @@ MobileStepper.propTypes = { /** * Set the text that appears for the back button. */ - backButtonText: PropTypes.string, - /** - * @ignore - */ - buttonClassName: PropTypes.string, + backButtonText: PropTypes.node, /** * Useful to extend the style applied to components. */ @@ -147,22 +128,10 @@ MobileStepper.propTypes = { * 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']), /** * Set the text that appears for the next button. */ - nextButtonText: PropTypes.string, + nextButtonText: PropTypes.node, /** * Passed into the onTouchTap prop of the Back button. */ @@ -174,24 +143,25 @@ MobileStepper.propTypes = { /** * Set the text that appears for the next button. */ - position: PropTypes.oneOf(['bottom', 'top']), - /** - * @ignore - */ - progressClassName: PropTypes.string, + position: PropTypes.oneOf(['bottom', 'top', 'static']), /** * The total steps. */ steps: PropTypes.number.isRequired, + /** + * The type of mobile stepper to use. + */ + type: PropTypes.oneOf(['text', 'dots', 'progress']), }; MobileStepper.defaultProps = { activeStep: 0, - kind: 'dots', + backButtonText: 'Back', disableBack: false, disableNext: false, - backButtonText: 'Back', nextButtonText: 'Next', + position: 'bottom', + type: 'dots', }; export default withStyles(styleSheet)(MobileStepper); diff --git a/src/MobileStepper/MobileStepper.spec.js b/src/MobileStepper/MobileStepper.spec.js index 8f5b03797ad738..0dadff72c6bf60 100644 --- a/src/MobileStepper/MobileStepper.spec.js +++ b/src/MobileStepper/MobileStepper.spec.js @@ -28,31 +28,33 @@ describe('', () => { assert.strictEqual(wrapper.name(), 'withStyles(Paper)'); assert.strictEqual(wrapper.props().elevation, 0, 'should have no elevation'); }); + it('should render with the root class', () => { const wrapper = shallow(); assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the root class'); }); + 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, 'should have the mobileStepper class'); }); - it('should render with the fixedBottom class if position prop is set to bottom', () => { + + it('should render with the bottom class if position prop is set to bottom', () => { const wrapper = shallow(); - assert.strictEqual( - wrapper.hasClass(classes.fixedBottom), - true, - 'should have the fixedBottom class', - ); + assert.strictEqual(wrapper.hasClass(classes['position-bottom']), true); }); - it('should render with the fixedTop class if position prop is set to top', () => { + + it('should render with the top class if position prop is set to top', () => { const wrapper = shallow(); - assert.strictEqual(wrapper.hasClass(classes.fixedTop), true, 'should have the fixedTop class'); + assert.strictEqual(wrapper.hasClass(classes['position-top']), true); }); + it('should render two buttons', () => { const wrapper = shallow(); assert.lengthOf(wrapper.find(Button), 2, 'should render two buttons'); }); + it('should render the back button', () => { const wrapper = shallow(); const backButton = wrapper.childAt(0); @@ -63,6 +65,7 @@ describe('', () => { 'should render a single component', ); }); + it('should render next button', () => { const wrapper = shallow(); const nextButton = wrapper.childAt(2); @@ -73,38 +76,44 @@ describe('', () => { 'should render a single component', ); }); + it('should set the backButtonText', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.strictEqual( wrapper.childAt(0).childAt(1).text(), 'Past', 'should set the back button text', ); }); + it('should set the nextButtonText', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.strictEqual( wrapper.childAt(2).childAt(0).text(), 'Future', 'should set the back button text', ); }); + it('should disable the back button if prop disableBack is passed', () => { const wrapper = shallow(); const backButton = wrapper.childAt(0); - assert.strictEqual(backButton.prop('disabled'), true, 'should disable the back button'); + assert.strictEqual(backButton.props().disabled, true, 'should disable the back button'); }); + it('should disable the next button if prop disableNext is passed', () => { const wrapper = shallow(); const nextButton = wrapper.childAt(2); - assert.strictEqual(nextButton.prop('disabled'), true, 'should disable the next button'); + assert.strictEqual(nextButton.props().disabled, true, 'should disable the next button'); }); - it('should render just two buttons when supplied with kind text', () => { - const wrapper = shallow(); + + it('should render just two buttons when supplied with type text', () => { + const wrapper = shallow(); assert.lengthOf(wrapper.children(), 2, 'should render exactly two children'); }); - it('should render dots when supplied with kind dots', () => { - const wrapper = shallow(); + + it('should render dots when supplied with type dots', () => { + const wrapper = shallow(); assert.lengthOf(wrapper.children(), 3, 'should render exactly three children'); assert.strictEqual( wrapper.childAt(1).hasClass(classes.dots), @@ -112,44 +121,49 @@ describe('', () => { 'should have a single dots class', ); }); - it('should render a dot for each step when using dots kind', () => { - const wrapper = shallow(); + + it('should render a dot for each step when using dots type', () => { + const wrapper = shallow(); assert.lengthOf(wrapper.find(`.${classes.dot}`), 2, 'should render exactly two dots'); }); + it('should render the first dot as active if activeStep is not set', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.strictEqual( wrapper.childAt(1).childAt(0).hasClass(classes.dotActive), true, 'should render the first dot active', ); }); + it('should honour the activeStep prop', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.strictEqual( wrapper.childAt(1).childAt(1).hasClass(classes.dotActive), true, 'should render the second dot active', ); }); - it('should render a when supplied with kind progress', () => { - const wrapper = shallow(); + + it('should render a when supplied with type progress', () => { + const wrapper = shallow(); assert.lengthOf(wrapper.find(LinearProgress), 1, 'should render a '); }); + it('should calculate the value correctly', () => { const props = { onBack: defaultProps.onBack, onNext: defaultProps.onNext, }; - let wrapper = shallow(); + let wrapper = shallow(); let linearProgressProps = wrapper.find(LinearProgress).props(); assert.strictEqual(linearProgressProps.value, 0, 'should set value to 0'); - wrapper = shallow(); + wrapper = shallow(); linearProgressProps = wrapper.find(LinearProgress).props(); assert.strictEqual(linearProgressProps.value, 50, 'should set value to 50'); - wrapper = shallow(); + wrapper = shallow(); linearProgressProps = wrapper.find(LinearProgress).props(); assert.strictEqual( linearProgressProps.value,