diff --git a/src/components/Modal/InnerComponents/CustomModalDialog.js b/src/components/Modal/InnerComponents/CustomModalDialog.js new file mode 100644 index 00000000000..f797c1de344 --- /dev/null +++ b/src/components/Modal/InnerComponents/CustomModalDialog.js @@ -0,0 +1,85 @@ +/** + * CustomModalDialog creates custom ReactBootstrap ModalDialog + * https://github.com/react-bootstrap/react-bootstrap/blob/master/src/ModalDialog.js + * + * This extends ModalDialog and adds contentClassName prop for setting + * `modal-content` div's class + */ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { utils } from 'react-bootstrap'; + +const bsClass = utils.bootstrapUtils.bsClass; +const bsSizes = utils.bootstrapUtils.bsSizes; +const getClassSet = utils.bootstrapUtils.getClassSet; +const prefix = utils.bootstrapUtils.prefix; +const splitBsProps = utils.bootstrapUtils.splitBsProps; + +// React Bootstrap utils/StyleConfig Size is currently not exported +const Size = { + LARGE: 'large', + SMALL: 'small', +}; + +class CustomModalDialog extends React.Component { + render() { + const { + dialogClassName, + contentClassName, + className, + style, + children, + ...props + } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const bsClassName = prefix(bsProps); + + const modalStyle = { display: 'block', ...style }; + + const dialogClasses = { + ...getClassSet(bsProps), + [bsClassName]: false, + [prefix(bsProps, 'dialog')]: true, + }; + + return ( +
+
+
+ {children} +
+
+
+ ); + } +} + +CustomModalDialog.propTypes = { + /** A css class to apply to the Modal dialog DOM node. */ + dialogClassName: PropTypes.string, + /** custom modal-content class added to the content DOM node */ + contentClassName: PropTypes.string, + /** base modal class name */ + className: PropTypes.string, + /** additional modal styles */ + style: PropTypes.object, + /** Children nodes */ + children: PropTypes.node, +}; + +export default bsClass( + 'modal', + bsSizes([Size.LARGE, Size.SMALL], CustomModalDialog), +); diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js new file mode 100644 index 00000000000..6a560019f7c --- /dev/null +++ b/src/components/Modal/Modal.js @@ -0,0 +1,17 @@ +import CustomModalDialog from './InnerComponents/CustomModalDialog'; +import { Modal as BsModal } from 'react-bootstrap'; + +/** + * Modal Component for Patternfly React + */ +class Modal extends BsModal { + render() { + return super.render(); + } +} + +Modal.defaultProps = Object.assign(BsModal.defaultProps, { + dialogComponentClass: CustomModalDialog, +}); + +export default Modal; diff --git a/src/components/Modal/Modal.stories.js b/src/components/Modal/Modal.stories.js new file mode 100644 index 00000000000..79f7c75fb4e --- /dev/null +++ b/src/components/Modal/Modal.stories.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withInfo } from '@storybook/addon-info'; +import { defaultTemplate } from '../../../storybook/decorators/storyTemplates'; + +import { + MockModalManager, + basicExampleSource, +} from './__mocks__/mockModalManager'; + +import { + MockAboutModalManager, + aboutExampleSource, +} from './__mocks__/mockAboutModalManager'; + +const stories = storiesOf('Modal Overlay', module); + +const description = ( +

+ This component is based on React Bootstrap Modal component. See{' '} + + React Bootstrap Docs + {' '} + for complete Modal component documentation. +

+); + +stories.addDecorator( + defaultTemplate({ + title: 'Modal Overlay', + documentationLink: + 'http://www.patternfly.org/pattern-library/forms-and-controls/modal-overlay/', + description: description, + }), +); + +stories.add( + 'Basic example', + withInfo({ + source: false, + propTablesExclude: [MockModalManager], + text: ( +
+

Story Source

+
{basicExampleSource}
+
+ ), + })(() => ), +); + +stories.add( + 'About Modal', + withInfo({ + source: false, + propTablesExclude: [MockAboutModalManager], + text: ( +
+

Story Source

+
{aboutExampleSource}
+
+ ), + })(() => ), +); diff --git a/src/components/Modal/__mocks__/mockAboutModalManager.js b/src/components/Modal/__mocks__/mockAboutModalManager.js new file mode 100644 index 00000000000..859b9c14c3f --- /dev/null +++ b/src/components/Modal/__mocks__/mockAboutModalManager.js @@ -0,0 +1,131 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { Modal } from '../index'; +import logo from 'patternfly/dist/img/logo-alt.svg'; + +export class MockAboutModalManager extends React.Component { + constructor() { + super(); + this.state = { showModal: false }; + this.open = this.open.bind(this); + this.close = this.close.bind(this); + } + open() { + this.setState({ showModal: true }); + } + close() { + this.setState({ showModal: false }); + } + render() { + return ( +
+ + + + + + + +

Product Title

+
+
    +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
+
+
+ Trademark and Copyright Information +
+
+ + Patternfly Logo + +
+
+ ); + } +} + +export const aboutExampleSource = ` + + + + + + + +

Product Title

+
+
    +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
  • + Label Version +
  • +
+
+
+ Trademark and Copyright Information +
+
+ + Patternfly Logo + +
+`; diff --git a/src/components/Modal/__mocks__/mockModalManager.js b/src/components/Modal/__mocks__/mockModalManager.js new file mode 100644 index 00000000000..86775a91de4 --- /dev/null +++ b/src/components/Modal/__mocks__/mockModalManager.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { Modal } from '../index'; + +export class MockModalManager extends React.Component { + constructor() { + super(); + this.state = { showModal: false }; + this.open = this.open.bind(this); + this.close = this.close.bind(this); + } + open() { + this.setState({ showModal: true }); + } + close() { + this.setState({ showModal: false }); + } + render() { + return ( +
+ + + + + + Modal Overlay Title + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+
+ ); + } +} + +export const basicExampleSource = ` + + + + + + Modal Overlay Title + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+`; diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js new file mode 100644 index 00000000000..c6b35681c7e --- /dev/null +++ b/src/components/Modal/index.js @@ -0,0 +1 @@ +export { default as Modal } from './Modal'; diff --git a/src/components/Wizard/Wizard.js b/src/components/Wizard/Wizard.js new file mode 100644 index 00000000000..2b7268f4c3f --- /dev/null +++ b/src/components/Wizard/Wizard.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WizardHeader from './WizardHeader'; + +/** + * Wizard - main Wizard component. + */ +const Wizard = ({ children, className, embedded, ...rest }) => { + const renderChildren = () => { + return React.Children.map(children, child => { + if (child && child.type === WizardHeader) { + return React.cloneElement(child, { + embedded: embedded, + }); + } else { + return child; + } + }); + }; + + return ( +
+ {renderChildren()} +
+ ); +}; +Wizard.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Embedded wizard */ + embedded: PropTypes.bool, +}; +export default Wizard; diff --git a/src/components/Wizard/Wizard.stories.js b/src/components/Wizard/Wizard.stories.js new file mode 100644 index 00000000000..8d1bfba6474 --- /dev/null +++ b/src/components/Wizard/Wizard.stories.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withInfo } from '@storybook/addon-info'; +import { Row, Col } from 'react-bootstrap'; +import { withKnobs } from '@storybook/addon-knobs'; +import { defaultTemplate } from '../../../storybook/decorators/storyTemplates'; + +import { mockWizardItems } from './__mocks__/mockWizardItems'; + +import { + MockLoadingWizardManager, + mockLoadingWizardSource, +} from './__mocks__/mockLoadingWizardManager'; + +import { + MockModalWizardManager, + mockModalWizardSource, +} from './__mocks__/mockModalWizardManager'; + +// import { +// MockEmbeddedWizardManager, +// mockEmbeddedWizardSource, +// } from './__mocks__/mockEmbeddedWizardManager'; + +const stories = storiesOf('Wizard', module); +stories.addDecorator(withKnobs); +stories.addDecorator( + defaultTemplate({ + title: 'Wizard', + documentationLink: + 'http://www.patternfly.org/pattern-library/communication/wizard/#/overview', + }), +); + +stories.add( + 'Loading wizard', + withInfo({ + source: false, + propTablesExclude: [Row, Col, MockLoadingWizardManager], + text: ( +
+

