From 00ebaddbc9f9792a8be99c4695bc66a263f6780e Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Tue, 12 Mar 2019 00:42:13 -0400 Subject: [PATCH 1/8] feat(pf4): wizard --- package.json | 3 +- .../src/components/Wizard/Wizard.docs.js | 16 + .../src/components/Wizard/Wizard.test.tsx | 40 ++ .../src/components/Wizard/Wizard.tsx | 352 ++++++++++ .../src/components/Wizard/WizardBody.tsx | 15 + .../src/components/Wizard/WizardHeader.tsx | 27 + .../src/components/Wizard/WizardNav.tsx | 32 + .../src/components/Wizard/WizardNavItem.tsx | 29 + .../src/components/Wizard/WizardToggle.tsx | 74 +++ .../Wizard/__snapshots__/Wizard.test.tsx.snap | 601 ++++++++++++++++++ .../Wizard/examples/SimpleWizard.js | 68 ++ .../Wizard/examples/Steps/SampleFormOne.js | 42 ++ .../Wizard/examples/Steps/SampleFormTwo.js | 42 ++ .../Wizard/examples/ValidationWizard.js | 134 ++++ .../react-core/src/components/Wizard/index.ts | 1 + .../react-core/src/components/index.ts | 1 + packages/patternfly-4/react-core/tslint.json | 68 +- .../patternfly-4/react-docs/gatsby-node.js | 5 +- .../components/componentDocs/componentDocs.js | 28 +- .../components/propsTableTs/propsTableTs.js | 54 +- .../demo-app-ts/package.json | 6 +- .../react-integration/demo-app-ts/src/App.tsx | 2 + .../demo-app-ts/src/Wizard.tsx | 39 ++ .../patternfly-4-component/index.js | 2 +- .../templates/component/component.tsx.hbs | 2 +- 25 files changed, 1629 insertions(+), 54 deletions(-) create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/Wizard.docs.js create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/Wizard.test.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/Wizard.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/WizardBody.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/WizardHeader.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/WizardNav.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/WizardNavItem.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/__snapshots__/Wizard.test.tsx.snap create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/examples/SimpleWizard.js create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormOne.js create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormTwo.js create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/examples/ValidationWizard.js create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/index.ts create mode 100644 packages/patternfly-4/react-integration/demo-app-ts/src/Wizard.tsx diff --git a/package.json b/package.json index 234d8dbfc68..dc2b6f18af6 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,7 @@ }, "scripts": { "bootstrap": "lerna bootstrap", - "build": "yarn prebuild:pf4 && lerna run --parallel build", - "prebuild:pf4": "lerna run --scope='@patternfly/react-styles' build", + "build": "lerna run build", "build:docs": "yarn build && lerna run docbuild", "build:prdocs": "lerna run pr-build --stream", "build:storybook": "build-storybook -c storybook -o .out", diff --git a/packages/patternfly-4/react-core/src/components/Wizard/Wizard.docs.js b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.docs.js new file mode 100644 index 00000000000..97cf464a85b --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.docs.js @@ -0,0 +1,16 @@ +import { Wizard } from '@patternfly/react-core'; +import SimpleWizard from './examples/SimpleWizard'; +import ValidationWizard from './examples/ValidationWizard'; +import SampleFormOne from './examples/Steps/SampleFormOne'; +import SampleFormTwo from './examples/Steps/SampleFormTwo'; + +export default { + title: 'Wizard', + components: { + Wizard + }, + examples: [ + { component: SimpleWizard, title: 'Simple Wizard' }, + { component: ValidationWizard, title: 'Validation Wizard', liveScope: { SampleFormOne, SampleFormTwo } } + ] +}; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/Wizard.test.tsx b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.test.tsx new file mode 100644 index 00000000000..1ef72511c27 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.test.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import Wizard from './Wizard'; +import { Step } from './Wizard'; + +test('Wizard should match snapshot', () => { + const steps: Step[] = [ + { name: 'A', component:

Step 1

}, + { + name: 'B', + steps: [ + { + name: 'B-1', + component:

Step 2

, + enableNext: true + }, + { + name: 'B-2', + component:

Step 3

, + enableNext: false + } + ] + }, + { name: 'C', component:

Step 4

}, + { name: 'D', component:

Step 5

} + ]; + const view = mount( + + ); + // ran into: https://github.com/airbnb/enzyme/issues/1213 + // so instead of: expect(view).toMatchSnapshot(); + const fragment = view.instance().render(); + expect(mount(
{fragment}
).getElement()).toMatchSnapshot() +}); \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/Wizard.tsx b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.tsx new file mode 100644 index 00000000000..919d478b468 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.tsx @@ -0,0 +1,352 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; +import { Backdrop } from '../Backdrop'; +import { Bullseye } from '../../layouts/Bullseye'; +import { BackgroundImage, BackgroundImageSrc } from '../BackgroundImage'; +import { Button } from '../Button'; +import WizardHeader from './WizardHeader'; +import WizardToggle from './WizardToggle'; +import WizardBody from './WizardBody'; +import WizardNav from './WizardNav'; +import WizardNavItem from './WizardNavItem'; +import * as ReactDOM from 'react-dom'; +import { canUseDOM } from 'exenv'; +import { KEY_CODES } from '../../helpers/constants'; +import { BackgroundImageSrcMap } from '../BackgroundImage'; +// because of the way this module is exported, cannot use regular import syntax +// tslint:disable-next-line +const FocusTrap: any = require('focus-trap-react'); + +export interface Step { + /** Optional identifier */ + id?: string | number; + /** The name of the step */ + name: string; + /** The component to render in the main body */ + component?: any; + /** The condition needed to enable the Next button */ + enableNext?: boolean; + /** True to hide the Cancel button */ + hideCancelButton?: boolean; + /** True to hide the Back button */ + hideBackButton?: boolean; + /** Sub steps */ + steps?: any[]; +} + +interface ComputedStep extends Step { + /** The condition needed to be able to navigate to this step */ + canJumpTo?: boolean; +}; + +interface WizardProps { + /** True to show the wizard */ + isOpen?: boolean; + /** The wizard title */ + title: string; + /** The wizard description */ + description?: string; + /** Mapping of image sizes to image paths */ + backgroundImgSrc?: string | BackgroundImageSrcMap; + /** Calback function to close the wizard */ + onClose?(): any; + /** Callback function to save at the end of the wizard, if not specified uses onClose */ + onSave?(): any; + /** Callback function after Next button is clicked */ + onNext?(step: any): { id?: string | number; name: string; }; + /** Callback function after Back button is clicked */ + onBack?(step: any): { id?: string | number; name: string; }; + /** Calback function when a step in the nav is clicked */ + onGoToStep?(step: any): { id?: string | number; name: string; }; + /** Additional classes spread to the Wizard */ + className?: string; + /** The wizard steps configuration object */ + steps: Step[]; + /** The step to start the wizard at (1 or higher) */ + startAtStep?: number; + /** The Next button text */ + nextButtonText?: string; + /** The Back button text */ + backButtonText?: string; + /** The Cancel button text */ + cancelButtonText?: string; + /** The text for the Next button on the last step */ + lastStepButtonText?: string; + /** Alignment of the footer items */ + footerRightAlign?: boolean; + /** aria-label for the close button */ + ariaLabelCloseButton?: string; + /** aria-label for the Nav */ + ariaLabelNav?: string; +} + +const images = { + [BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg', + [BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg', + [BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg', + [BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg', + [BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg', + [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg#image_overlay' +}; + +class Wizard extends React.Component { + public static defaultProps = { + isOpen: false, + description: '', + backgroundImgSrc: images, + onClose: null, + onSave: null, + onBack: null, + onNext: null, + onGoToStep: null, + className: '', + startAtStep: 1, + nextButtonText: 'Next', + backButtonText: 'Back', + cancelButtonText: 'Cancel', + lastStepButtonText: 'Save', + footerRightAlign: false, + ariaLabelCloseButton: 'Close', + ariaLabelNav: 'Steps' + }; + + public state = { + currentStep: Number.isInteger(this.props.startAtStep) ? this.props.startAtStep : 1 + }; + + public container = null; + + private handleKeyClicks = event => { + if (event.keyCode === KEY_CODES.ESCAPE_KEY && this.props.isOpen) { + this.props.onClose(); + } + }; + + public componentDidMount() { + document.body.appendChild(this.container); + document.addEventListener('keydown', this.handleKeyClicks, false); + } + + public componentWillUnmount() { + document.body.removeChild(this.container); + document.removeEventListener('keydown', this.handleKeyClicks, false); + } + + private onNext = () => { + const { onNext, onClose, onSave } = this.props; + const { currentStep } = this.state; + const flattenedSteps = this.getFlattenedSteps(); + const maxSteps = flattenedSteps.length; + if (currentStep >= maxSteps) { + // Hit the save button at the end of the wizard + if (onSave) { + return onSave(); + } + return onClose(); + } else { + const newStep = currentStep + 1; + this.setState({ + currentStep: newStep + }); + const { id, name } = flattenedSteps[newStep - 1]; + return onNext && onNext({ id, name }); + } + }; + + private onBack = () => { + const { onBack } = this.props; + const { currentStep } = this.state; + const flattenedSteps = this.getFlattenedSteps(); + const newStep = currentStep - 1 <= 0 ? 0 : currentStep - 1; + this.setState({ + currentStep: newStep + }); + const { id, name } = flattenedSteps[newStep - 1]; + return onBack && onBack({ id, name }); + }; + + private goToStep = step => { + const { onGoToStep } = this.props; + let newStep; + const flattenedSteps = this.getFlattenedSteps(); + const maxSteps = flattenedSteps.length; + if (step < 1) { + newStep = 1; + } else if (newStep > maxSteps) { + newStep = maxSteps; + } else { + newStep = step; + } + this.setState({ + currentStep: newStep + }); + const { id, name } = flattenedSteps[newStep - 1]; + return onGoToStep && onGoToStep({ id, name }); + }; + + private getFlattenedSteps = () => { + const { steps } = this.props; + const flattenedSteps = []; + for (const step of steps) { + if (step.steps) { + for (const childStep of step.steps) { + flattenedSteps.push(childStep); + } + } else { + flattenedSteps.push(step); + } + } + return flattenedSteps; + }; + + private getFlattenedStepsIndex = stepName => { + const flattenedSteps = this.getFlattenedSteps(); + for (let i = 0; i < flattenedSteps.length; i++) { + if (flattenedSteps[i].name === stepName) { + return i + 1; + } + } + } + + private initSteps = (steps, activeStep) => { + // Set canJumpTo on all steps leading up to and including the active step + const computedSteps: ComputedStep[] = steps; + for (const step of computedSteps) { + let found = false; + if (step.steps) { + for (const subStep of step.steps) { + if (activeStep === subStep) { + // one of the children matches + subStep.canJumpTo = true; + found = true; + break; + } else { + subStep.canJumpTo = true; + } + } + } + if (found) { + break; + } + if (activeStep === step) { + step.canJumpTo = true; + break; + } else { + step.canJumpTo = true; + } + } + return computedSteps; + }; + + public render() { + if (!canUseDOM) { + return null; + } + if (!this.container) { + this.container = document.createElement('div'); + } + const { + isOpen, + title, + description, + backgroundImgSrc = images, + onClose, + onSave, + onBack, + onNext, + onGoToStep, + className, + steps, + startAtStep, + nextButtonText, + backButtonText, + cancelButtonText, + lastStepButtonText, + footerRightAlign, + ariaLabelCloseButton, + ariaLabelNav, + ...rest + } = this.props; + const { currentStep } = this.state; + const flattenedSteps = this.getFlattenedSteps(); + const activeStep = flattenedSteps[currentStep - 1]; + const computedSteps: ComputedStep[] = this.initSteps(steps, activeStep); + const firstStep = activeStep === flattenedSteps[0]; + const lastStep = activeStep === flattenedSteps[flattenedSteps.length - 1]; + const isValid = activeStep.enableNext !== undefined ? activeStep.enableNext : true; + + const nav = isNavOpen => ( + + {computedSteps.map((step, index) => { + let enabled; + let navItemStep; + if (step.steps) { + let hasActiveChild = false; + let canJumpToParent = false; + for (const subStep of step.steps) { + if (activeStep === subStep) { + // one of the children matches + hasActiveChild = true; + } + if (subStep.canJumpTo) { + canJumpToParent = true; + } + } + navItemStep = this.getFlattenedStepsIndex(step.steps[0].name); + return ( + + + {step.steps.map((childStep, indexChild) => { + navItemStep = this.getFlattenedStepsIndex(childStep.name); + enabled = Boolean(childStep.canJumpTo); + return + })} + + + ); + } + navItemStep = this.getFlattenedStepsIndex(step.name); + enabled = Boolean(step.canJumpTo); + return ; + })} + + ); + + return ( + isOpen && ReactDOM.createPortal( + + + +
+ + + +
+
+ {nav(false)} + {activeStep.component} +
+
+ + {!firstStep && !activeStep.hideBackButton && } + {!activeStep.hideCancelButton && } +
+
+
+
+
+
, + this.container + ) + ); + } +} + +export default Wizard; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardBody.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardBody.tsx new file mode 100644 index 00000000000..298a22eaba2 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardBody.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; + +interface WizardBodyProps { + children?: any; +} + +const WizardBody: React.SFC = ({ children }) => ( +
+ {children} +
+); + +export default WizardBody; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardHeader.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardHeader.tsx new file mode 100644 index 00000000000..188f904835b --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardHeader.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; +import { Button } from '../Button'; +import { Title } from '../Title'; +import { TimesIcon } from '@patternfly/react-icons'; + +interface WizardHeaderProps { + onClose?(): any; + title: string; + description: string; + ariaLabel: string; +} + +const WizardHeader: React.SFC = ({ onClose = null, title, description, ariaLabel }) => ( +
+ + {title} + {description &&

+ {description} +

} +
+); + +export default WizardHeader; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardNav.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardNav.tsx new file mode 100644 index 00000000000..1612e037c71 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardNav.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; + +interface WizardNavProps { + children?: any; + ariaLabel?: string; + isOpen?: boolean; + returnList?: boolean; +} + +const WizardNav: React.SFC = ({ children, ariaLabel, isOpen = false, returnList = false }) => { + const innerList = ( +
    + {children} +
+ ); + + if (returnList) { + return innerList; + } + + return ( + + ); +}; + +export default WizardNav; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardNavItem.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardNavItem.tsx new file mode 100644 index 00000000000..190be4834f8 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardNavItem.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; + +interface WizardNavItemProps { + children?: any; + label?: string; + current?: boolean; + disabled?: boolean; + onNavItemClick(step: number): any; + step: number; + hasChildren?: boolean; +} + +const WizardNavItem: React.SFC = ({ children = null, label = '', current = null, disabled = null, step, onNavItemClick, hasChildren = null }) => ( +
  • + onNavItemClick(step)} + className={css(styles.wizardNavLink, current && 'pf-m-current', disabled && 'pf-m-disabled')} + aria-disabled={disabled ? 'true' : null} + tabIndex={disabled ? -1 : null}> + {label} + + {children} +
  • +); + +export default WizardNavItem; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx new file mode 100644 index 00000000000..82f7d6c7e93 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; +import { AngleRightIcon, CaretDownIcon } from '@patternfly/react-icons'; +import { Step } from './Wizard'; + +interface WizardToggleProps { + nav: any; + steps: Step[]; + activeStep: Step; +} + +class WizardToggle extends React.Component { + public state = { + isOpen: false + }; + + private toggleNav = () => { + this.setState({ + isOpen: !this.state.isOpen + }) + } + + public render() { + const { isOpen } = this.state; + const { nav, steps, activeStep } = this.props; + let activeStepIndex; + let activeStepName; + let activeStepSubName; + for (let i = 0; i < steps.length; i++) { + if (steps[i].steps) { + for (let j = 0; j < steps[i].steps.length; j++) { + if (steps[i].steps[j] === activeStep) { + activeStepIndex = i + 1; + activeStepName = steps[i].name; + activeStepSubName = steps[i].steps[j].name; + break; + } + } + } + if (steps[i] === activeStep) { + activeStepIndex = i + 1; + activeStepName = steps[i].name; + break; + } + } + return ( + <> + + {isOpen && ( +
    +
    + {nav(isOpen)} +
    +
    + )} + + ); + } + +} + +export default WizardToggle; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/Wizard/__snapshots__/Wizard.test.tsx.snap b/packages/patternfly-4/react-core/src/components/Wizard/__snapshots__/Wizard.test.tsx.snap new file mode 100644 index 00000000000..122da4eeea6 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/__snapshots__/Wizard.test.tsx.snap @@ -0,0 +1,601 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wizard should match snapshot 1`] = ` +
    + Object { + "$$typeof": Symbol(react.portal), + "children": + + +
    + + + + Step 1 +

    , + "name": "A", + } + } + nav={[Function]} + steps={ + Array [ + Object { + "canJumpTo": true, + "component":

    + Step 1 +

    , + "name": "A", + }, + Object { + "name": "B", + "steps": Array [ + Object { + "component":

    + Step 2 +

    , + "enableNext": true, + "name": "B-1", + }, + Object { + "component":

    + Step 3 +

    , + "enableNext": false, + "name": "B-2", + }, + ], + }, + Object { + "component":

    + Step 4 +

    , + "name": "C", + }, + Object { + "component":

    + Step 5 +

    , + "name": "D", + }, + ] + } + /> +
    +
    + + + + + + + + + + + + +

    + Step 1 +

    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    , + "containerInfo":
    +
    +
    +
    +
    +
    +
    + +

    + Wizard title +

    +

    + Description here +

    +
    + +
    +
    + +
    +

    + Step 1 +

    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +

    + Wizard title +

    +

    + Description here +

    +
    + +
    +
    + +
    +

    + Step 1 +

    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    , + "implementation": null, + "key": null, + } +
    +`; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/examples/SimpleWizard.js b/packages/patternfly-4/react-core/src/components/Wizard/examples/SimpleWizard.js new file mode 100644 index 00000000000..db3221671f8 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/examples/SimpleWizard.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { Button, Wizard, BackgroundImageSrc } from '@patternfly/react-core'; + +class SimpleWizard extends React.Component { + state = { + isOpen: false + }; + + toggleOpen = () => { + this.setState(({ isOpen }) => ({ + isOpen: !isOpen + })); + }; + + render() { + const { isOpen } = this.state; + + const images = { + [BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg', + [BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg', + [BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg', + [BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg', + [BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg', + [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg#image_overlay' + }; + + const steps = [ + { name: 'Step 1', component:

    Step 1

    }, + { + name: 'Step 2', + steps: [ + { name: 'Step 2 - A', component:

    Step 2 - A

    }, + { name: 'Step 2 - B', component:

    Step 2 - B

    } + ] + }, + { name: 'Step 3', component:

    Step 3

    }, + { + name: 'Step 4', + steps: [ + { name: 'Step 4 - A', component:

    Step 4 - A

    }, + { name: 'Step 4 - B', component:

    Step 4 - B

    } + ] + }, + { name: 'Final Step', component:

    Final Step

    , hideCancelButton: true, hideBackButton: true } + ]; + + return ( + + + {isOpen && ( + + )} + + ); + } +} + +export default SimpleWizard; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormOne.js b/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormOne.js new file mode 100644 index 00000000000..6aa66ce6ae4 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormOne.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +class SampleFormOne extends React.Component { + state = { + value: this.props.formValue, + isValid: this.props.isFormValid + }; + + handleTextInputChange = value => { + const isValid = /^\d+$/.test(value); + this.setState({ value, isValid }); + this.props.onChange(isValid, value); + }; + + render() { + const { value, isValid } = this.state; + + return ( +
    + + + +
    + ); + } +} + +export default SampleFormOne; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormTwo.js b/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormTwo.js new file mode 100644 index 00000000000..a048070a6ea --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/examples/Steps/SampleFormTwo.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +class SampleFormTwo extends React.Component { + state = { + value: this.props.formValue, + isValid: this.props.isFormValid + }; + + handleTextInputChange = value => { + const isValid = /^\d+$/.test(value); + this.setState({ value, isValid }); + this.props.onChange(isValid, value); + }; + + render() { + const { value, isValid } = this.state; + + return ( +
    + + + +
    + ); + } +} + +export default SampleFormTwo; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/examples/ValidationWizard.js b/packages/patternfly-4/react-core/src/components/Wizard/examples/ValidationWizard.js new file mode 100644 index 00000000000..45af78b62dd --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/examples/ValidationWizard.js @@ -0,0 +1,134 @@ +import React from 'react'; +import { Button, Wizard, BackgroundImageSrc } from '@patternfly/react-core'; +import SampleFormOne from './Steps/SampleFormOne'; +import SampleFormTwo from './Steps/SampleFormTwo'; + +class ValidationWizard extends React.Component { + state = { + isOpen: false, + isFormValidA: false, + formValueA: 'Five', + isFormValidB: false, + formValueB: 'Six', + allStepsValid: false + }; + + toggleOpen = () => { + this.setState(({ isOpen }) => ({ + isOpen: !isOpen + })); + }; + + onFormChangeA = (isValid, value) => { + this.setState( + { + isFormValidA: isValid, + formValueA: value + }, + this.areAllStepsValid + ); + }; + + onFormChangeB = (isValid, value) => { + this.setState( + { + isFormValidB: isValid, + formValueB: value + }, + this.areAllStepsValid + ); + }; + + areAllStepsValid = () => { + this.setState({ + allStepsValid: this.state.isFormValidA && this.state.isFormValidB + }); + }; + + onNext = ({ id, name }) => { + console.log(`id: ${id}, name: ${name}`); + this.areAllStepsValid(); + }; + + onBack = ({ id, name }) => { + console.log(`id: ${id}, name: ${name}`); + this.areAllStepsValid(); + }; + + onGoToStep = ({ id, name }) => { + console.log(`id: ${id}, name: ${name}`); + }; + + onSave = () => { + console.log('Saved and closed the wizard'); + this.setState({ + isOpen: false + }); + }; + + render() { + const { isOpen, isFormValidA, isFormValidB, formValueA, formValueB, allStepsValid } = this.state; + + const images = { + [BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg', + [BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg', + [BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg', + [BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg', + [BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg', + [BackgroundImageSrc.filter]: '/assets/images/background-filter.svg#image_overlay' + }; + + const steps = [ + { id: 1, name: 'Information', component:

    Step 1

    }, + { + name: 'Configuration', + steps: [ + { + id: 2, + name: 'Substep A with validation', + component: ( + + ), + enableNext: isFormValidA + }, + { + id: 3, + name: 'Substep B with validation', + component: ( + + ), + enableNext: isFormValidB + }, + { id: 4, name: 'Substep C', component:

    Substep C

    } + ] + }, + { id: 5, name: 'Additional', component:

    Step 3

    , enableNext: allStepsValid }, + { id: 6, name: 'Review', component:

    Step 4

    } + ]; + + return ( + + + {isOpen && ( + + )} + + ); + } +} + +export default ValidationWizard; diff --git a/packages/patternfly-4/react-core/src/components/Wizard/index.ts b/packages/patternfly-4/react-core/src/components/Wizard/index.ts new file mode 100644 index 00000000000..f07d6622b01 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/index.ts @@ -0,0 +1 @@ +export { default as Wizard } from './Wizard'; diff --git a/packages/patternfly-4/react-core/src/components/index.ts b/packages/patternfly-4/react-core/src/components/index.ts index b548d065782..27209051b7a 100644 --- a/packages/patternfly-4/react-core/src/components/index.ts +++ b/packages/patternfly-4/react-core/src/components/index.ts @@ -36,3 +36,4 @@ export * from './TextArea'; export * from './TextInput'; export * from './Title'; export * from './Tooltip'; +export * from './Wizard'; diff --git a/packages/patternfly-4/react-core/tslint.json b/packages/patternfly-4/react-core/tslint.json index 8809d4ae45e..3ab4ab38695 100644 --- a/packages/patternfly-4/react-core/tslint.json +++ b/packages/patternfly-4/react-core/tslint.json @@ -1,29 +1,43 @@ { - "defaultSeverity": "error", - "extends": [ - "tslint:recommended", - "tslint-react", - "tslint-config-prettier" + "defaultSeverity": "error", + "extends": [ + "tslint:recommended", + "tslint-react", + "tslint-config-prettier" + ], + "jsRules": { + "object-literal-sort-keys": false + }, + "rules": { + "member-ordering": false, + "array-type": [ + true, + "array" ], - "jsRules": { - "object-literal-sort-keys": false - }, - "rules": { - "member-ordering": false, - "array-type": [true, "array"], - "interface-name": [true, "never-prefix"], - "jsx-boolean-value": [true, "never"], - "jsx-no-lambda": false, - "jsx-self-close": true, - "jsx-wrap-multiline": true, - "object-literal-sort-keys": false, - "quotemark": [true, "single", "jsx-double"], - "variable-name": false - }, - "rulesDirectory": [], - "linterOptions": { - "exclude": [ - "**/*.js" - ] - } -} \ No newline at end of file + "interface-name": [ + true, + "never-prefix" + ], + "jsx-boolean-value": [ + true, + "never" + ], + "jsx-no-lambda": false, + "jsx-self-close": true, + "jsx-wrap-multiline": true, + "object-literal-sort-keys": false, + "quotemark": [ + true, + "single", + "jsx-double" + ], + "variable-name": false, + "ordered-imports": false + }, + "rulesDirectory": [], + "linterOptions": { + "exclude": [ + "**/*.js" + ] + } +} \ No newline at end of file diff --git a/packages/patternfly-4/react-docs/gatsby-node.js b/packages/patternfly-4/react-docs/gatsby-node.js index 5a28e70f367..360df818965 100644 --- a/packages/patternfly-4/react-docs/gatsby-node.js +++ b/packages/patternfly-4/react-docs/gatsby-node.js @@ -93,7 +93,10 @@ exports.createPages = async ({ graphql, actions }) => { } } examples: allFile( - filter: { sourceInstanceName: { eq: "components" }, relativePath: { glob: "**/examples/!(*.styles).js" } } + filter: { + sourceInstanceName: { eq: "components" } + relativePath: { glob: "**/examples/!(*.styles).+(js|tsx)" } + } ) { edges { node { diff --git a/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js b/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js index 9ef1bef441b..9da785279f9 100644 --- a/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js +++ b/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js @@ -96,7 +96,9 @@ class ComponentDocs extends React.PureComponent { ); })} - {Object.entries(components).map(([componentName]) => { + {Object.entries(components).map(component => { + const componentName = component[0]; + const componentFunction = component[1]; // Only generate docs for props for javascript code. const componentDocsJs = getDocGenInfo(componentName); if (componentDocsJs) { @@ -114,7 +116,14 @@ class ComponentDocs extends React.PureComponent { if (!componentDocsJs) { componentDocsTs = getDocGenInfoTs(componentName); if (componentDocsTs) { - return ; + return ( + + ); } } return null; @@ -171,6 +180,7 @@ export default props => ( name children { name + kindString comment { shortText } @@ -181,6 +191,20 @@ export default props => ( flags { isOptional } + signatures { + comment { + shortText + } + parameters { + name + type { + name + } + } + type { + name + } + } } } } diff --git a/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js b/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js index 2ec2c512f07..324fb1c32ef 100644 --- a/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js +++ b/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js @@ -1,7 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styles from './propsTableTs.styles'; -import { css } from '@patternfly/react-styles'; import { Table, Row, TD, TH, Body, Heading } from '../table'; import Section from '../section'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; @@ -13,38 +11,62 @@ const docGenPropShape = PropTypes.shape({ name: PropTypes.string, type: PropTypes.string }), - flags: PropTypes.shape({ isOptional: PropTypes.bool }) + flags: PropTypes.shape({ isOptional: PropTypes.bool }), + signatures: PropTypes.array }); const propTypes = { name: PropTypes.string.isRequired, - props: PropTypes.arrayOf(docGenPropShape) + props: PropTypes.arrayOf(docGenPropShape), + defaultProps: PropTypes.any }; const defaultProps = { - props: [] + props: [], + defaultProps: {} }; -export const PropsTableTs = ({ name, props }) => ( +export const PropsTableTs = ({ name, props, defaultProps: defaults }) => (
    - {/* */} + - {props.map(prop => ( - - - - - {/* */} - - - ))} + {props.map(prop => { + let typeName = prop.type && prop.type.name; + let comment = prop.comment && prop.comment.shortText;; + if (!prop.type && prop.kindString && prop.kindString === 'Method') { + typeName = 'func'; + comment = prop.signatures.length && prop.signatures[0].comment && prop.signatures[0].comment.shortText; + } else if (!prop.type.name) { + if (prop.type.type === 'reflection') { + typeName = 'func'; + } else { + typeName = prop.type.type; + } + } + // TODO: Parse function signature and return that info + return ( + + + + + + + + ); + })}
    Name Type RequiredDefaultDefault Description {prop.name}{prop.type.name}{prop.flags.isOptional === null && }{Boolean(prop.defaultValue) && prop.defaultValue.value}{prop.comment && prop.comment.shortText}{prop.name}{typeName}{prop.flags.isOptional === null && } + {typeName === 'union' ? ( +
    {JSON.stringify(defaults[prop.name], null, 2)}
    + ) : ( + JSON.stringify(defaults[prop.name], null, 2) + )} +
    {comment}
    diff --git a/packages/patternfly-4/react-integration/demo-app-ts/package.json b/packages/patternfly-4/react-integration/demo-app-ts/package.json index 3f4077b6244..11c1b990445 100644 --- a/packages/patternfly-4/react-integration/demo-app-ts/package.json +++ b/packages/patternfly-4/react-integration/demo-app-ts/package.json @@ -3,7 +3,8 @@ "private": true, "dependencies": { "react": "^16.7.0", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "@patternfly/react-core": "file:../../react-core" }, "scripts": { "build:demo": "react-scripts build", @@ -16,9 +17,6 @@ "react-scripts": "2.1.3", "typescript": "3.2.2" }, - "dependencies": { - "@patternfly/react-core": "file:../../react-core" - }, "eslintConfig": { "extends": "react-app" }, diff --git a/packages/patternfly-4/react-integration/demo-app-ts/src/App.tsx b/packages/patternfly-4/react-integration/demo-app-ts/src/App.tsx index f9e21e3f7ff..ab25e9c7f1b 100755 --- a/packages/patternfly-4/react-integration/demo-app-ts/src/App.tsx +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/App.tsx @@ -20,6 +20,7 @@ import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; import NavTest from './Nav'; +import WizardTest from './Wizard'; class myProps implements AvatarProps { alt: string = 'avatar'; @@ -70,6 +71,7 @@ class App extends Component {
    Hello
    + }> Success notification description. This is a link. diff --git a/packages/patternfly-4/react-integration/demo-app-ts/src/Wizard.tsx b/packages/patternfly-4/react-integration/demo-app-ts/src/Wizard.tsx new file mode 100644 index 00000000000..31d51a65815 --- /dev/null +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/Wizard.tsx @@ -0,0 +1,39 @@ +import { Wizard } from '@patternfly/react-core'; +import React, { Component } from 'react'; + +class WizardTest extends Component { + render() { + const steps = [ + { name: 'A', component:

    Step 1

    }, + { + name: 'B', + steps: [ + { + name: 'B-1', + component:

    Step 2

    , + enableNext: true + }, + { + name: 'B-2', + component:

    Step 3

    , + enableNext: false, + canJumpTo: false + } + ] + }, + { name: 'C', component:

    Step 4

    }, + { name: 'D', component:

    Step 5

    } + ]; + return ( + + ); + } +} + +export default WizardTest; diff --git a/scripts/generators/patternfly-4-component/index.js b/scripts/generators/patternfly-4-component/index.js index 7a0d8608477..5143d7225ba 100644 --- a/scripts/generators/patternfly-4-component/index.js +++ b/scripts/generators/patternfly-4-component/index.js @@ -59,7 +59,7 @@ function setPF4Generators(plop) { data, type: 'add', templateFile: join(base, 'example.js.hbs'), - path: join(reactCoreRoot, './src/{{typeDir}}/{{componentName}}/example/Simple{{componentName}}.ts') + path: join(reactCoreRoot, './src/{{typeDir}}/{{componentName}}/examples/Simple{{componentName}}.tsx') }, { data, diff --git a/scripts/generators/patternfly-4-component/templates/component/component.tsx.hbs b/scripts/generators/patternfly-4-component/templates/component/component.tsx.hbs index 0eb7d71efe5..f2f26925f59 100644 --- a/scripts/generators/patternfly-4-component/templates/component/component.tsx.hbs +++ b/scripts/generators/patternfly-4-component/templates/component/component.tsx.hbs @@ -13,7 +13,7 @@ children?: any; className?: string; } -const {{componentName}}: React.SFC< {{componentName}} Props> = (props) => ( +const {{componentName}}: React.SFC<{{componentName}}Props> = (props) => (
    {props.children}
    From de204d24cc5d2b549429f7595d74854d939d25be Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Tue, 12 Mar 2019 01:04:31 -0400 Subject: [PATCH 2/8] add index.d.ts --- .../patternfly-4/react-core/src/components/Wizard/index.d.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/patternfly-4/react-core/src/components/Wizard/index.d.ts diff --git a/packages/patternfly-4/react-core/src/components/Wizard/index.d.ts b/packages/patternfly-4/react-core/src/components/Wizard/index.d.ts new file mode 100644 index 00000000000..dee947532b4 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/index.d.ts @@ -0,0 +1 @@ +export { default as Wizard, WizardProps } from './Wizard'; From 08ea9e619913dd3e129ae738fb1a021292fad96b Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Tue, 12 Mar 2019 11:01:34 -0400 Subject: [PATCH 3/8] toggle caret icon rotate when open --- .../react-core/src/components/Wizard/WizardToggle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx index 82f7d6c7e93..9dd5d740fd0 100644 --- a/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx @@ -46,7 +46,7 @@ class WizardToggle extends React.Component { } return ( <> - } -
    +
    diff --git a/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx index 5b0db23bb73..add45a82ae9 100644 --- a/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx @@ -3,11 +3,13 @@ import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; import { css } from '@patternfly/react-styles'; import { AngleRightIcon, CaretDownIcon } from '@patternfly/react-icons'; import { WizardStep } from './Wizard'; +import WizardBody from './WizardBody'; interface WizardToggleProps { nav: any; steps: WizardStep[]; activeStep: WizardStep; + children: React.ReactNode } class WizardToggle extends React.Component { @@ -23,7 +25,7 @@ class WizardToggle extends React.Component { public render() { const { isOpen } = this.state; - const { nav, steps, activeStep } = this.props; + const { nav, steps, activeStep, children } = this.props; let activeStepIndex; let activeStepName; let activeStepSubName; @@ -59,13 +61,13 @@ class WizardToggle extends React.Component {