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(
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ 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 }) => (
+
| Name |
@@ -34,6 +44,40 @@ export const PropsTableTs = ({ name, props }) => (
Description |
+ {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 (
+
+ | {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"