Story Source

+
{mockLoadingWizardSource}
+
+ ), + })(() => ( + + + + + + )), +); + +stories.add( + 'Modal wizard', + withInfo({ + source: false, + propTablesExclude: [Row, Col, MockModalWizardManager], + text: ( +
+

Story Source

+
{mockModalWizardSource}
+
+ ), + })(() => ( + + + + + + )), +); + +/** + * Embedded Wizard will be finalized in a subsequent PR. + * + * Open PF Core issues: + * https://github.com/patternfly/patternfly/issues/869 + * https://github.com/patternfly/patternfly/issues/841 + */ +// stories.add( +// 'Embedded in page', +// withInfo({ +// source: false, +// propTablesExclude: [Row, Col, MockEmbeddedWizardManager], +// text: ( +//
+//

Story Source

+//
{mockEmbeddedWizardSource}
+//
+// ), +// })(() => ( +// +// +// +// +// +// )), +// ); diff --git a/src/components/Wizard/Wizard.test.js b/src/components/Wizard/Wizard.test.js new file mode 100644 index 00000000000..8179914ac60 --- /dev/null +++ b/src/components/Wizard/Wizard.test.js @@ -0,0 +1,99 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Row, Col } from 'react-bootstrap'; +import { Button } from '../Button'; +import { Wizard } from './index'; + +import { + mockWizardItems, + mockLoadingContents, +} from './__mocks__/mockWizardItems'; + +import { + renderWizardSteps, + renderSidebarItems, + renderWizardContents, +} from './__mocks__/mockWizardRenderers'; + +test('Wizard loading renders properly', () => { + const component = renderer.create( + + + + + + + {mockLoadingContents()} + + + + + + + + + + , + ); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test('Wizard embedded renders properly', () => { + const onStepClick = jest.fn(); + const onSidebarItemClick = jest.fn(); + const activeStepIndex = 0; + const activeSubStepIndex = 0; + const onNextButtonClick = jest.fn(); + const onBackButtonClick = jest.fn(); + + const component = renderer.create( + + + + + + + + {renderWizardContents( + mockWizardItems, + activeStepIndex, + activeSubStepIndex, + )} + + + + + + + + + + , + ); + + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); diff --git a/src/components/Wizard/WizardBody.js b/src/components/Wizard/WizardBody.js new file mode 100644 index 00000000000..abd4858d141 --- /dev/null +++ b/src/components/Wizard/WizardBody.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardBody component for Patternfly React + */ +const WizardBody = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-body', className); + return ( +
+ {children} +
+ ); +}; +WizardBody.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardBody; diff --git a/src/components/Wizard/WizardContents.js b/src/components/Wizard/WizardContents.js new file mode 100644 index 00000000000..a4155ade54a --- /dev/null +++ b/src/components/Wizard/WizardContents.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardContents component for Patternfly React + */ +const WizardContents = ({ + children, + className, + stepIndex, + subStepIndex, + activeStepIndex, + activeSubStepIndex, + ...rest +}) => { + const classes = cx( + 'wizard-pf-contents', + { + // hide contents if the step is not active + // OR if we have sub steps and this sub step is not active + hidden: + activeStepIndex !== stepIndex || + (activeSubStepIndex !== null && activeSubStepIndex !== subStepIndex), + }, + className, + ); + return ( +
+ {children} +
+ ); +}; +WizardContents.propTypes = { + /** WizardStep nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** The wizard step index for these contents */ + stepIndex: PropTypes.number, + /** The wizard sub step index for these contents */ + subStepIndex: PropTypes.number, + /** The active wizard step index */ + activeStepIndex: PropTypes.number, + /** The active wizard sub step index */ + activeSubStepIndex: PropTypes.number, +}; +export default WizardContents; diff --git a/src/components/Wizard/WizardFooter.js b/src/components/Wizard/WizardFooter.js new file mode 100644 index 00000000000..d7d3cbf240d --- /dev/null +++ b/src/components/Wizard/WizardFooter.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardFooter component for Patternfly React + */ +const WizardFooter = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-footer', className); + return ( +
+ {children} +
+ ); +}; +WizardFooter.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardFooter; diff --git a/src/components/Wizard/WizardHeader.js b/src/components/Wizard/WizardHeader.js new file mode 100644 index 00000000000..4ff4632a815 --- /dev/null +++ b/src/components/Wizard/WizardHeader.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardHeader component for Patternfly React + */ +const WizardHeader = ({ children, className, embedded, title, ...rest }) => { + const classes = cx({ 'wizard-pf-header': !embedded }, className); + + if (embedded) { + return ( +

+ {title} +

+ ); + } else { + return ( +
+

{title}

+
+ ); + } +}; +WizardHeader.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Embedded wizard */ + embedded: PropTypes.bool, + /** The wizard title */ + title: PropTypes.string, +}; +export default WizardHeader; diff --git a/src/components/Wizard/WizardMain.js b/src/components/Wizard/WizardMain.js new file mode 100644 index 00000000000..a7872506517 --- /dev/null +++ b/src/components/Wizard/WizardMain.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardMain component for Patternfly React + */ +const WizardMain = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-main', className); + return ( +
+ {children} +
+ ); +}; +WizardMain.propTypes = { + /** WizardStep nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardMain; diff --git a/src/components/Wizard/WizardReviewContent.js b/src/components/Wizard/WizardReviewContent.js new file mode 100644 index 00000000000..864a21d2f7f --- /dev/null +++ b/src/components/Wizard/WizardReviewContent.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardReviewContent component for Patternfly React + */ +const WizardReviewContent = ({ children, className, collapsed, ...rest }) => { + const classes = cx( + 'wizard-pf-review-content', + { collapse: collapsed }, + className, + ); + return ( +
+ {children} +
+ ); +}; +WizardReviewContent.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Step collapsed */ + collapsed: PropTypes.bool, +}; +export default WizardReviewContent; diff --git a/src/components/Wizard/WizardReviewItem.js b/src/components/Wizard/WizardReviewItem.js new file mode 100644 index 00000000000..2cb910e897a --- /dev/null +++ b/src/components/Wizard/WizardReviewItem.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardReviewItem component for Patternfly React + */ +const WizardReviewItem = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-review-item', className); + return ( +
+ {children} +
+ ); +}; +WizardReviewItem.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardReviewItem; diff --git a/src/components/Wizard/WizardReviewStep.js b/src/components/Wizard/WizardReviewStep.js new file mode 100644 index 00000000000..8e70282e6d9 --- /dev/null +++ b/src/components/Wizard/WizardReviewStep.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ListGroupItem } from '../ListGroup'; + +/** + * WizardReviewStep component for Patternfly React + */ +const WizardReviewStep = ({ children, onClick, title, collapsed, ...rest }) => { + return ( + + + {title} + + {children} + + ); +}; +WizardReviewStep.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Click handler */ + onClick: PropTypes.func, + /** Step title */ + title: PropTypes.string, + /** Step collapsed */ + collapsed: PropTypes.bool, +}; +export default WizardReviewStep; diff --git a/src/components/Wizard/WizardReviewSteps.js b/src/components/Wizard/WizardReviewSteps.js new file mode 100644 index 00000000000..2e0c540988a --- /dev/null +++ b/src/components/Wizard/WizardReviewSteps.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { ListGroup } from '../ListGroup'; + +/** + * WizardReviewSteps component for Patternfly React + */ +const WizardReviewSteps = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-review-steps', className); + return ( +
+ {children} +
+ ); +}; +WizardReviewSteps.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardReviewSteps; diff --git a/src/components/Wizard/WizardReviewSubStep.js b/src/components/Wizard/WizardReviewSubStep.js new file mode 100644 index 00000000000..2f012bd63b4 --- /dev/null +++ b/src/components/Wizard/WizardReviewSubStep.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ListGroupItem } from '../ListGroup'; + +/** + * WizardReviewSubStep component for Patternfly React + */ +const WizardReviewSubStep = ({ + children, + onClick, + label, + title, + collapsed, + ...rest +}) => { + return ( + + + {label} + {title} + + {children} + + ); +}; +WizardReviewSubStep.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Click handler */ + onClick: PropTypes.func, + /** Review step label */ + label: PropTypes.string, + /** Review step title */ + title: PropTypes.string, + /** Step collapsed */ + collapsed: PropTypes.bool, +}; +export default WizardReviewSubStep; diff --git a/src/components/Wizard/WizardReviewSubSteps.js b/src/components/Wizard/WizardReviewSubSteps.js new file mode 100644 index 00000000000..6f6e3804ff3 --- /dev/null +++ b/src/components/Wizard/WizardReviewSubSteps.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { ListGroup } from '../ListGroup'; + +/** + * WizardReviewSubSteps component for Patternfly React + */ +const WizardReviewSubSteps = ({ children, className, collapsed, ...rest }) => { + const classes = cx( + 'wizard-pf-review-substeps', + { collapse: collapsed }, + className, + ); + return ( +
+ {children} +
+ ); +}; +WizardReviewSubSteps.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Step collapsed */ + collapsed: PropTypes.bool, +}; +export default WizardReviewSubSteps; diff --git a/src/components/Wizard/WizardRow.js b/src/components/Wizard/WizardRow.js new file mode 100644 index 00000000000..b3629c8cdbc --- /dev/null +++ b/src/components/Wizard/WizardRow.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardRow component for Patternfly React + */ +const WizardRow = ({ children, className, ...rest }) => { + const classes = cx('wizard-pf-row', className); + return ( +
+ {children} +
+ ); +}; +WizardRow.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardRow; diff --git a/src/components/Wizard/WizardSidebar.js b/src/components/Wizard/WizardSidebar.js new file mode 100644 index 00000000000..aec1baae2ef --- /dev/null +++ b/src/components/Wizard/WizardSidebar.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardSidebar component for Patternfly React + */ +const WizardSidebar = ({ items, className, ...rest }) => { + const classes = cx('wizard-pf-sidebar', className); + return ( +
+ {items} +
+ ); +}; +WizardSidebar.propTypes = { + /** Wizard sidebar items */ + items: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardSidebar; diff --git a/src/components/Wizard/WizardSidebarGroup.js b/src/components/Wizard/WizardSidebarGroup.js new file mode 100644 index 00000000000..59c444636d0 --- /dev/null +++ b/src/components/Wizard/WizardSidebarGroup.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { ListGroup } from '../ListGroup'; + +/** + * WizardSidebarGroup component for Patternfly React + */ +const WizardSidebarGroup = ({ + children, + className, + step, + activeStep, + ...rest +}) => { + const classes = cx({ hidden: step !== activeStep }, className); + return ( + + {children} + + ); +}; +WizardSidebarGroup.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** The wizard step number for this step */ + step: PropTypes.string, + /** The active step */ + activeStep: PropTypes.string, +}; +export default WizardSidebarGroup; diff --git a/src/components/Wizard/WizardSidebarGroupItem.js b/src/components/Wizard/WizardSidebarGroupItem.js new file mode 100644 index 00000000000..0cdb8c1ad02 --- /dev/null +++ b/src/components/Wizard/WizardSidebarGroupItem.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { ListGroupItem } from '../ListGroup'; + +/** + * WizardSidebarGroupItem component for Patternfly React + */ +const WizardSidebarGroupItem = ({ + stepIndex, + subStepIndex, + className, + subStep, + label, + title, + activeSubStep, + onClick, + ...rest +}) => { + const classes = cx({ active: subStep === activeSubStep }, className); + return ( + + { + e.preventDefault(); + onClick(stepIndex, subStepIndex); + }} + > + {label} + {title} + + + ); +}; +WizardSidebarGroupItem.propTypes = { + /** The wizard parent step index */ + stepIndex: PropTypes.number, + /** The wizard sub step index */ + subStepIndex: PropTypes.number, + /** Additional css classes */ + className: PropTypes.string, + /** This wizard sub step name */ + subStep: PropTypes.string, + /** This wizard sub step label */ + label: PropTypes.string, + /** This wizard sub step title */ + title: PropTypes.string, + /** The currently active wizard substep */ + activeSubStep: PropTypes.string, + /** Sidebar group item click handler */ + onClick: PropTypes.func, +}; +export default WizardSidebarGroupItem; diff --git a/src/components/Wizard/WizardStep.js b/src/components/Wizard/WizardStep.js new file mode 100644 index 00000000000..92db57d1d80 --- /dev/null +++ b/src/components/Wizard/WizardStep.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardStep component for Patternfly React + */ +const WizardStep = ({ + children, + className, + stepIndex, + step, + label, + title, + activeStep, + onClick, + ...rest +}) => { + const classes = cx( + 'wizard-pf-step', + { active: step === activeStep }, + className, + ); + return ( +
  • + { + e.preventDefault(); + onClick(stepIndex); + }} + > + {label} + {title} + {children} + +
  • + ); +}; +WizardStep.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** The wizard step index */ + stepIndex: PropTypes.number, + /** The wizard step for this step */ + step: PropTypes.string, + /** The wizard step number label */ + label: PropTypes.string, + /** The wizard step title */ + title: PropTypes.string, + /** The active step */ + activeStep: PropTypes.string, + /** Step click handler */ + onClick: PropTypes.func, +}; +export default WizardStep; diff --git a/src/components/Wizard/WizardSteps.js b/src/components/Wizard/WizardSteps.js new file mode 100644 index 00000000000..33263eb7f4e --- /dev/null +++ b/src/components/Wizard/WizardSteps.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardSteps component for Patternfly React + */ +const WizardSteps = ({ steps, className, ...rest }) => { + const classes = cx('wizard-pf-steps', className); + return ( +
    +
      {steps}
    +
    + ); +}; +WizardSteps.propTypes = { + /** WizardStep nodes */ + steps: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, +}; +export default WizardSteps; diff --git a/src/components/Wizard/WizardSubStep.js b/src/components/Wizard/WizardSubStep.js new file mode 100644 index 00000000000..1822908d229 --- /dev/null +++ b/src/components/Wizard/WizardSubStep.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +/** + * WizardSubStep component for Patternfly React + */ +const WizardSubStep = ({ + className, + subStep, + title, + activeSubStep, + ...rest +}) => { + const classes = cx( + 'wizard-pf-step-title-substep', + { active: subStep === activeSubStep }, + className, + ); + return ( + + {title} + + ); +}; +WizardSubStep.propTypes = { + /** Additional css classes */ + className: PropTypes.string, + /** The wizard sub step for this step */ + subStep: PropTypes.string, + /** The wizard sub step title */ + title: PropTypes.string, + /** The active step */ + activeSubStep: PropTypes.string, +}; +export default WizardSubStep; diff --git a/src/components/Wizard/__mocks__/mockEmbeddedWizardManager.js b/src/components/Wizard/__mocks__/mockEmbeddedWizardManager.js new file mode 100644 index 00000000000..33cfa711a08 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockEmbeddedWizardManager.js @@ -0,0 +1,143 @@ +import React from 'react'; +import MockWizardBase from './mockWizardBase'; +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { Wizard } from '../index'; + +import { mockWizardItems } from './mockWizardItems'; + +import { + renderWizardSteps, + renderSidebarItems, + renderWizardContents, +} from './mockWizardRenderers'; + +export class MockEmbeddedWizardManager extends MockWizardBase { + render() { + const { activeStepIndex, activeSubStepIndex } = this.state; + + return ( + + + + + + + + {renderWizardContents( + mockWizardItems, + activeStepIndex, + activeSubStepIndex, + )} + + + + + + + {(activeStepIndex === 0 || activeStepIndex === 1) && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 0 && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 1 && ( + + )} + + + ); + } +} + +export const mockEmbeddedWizardSource = ` + + + + + + + + {renderWizardContents( + mockWizardItems, + activeStepIndex, + activeSubStepIndex, + )} + + + + + + + {(activeStepIndex === 0 || activeStepIndex === 1) && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 0 && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 1 && ( + + )} + + +`; diff --git a/src/components/Wizard/__mocks__/mockLoadingWizardManager.js b/src/components/Wizard/__mocks__/mockLoadingWizardManager.js new file mode 100644 index 00000000000..aaa9b68c200 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockLoadingWizardManager.js @@ -0,0 +1,121 @@ +import React from 'react'; +import { bindMethods } from '../../../common/helpers'; +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { Modal } from '../../Modal'; +import { Wizard } from '../index'; + +import { mockLoadingContents } from './mockWizardItems'; + +export class MockLoadingWizardManager extends React.Component { + constructor(props) { + super(props); + this.state = { showModal: false }; + bindMethods(this, ['open', 'close']); + } + open() { + this.setState({ showModal: true }); + } + close() { + this.setState({ showModal: false }); + } + render() { + const { showModal } = this.state; + + return ( +
    + + + + + + + Wizard Title + + + + {mockLoadingContents()} + + + + + + + + + +
    + ); + } +} + +export const mockLoadingWizardSource = ` +
    + + + + + + + Wizard Title + + + + {mockLoadingContents()} + + + + + + + + + +
    +`; diff --git a/src/components/Wizard/__mocks__/mockModalWizardManager.js b/src/components/Wizard/__mocks__/mockModalWizardManager.js new file mode 100644 index 00000000000..05ccd085921 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockModalWizardManager.js @@ -0,0 +1,207 @@ +import React from 'react'; +import MockWizardBase from './mockWizardBase'; +import { bindMethods } from '../../../common/helpers'; +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { Modal } from '../../Modal'; +import { Wizard } from '../index'; + +import { mockWizardItems } from './mockWizardItems'; + +import { + renderWizardSteps, + renderSidebarItems, + renderWizardContents, +} from './mockWizardRenderers'; + +export class MockModalWizardManager extends MockWizardBase { + constructor(props) { + super(props); + bindMethods(this, ['open', 'close']); + } + open() { + this.setState({ showModal: true }); + } + close() { + this.setState({ showModal: false }); + } + render() { + const { showModal, activeStepIndex, activeSubStepIndex } = this.state; + + return ( +
    + + + + + + + Wizard Title + + + + + + + {renderWizardContents( + mockWizardItems, + activeStepIndex, + activeSubStepIndex, + )} + + + + + + + {(activeStepIndex === 0 || activeStepIndex === 1) && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 0 && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 1 && ( + + )} + + + +
    + ); + } +} + +export const mockModalWizardSource = ` +
    + + + + + + + Wizard Title + + + + + + + {renderWizardContents( + mockWizardItems, + activeStepIndex, + activeSubStepIndex, + )} + + + + + + + {(activeStepIndex === 0 || activeStepIndex === 1) && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 0 && ( + + )} + {activeStepIndex === 2 && + activeSubStepIndex === 1 && ( + + )} + + + +
    +`; diff --git a/src/components/Wizard/__mocks__/mockWizardBase.js b/src/components/Wizard/__mocks__/mockWizardBase.js new file mode 100644 index 00000000000..437ff9d8ba8 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockWizardBase.js @@ -0,0 +1,78 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { bindMethods } from '../../../common/helpers'; + +class MockWizardBase extends React.Component { + constructor(props) { + super(props); + this.state = { + activeStepIndex: props.initialStepIndex || 0, + activeSubStepIndex: props.initialSubStepIndex || 0, + }; + bindMethods(this, [ + 'onSidebarItemClick', + 'onStepClick', + 'onNextButtonClick', + 'onBackButtonClick', + ]); + } + onSidebarItemClick(stepIndex, subStepIndex) { + this.setState({ + activeStepIndex: stepIndex, + activeSubStepIndex: subStepIndex, + }); + } + onStepClick(stepIndex) { + if (stepIndex === this.state.activeStepIndex) { + return; + } + this.setState({ + activeStepIndex: stepIndex, + activeSubStepIndex: 0, + }); + } + onNextButtonClick() { + const { steps } = this.props; + const { activeStepIndex, activeSubStepIndex } = this.state; + const activeStep = steps[activeStepIndex]; + + if (activeSubStepIndex < activeStep.subSteps.length - 1) { + this.setState(prevState => ({ + activeSubStepIndex: prevState.activeSubStepIndex + 1, + })); + } else if (activeStepIndex < steps.length - 1) { + this.setState(prevState => ({ + activeStepIndex: prevState.activeStepIndex + 1, + activeSubStepIndex: 0, + })); + } + } + onBackButtonClick() { + const { steps } = this.props; + const { activeStepIndex, activeSubStepIndex } = this.state; + + if (activeSubStepIndex > 0) { + this.setState(prevState => ({ + activeSubStepIndex: prevState.activeSubStepIndex - 1, + })); + } else if (activeStepIndex > 0) { + this.setState(prevState => ({ + activeStepIndex: prevState.activeStepIndex - 1, + activeSubStepIndex: + steps[prevState.activeStepIndex - 1].subSteps.length - 1, + })); + } + } + render() { + return false; + } +} +MockWizardBase.propTypes = { + /** Initial step index */ + initialStepIndex: PropTypes.number, + /** Initial sub step index */ + initialSubStepIndex: PropTypes.number, + /** Wizard steps */ + steps: PropTypes.array, +}; +export default MockWizardBase; diff --git a/src/components/Wizard/__mocks__/mockWizardDeployContents.js b/src/components/Wizard/__mocks__/mockWizardDeployContents.js new file mode 100644 index 00000000000..2537e8e0b07 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockWizardDeployContents.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class MockWizardDeployContents extends React.Component { + constructor() { + super(); + this.state = { deploying: true }; + } + componentWillReceiveProps(nextProps) { + const { active } = this.props; + if (!nextProps.active) { + this.setState({ deploying: true }); + } else if (!active && nextProps.active) { + setTimeout(() => { + this.setState({ deploying: false }); + }, 3000); + } + } + render() { + if (this.state.deploying) { + return ( +
    +
    +

    Deployment in progress

    +

    + Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi + vivamus, lorem sociosqu eget nunc amet.{' '} +

    +
    + ); + } else { + return ( +
    +
    + +
    +

    + Deployment was successful +

    +

    + Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi + vivamus, lorem sociosqu eget nunc amet.{' '} +

    + +
    + ); + } + } +} +MockWizardDeployContents.propTypes = { + active: PropTypes.bool, +}; +export default MockWizardDeployContents; diff --git a/src/components/Wizard/__mocks__/mockWizardItems.js b/src/components/Wizard/__mocks__/mockWizardItems.js new file mode 100644 index 00000000000..3463612ce82 --- /dev/null +++ b/src/components/Wizard/__mocks__/mockWizardItems.js @@ -0,0 +1,103 @@ +import React from 'react'; + +export const mockLoadingContents = () => { + return ( +
    +
    +

    Loading Wizard

    +

    + Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, + lorem sociosqu eget nunc amet.{' '} +

    +
    + ); +}; + +export const mockWizardItems = [ + { + step: '1', + label: '1', + title: 'First Step', + subSteps: [ + { + subStep: '1.1', + label: '1A.', + title: 'Details', + contents: { + label1: 'Name', + label2: 'Description', + }, + }, + { + subStep: '1.2', + label: '1B.', + title: 'Settings', + contents: { + label1: 'Lorem ipsum', + label2: 'Dolor', + }, + }, + ], + }, + { + step: '2', + label: '2', + title: 'Second Step', + subSteps: [ + { + subStep: '2.1', + label: '2A.', + title: 'Details', + contents: { + label1: 'Aliquam', + label2: 'Fermentum', + }, + }, + { + subStep: '2.2', + label: '2B.', + title: 'Settings', + contents: { + label1: 'Consectetur', + label2: 'Adipiscing', + }, + }, + ], + }, + { + step: '3', + label: '3', + title: 'Review', + subSteps: [ + { + subStep: '3.1', + label: '3A.', + title: 'Summary', + }, + { + subStep: '3.2', + label: '3B.', + title: 'Progress', + }, + ], + }, +]; + +export const mockWizardFormContents = (label1, label2) => { + return ( +
    +
    + +
    + +
    +
    +
    + +
    +