/** * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ import "./wizard.scss"; import React from "react"; import { cssNames, prevDefault } from "../../utils"; import { Button } from "../button"; import { Stepper } from "../stepper"; import { SubTitle } from "../layout/sub-title"; import { Spinner } from "../spinner"; import { debounce } from "lodash"; export interface WizardCommonProps { data?: Partial; save?: (data: Partial, callback?: () => void) => void; reset?: () => void; done?: () => void; hideSteps?: boolean; } export interface WizardProps extends WizardCommonProps { className?: string; step?: number; title?: string; header?: React.ReactNode; onChange?: (step: number) => void; children?: React.ReactElement>[] | React.ReactElement>; } interface State { step: number; } export class Wizard extends React.Component, State> { public state: State = { step: this.getValidStep(this.props.step ?? 0), }; get steps(): React.ReactElement>[] { const { className, title, step, header, onChange, children, ...commonProps } = this.props; const steps = React.Children.toArray(children) as React.ReactElement>[]; return steps .filter(step => !step.props.skip) .map((stepElem, i) => ( React.cloneElement(stepElem, { step: i + 1, wizard: this, next: this.nextStep, prev: this.prevStep, first: this.firstStep, last: this.lastStep, isFirst: this.isFirstStep, isLast: this.isLastStep, ...commonProps, ...stepElem.props, }) )); } get step() { return this.state.step; } set step(step: number) { step = this.getValidStep(step); if (step === this.step) return; this.setState({ step }, () => { this.props.onChange?.(step); }); } protected getValidStep(step: number) { return Math.min(Math.max(1, step), this.steps.length) || 1; } isFirstStep = () => this.step === 1; isLastStep = () => this.step === this.steps.length; firstStep = (): any => this.step = 1; nextStep = (): any => this.step++; prevStep = (): any => this.step--; lastStep = (): any => this.step = this.steps.length; render() { const { className, title, header, hideSteps } = this.props; const steps = this.steps.map(stepElem => ({ title: stepElem.props.title })); const step = React.cloneElement(this.steps[this.step - 1]); return (
{header} {title ? : null} {!hideSteps && steps.length > 1 ? : null}
{step}
); } } export interface WizardStepProps extends WizardCommonProps { wizard?: Wizard; title?: string; className?: string | object; contentClass?: string | object; customButtons?: React.ReactNode; // render custom buttons block in footer moreButtons?: React.ReactNode; // add more buttons to section in the footer loading?: boolean; // indicator of loading content for the step waiting?: boolean; // indicator of waiting response before going to next step disabledNext?: boolean; // disable next button flag, e.g when filling step is not finished hideNextBtn?: boolean; hideBackBtn?: boolean; step?: number; prevLabel?: React.ReactNode; // custom label for prev button nextLabel?: React.ReactNode; // custom label for next button next?: () => void | boolean | Promise; // custom action for next button prev?: () => void; // custom action for prev button first?: () => void; last?: () => void; isFirst?: () => boolean; isLast?: () => boolean; beforeContent?: React.ReactNode; afterContent?: React.ReactNode; noValidate?: boolean; // no validate form attribute skip?: boolean; // don't render the step scrollable?: boolean; children?: React.ReactNode | React.ReactNode[]; } interface WizardStepState { waiting?: boolean; } export class WizardStep extends React.Component, WizardStepState> { private form: HTMLFormElement | null = null; public state: WizardStepState = {}; private unmounting = false; static defaultProps: WizardStepProps = { scrollable: true, }; componentWillUnmount() { this.unmounting = true; } prev = () => { const { isFirst, prev, done } = this.props; if (isFirst?.() && done) done(); else prev?.(); }; next = () => { const next = this.props.next; const nextStep = this.props.wizard?.nextStep; if (nextStep !== next) { const result = next?.(); if (result instanceof Promise) { this.setState({ waiting: true }); result.then(nextStep).finally(() => { if (this.unmounting) return; this.setState({ waiting: false }); }); } else if (typeof result === "boolean" && result) { nextStep?.(); } } else { nextStep?.(); } }; //because submit MIGHT be called through pressing enter, it might be fired twice. //we'll debounce it to ensure it isn't submit = debounce(() => { if (!this.form) { return; } if (this.form.noValidate || this.form.checkValidity()) { this.next(); } }, 100); renderLoading() { return (
); } //make sure we call submit if the "enter" keypress doesn't trigger the events keyDown(evt: React.KeyboardEvent) { if (evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey || evt.repeat) { return; } if(evt.key === "Enter"){ this.submit(); } } render() { const { step, isFirst, isLast, children, loading, customButtons, disabledNext, scrollable, hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons, waiting, className, contentClass, prevLabel, nextLabel, } = this.props; if (skip) { return null; } return (
this.keyDown(evt)} ref={e => this.form = e} > {beforeContent}
{loading ? this.renderLoading() : children}
{customButtons ?? (
{moreButtons}
)} {afterContent}
); } }