diff --git a/package.json b/package.json index 8bc6e9f399f..5e488b3b962 100644 --- a/package.json +++ b/package.json @@ -97,13 +97,12 @@ }, "scripts": { "bootstrap": "lerna bootstrap", - "build": "yarn prebuild:pf4 && lerna run --parallel build", - "commit": "git-cz", - "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", "clean": "lerna run clean --parallel", + "commit": "git-cz", "generate": "yarn plop", "lint": "lerna run lint --parallel", "lint:fix": "lerna run lint:fix --parallel", 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..b39bedd3899 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.docs.js @@ -0,0 +1,20 @@ +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 + }, + types: { + WizardStepFunctionType: + '(newStep: { id?: string | number; name: string; }, prevStep: { prevId?: string | number; prevName: string; }) => void' + }, + 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..b962b05230b --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import { Wizard, WizardStep, WizardStepFunctionType } from './Wizard'; + +test('Wizard should match snapshot', () => { + const steps: WizardStep[] = [ + { 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 onBack: WizardStepFunctionType = (step) => { + const name = { step }; + }; + 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..2884073f150 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/Wizard.tsx @@ -0,0 +1,387 @@ +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 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 WizardStep { + /** 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 WizardStep { + /** The condition needed to be able to navigate to this step */ + canJumpTo?: boolean; +}; + +export type WizardStepFunctionType = (newStep: { id?: string | number; name: string; }, prevStep: { prevId?: string | number; prevName: string; }) => void; + +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?: WizardStepFunctionType; + /** Callback function after Back button is clicked */ + onBack?: WizardStepFunctionType; + /** Calback function when a step in the nav is clicked */ + onGoToStep?: WizardStepFunctionType; + /** Additional classes spread to the Wizard */ + className?: string; + /** The wizard steps configuration object */ + steps: WizardStep[]; + /** 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; + /** Can remove the default padding around the main body content by setting this to false */ + hasBodyPadding?: boolean; +} + +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' +}; + +let currentId = 0; + +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', + hasBodyPadding: true + }; + + public container = null; + public titleId = `pf-wizard-title-0`; + public descriptionId = `pf-wizard-description-0`; + + constructor(props) { + super(props); + const newId = currentId++; + this.titleId = `pf-wizard-title-${newId}`; + this.descriptionId = `pf-wizard-description-${newId}`; + } + + public state = { + currentStep: Number.isInteger(this.props.startAtStep) ? this.props.startAtStep : 1, + isNavOpen: false + }; + + private handleKeyClicks = event => { + if (event.keyCode === KEY_CODES.ESCAPE_KEY) { + if (this.state.isNavOpen) { + this.setState({ isNavOpen: !this.state.isNavOpen }) + } else if (this.props.isOpen) { + this.props.onClose(); + } + } + }; + + private toggleSiblingsFromScreenReaders = hide => { + const bodyChildren = document.body.children; + for (const child of Array.from(bodyChildren)) { + if (child !== this.container) { + hide ? child.setAttribute('aria-hidden', hide) : child.removeAttribute('aria-hidden'); + } + } + }; + + public componentDidMount() { + document.body.appendChild(this.container); + this.toggleSiblingsFromScreenReaders(true); + document.addEventListener('keydown', this.handleKeyClicks, false); + } + + public componentWillUnmount() { + document.body.removeChild(this.container); + this.toggleSiblingsFromScreenReaders(false); + document.removeEventListener('keydown', this.handleKeyClicks, false); + } + + private onNavToggle = isNavOpen => { + this.setState({ isNavOpen }) + } + + 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: prevId, name: prevName } = flattenedSteps[currentStep - 1]; + const { id, name } = flattenedSteps[newStep - 1]; + return onNext && onNext({ id, name }, { prevId, prevName }); + } + }; + + 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: prevId, name: prevName } = flattenedSteps[currentStep - 1]; + const { id, name } = flattenedSteps[newStep - 1]; + return onBack && onBack({ id, name }, { prevId, prevName }); + }; + + private goToStep = step => { + const { onGoToStep } = this.props; + const { currentStep } = this.state; + 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: prevId, name: prevName } = flattenedSteps[currentStep - 1]; + const { id, name } = flattenedSteps[newStep - 1]; + return onGoToStep && onGoToStep({ id, name }, { prevId, prevName }); + }; + + 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, + hasBodyPadding, + ...rest + } = this.props; + const { currentStep, isNavOpen } = 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 = isWizardNavOpen => ( + + {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( + + + +
+ + + +
+ + {!firstStep && !activeStep.hideBackButton && } + {!activeStep.hideCancelButton && } +
+
+
+
+
+
, + this.container + ) + ); + } +} + +export { 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..af29496a3f6 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardBody.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import styles from '@patternfly/patternfly/components/Wizard/wizard.css'; +import { css } from '@patternfly/react-styles'; + +interface WizardBodyProps { + children?: any; + hasBodyPadding: boolean; +} + +const WizardBody: React.SFC = ({ children, hasBodyPadding }) => ( +
+ {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..ebf68b4094a --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardHeader.tsx @@ -0,0 +1,33 @@ +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; + titleId: string; + descriptionId: string; +} + +const WizardHeader: React.SFC = ({ onClose, title, description, ariaLabel, titleId, descriptionId }) => ( +
+ + {title} + {description &&

+ {description} +

} +
+); + +WizardHeader.defaultProps = { + onClose: () => undefined +}; + +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..38cc46e40c2 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/WizardToggle.tsx @@ -0,0 +1,75 @@ +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 { WizardStep } from './Wizard'; +import WizardBody from './WizardBody'; + +interface WizardToggleProps { + nav: any; + steps: WizardStep[]; + activeStep: WizardStep; + children: React.ReactNode, + hasBodyPadding: boolean; + isNavOpen: boolean; + onNavToggle: (isOpen: boolean) => void; +} + +class WizardToggle extends React.Component { + + private toggleNav = () => { + this.props.onNavToggle(!this.props.isNavOpen); + } + + public render() { + const { isNavOpen: isOpen } = this.props; + const { nav, steps, activeStep, children, hasBodyPadding } = this.props; + let activeStepIndex; + let activeStepName; + let activeStepSubName; + for (let i = 0; i < steps.length; i++) { + if (steps[i].steps) { + // tslint:disable-next-line + 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 ( + <> + +
    +
    + {nav(isOpen)} + {activeStep.component} +
    + {children} +
    + + ); + } + +} + +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..1e14252ea2f --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/__snapshots__/Wizard.test.tsx.snap @@ -0,0 +1,559 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wizard should match snapshot 1`] = ` +
    + Object { + "$$typeof": Symbol(react.portal), + "children": + + +
    + + + + Step 1 +

    , + "name": "A", + } + } + hasBodyPadding={true} + isNavOpen={false} + nav={[Function]} + onNavToggle={[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", + }, + ] + } + > +
    + + +
    +
    +
    +
    +
    +
    , + "containerInfo":
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    , + "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..92ffb6ec2f6 --- /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 }, { prevId, prevName }) => { + console.log(`current id: ${id}, current name: ${name}, previous id: ${prevId}, previous name: ${prevName}`); + this.areAllStepsValid(); + }; + + onBack = ({ id, name }, { prevId, prevName }) => { + console.log(`current id: ${id}, current name: ${name}, previous id: ${prevId}, previous name: ${prevName}`); + this.areAllStepsValid(); + }; + + onGoToStep = ({ id, name }, { prevId, prevName }) => { + console.log(`current id: ${id}, current name: ${name}, previous id: ${prevId}, previous name: ${prevName}`); + }; + + 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..4ad68566220 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/Wizard/index.ts @@ -0,0 +1 @@ +export * 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 075fb3cc09c..8c482d8d4e6 100644 --- a/packages/patternfly-4/react-core/tslint.json +++ b/packages/patternfly-4/react-core/tslint.json @@ -1,30 +1,52 @@ { - "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"], + "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"], + "quotemark": [ + true, + "single", + "jsx-double" + ], "variable-name": false, "ordered-imports": false }, "rulesDirectory": [], "linterOptions": { - "exclude": [ - "**/*.js" - ] + "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 303367ba33d..4afce646396 100644 --- a/packages/patternfly-4/react-docs/gatsby-node.js +++ b/packages/patternfly-4/react-docs/gatsby-node.js @@ -91,7 +91,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 fe232903b0b..9e817dac92f 100644 --- a/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js +++ b/packages/patternfly-4/react-docs/src/components/componentDocs/componentDocs.js @@ -29,6 +29,7 @@ const propTypes = { ), components: PropTypes.objectOf(PropTypes.func), enumValues: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.any)), + types: PropTypes.object, rawExamples: PropTypes.array, images: PropTypes.array, fullPageOnly: PropTypes.bool, @@ -41,6 +42,7 @@ const defaultProps = { examples: [], components: {}, enumValues: {}, + types: {}, rawExamples: [], images: [], fullPageOnly: false, @@ -57,6 +59,7 @@ class ComponentDocs extends React.PureComponent { examples, components, enumValues, + types, fullPageOnly, rawExamples, images, @@ -96,11 +99,21 @@ class ComponentDocs extends React.PureComponent { ); })} - {Object.entries(components).map(([componentName]) => { + {Object.entries(components).map(component => { + const componentName = component[0]; + const componentFunction = component[1]; const componentDocsJs = getDocGenInfo(componentName); const componentDocsTs = getDocGenInfoTs(componentName); if (componentDocsTs) { - return ; + return ( + + ); } else if (componentDocsJs) { return ( ( name children { name + kindString comment { shortText } @@ -176,6 +190,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 ca3a70d268c..17c6130b4c5 100644 --- a/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js +++ b/packages/patternfly-4/react-docs/src/components/propsTableTs/propsTableTs.js @@ -11,20 +11,30 @@ 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), + types: PropTypes.object, + defaultProps: PropTypes.any }; const defaultProps = { - props: [] + props: [], + types: {}, + defaultProps: {} }; -export const PropsTableTs = ({ name, props }) => ( -
    +export const PropsTableTs = ({ name, props, defaultProps: defaults, types }) => ( +
    @@ -34,6 +44,40 @@ export const PropsTableTs = ({ name, props }) => ( + {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; + } + } else if (prop.type && prop.type.type && prop.type.type === 'reference') { + if (types[prop.type.name]) { + typeName = types[prop.type.name]; + } + } + // TODO: Parse function signature and return that info + return ( + + + + + + + + ); + })}
    NameDescription {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/src/App.tsx b/packages/patternfly-4/react-integration/demo-app-ts/src/App.tsx index d18ee5f9a74..c83d8a798a2 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 @@ -21,6 +21,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'; @@ -72,6 +73,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..b55b5731df4 --- /dev/null +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/Wizard.tsx @@ -0,0 +1,40 @@ +import { Wizard, WizardStep } from '@patternfly/react-core'; +import React, { Component } from 'react'; + +class WizardTest extends Component { + render() { + const steps: WizardStep[] = [ + { 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/packages/patternfly-4/react-integration/package.json b/packages/patternfly-4/react-integration/package.json index 0bcca642352..c113be19238 100644 --- a/packages/patternfly-4/react-integration/package.json +++ b/packages/patternfly-4/react-integration/package.json @@ -26,7 +26,8 @@ "cypress:open": "cypress open", "cypress:run": "cypress run", "integration:setup": "rimraf node_modules && yarn install", - "integration:test": "node ./scripts/server.js" + "integration:test": "node ./scripts/server.js", + "build:demo": "yarn integration:setup && yarn --cwd demo-app-ts build:demo" }, "dependencies": { "local-web-server": "^2.6.1" 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}
    diff --git a/yarn.lock b/yarn.lock index 04987929154..07f376c0a94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15976,6 +15976,7 @@ react-diff-view@^1.8.1: react-docgen@3.0.0-rc.2: version "3.0.0-rc.2" resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-3.0.0-rc.2.tgz#5939c64699fd9959da6d97d890f7b648e542dbcc" + integrity sha512-tXbIvq7Hxdc92jW570rztqsz0adtWEM5FX8bShJYozT2Y6L/LeHvBMQcED6mSqJ72niiNMPV8fi3S37OHrGMEw== dependencies: "@babel/parser" "^7.1.3" "@babel/runtime" "^7.0.0"