diff --git a/packages/react/.storybook/story-config.ts b/packages/react/.storybook/story-config.ts index 3e9d47c4..58aae017 100644 --- a/packages/react/.storybook/story-config.ts +++ b/packages/react/.storybook/story-config.ts @@ -75,7 +75,8 @@ export type Stories = | 'Tooltip' | 'Typography' | 'UserDropdownMenu' - | 'Welcome'; + | 'Welcome' + | 'Wizard'; export type StorybookConfig = Record< Stories, { @@ -233,6 +234,9 @@ const StoryConfig: StorybookConfig = { Welcome: { hierarchy: 'Welcome', }, + Wizard: { + hierarchy: `${StorybookCategories.Patterns}/Wizard`, + } }; export default StoryConfig; diff --git a/packages/react/src/components/Box/Box.tsx b/packages/react/src/components/Box/Box.tsx index 22d0ef50..c453b4b3 100644 --- a/packages/react/src/components/Box/Box.tsx +++ b/packages/react/src/components/Box/Box.tsx @@ -18,7 +18,7 @@ import MuiBox, {BoxProps as MuiBoxProps} from '@mui/material/Box'; import clsx from 'clsx'; -import {ElementType, FC, ReactElement} from 'react'; +import {ElementType, forwardRef, ForwardRefExoticComponent, MutableRefObject, ReactElement} from 'react'; import {WithWrapperProps} from '../../models'; import {composeComponentDisplayName} from '../../utils'; @@ -28,13 +28,15 @@ export type BoxProps = { const COMPONENT_NAME: string = 'Box'; -const Box: FC & WithWrapperProps = (props: BoxProps): ReactElement => { - const {className, ...rest} = props; +const Box: ForwardRefExoticComponent & WithWrapperProps = forwardRef( + (props: BoxProps, ref: MutableRefObject): ReactElement => { + const {className, ...rest} = props; - const classes: string = clsx('oxygen-box', className); + const classes: string = clsx('oxygen-box', className); - return ; -}; + return ; + }, +) as ForwardRefExoticComponent & WithWrapperProps; Box.displayName = composeComponentDisplayName(COMPONENT_NAME); Box.muiName = COMPONENT_NAME; diff --git a/packages/react/src/components/Stepper/__tests__/__snapshots__/Stepper.test.tsx.snap b/packages/react/src/components/Stepper/__tests__/__snapshots__/Stepper.test.tsx.snap index 1b6253ab..cfa70448 100644 --- a/packages/react/src/components/Stepper/__tests__/__snapshots__/Stepper.test.tsx.snap +++ b/packages/react/src/components/Stepper/__tests__/__snapshots__/Stepper.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Stepper should match the snapshot 1`] = `

Step 1

diff --git a/packages/react/src/components/Wizard/Wizard.stories.mdx b/packages/react/src/components/Wizard/Wizard.stories.mdx new file mode 100644 index 00000000..0a83f482 --- /dev/null +++ b/packages/react/src/components/Wizard/Wizard.stories.mdx @@ -0,0 +1,129 @@ +import {ArgsTable, Source, Story, Canvas, Meta} from '@storybook/addon-docs'; +import Wizard from './Wizard.tsx'; +import dedent from 'ts-dedent'; +import Typography from '../Typography'; +import StoryConfig from '../../../.storybook/story-config.ts'; + +export const meta = { + component: Wizard, + title: StoryConfig.Wizard.hierarchy, +}; + + + +export const Template = args => ; + +# ActionCard + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) + +## Overview + +Wizard can be used to break down user flows step by step. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor porttitor dolor eget tristique. Nam + elementum, quam vel varius porttitor, purus est vestibulum augue, sed suscipit ligula metus at nibh. Donec + eleifend suscipit nisi mollis sollicitudin. Vestibulum fermentum odio at maximus lacinia. Phasellus leo ipsum, + vestibulum hendrerit enim vitae, ullamcorper tincidunt erat. Sed quam nulla, pharetra non mattis non, + fringilla non massa. Donec maximus finibus dui et suscipit. Suspendisse potenti. In imperdiet hendrerit + accumsan. Vivamus lacus nunc, mollis ut elementum eget, tempus vitae lectus. Ut molestie ante quis quam + aliquam pretium. Vestibulum dignissim, odio vel volutpat porta, enim nisl auctor turpis, vel imperdiet nisl + lorem id ligula. Proin varius scelerisque ligula ac consequat. + , + + Aliquam vel ex tortor. Proin sed ullamcorper massa. Sed eu fringilla risus, a faucibus tortor. Duis euismod + enim sit amet nunc condimentum, eget tristique ex ultricies. Suspendisse potenti. Phasellus risus ligula, + imperdiet in imperdiet ac, hendrerit eu quam. Aliquam leo risus, vulputate nec auctor viverra, dictum a elit. + Curabitur a accumsan lorem. Cras nec metus sed diam vehicula luctus nec sit amet dolor. Donec ac nibh finibus, + varius arcu sit amet, dapibus neque. Morbi orci augue, commodo vitae tincidunt vel, tincidunt at justo. + Quisque sem mauris, consectetur sit amet lobortis vel, consectetur sit amet massa. + , + + Praesent varius porta tellus, ac mattis quam blandit at. Vestibulum in nisi at est rhoncus posuere ac vitae + ligula. Phasellus molestie purus ac nulla vestibulum gravida. Morbi lacinia vehicula aliquam. Praesent mollis + mollis arcu eu finibus. Morbi at nunc quam. Aliquam sed urna quis erat elementum bibendum vitae eu massa. + , + + Sed placerat molestie tristique. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac + turpis egestas. Vestibulum at libero bibendum, tempor nunc vel, luctus ipsum. Aenean ut diam ligula. Ut + auctor, justo a tincidunt fermentum, nibh dui consectetur massa, eu congue sapien eros ut nisl. Nam nec + fringilla sem. Pellentesque facilisis fermentum nibh, in volutpat ipsum porttitor ut. Quisque auctor lorem et + dolor suscipit, nec rhoncus justo aliquam. Praesent elit sapien, tempor id mi et, fermentum accumsan justo. Ut + a justo tortor. Proin in nisl vel arcu congue tristique. Proin mattis condimentum orci, quis accumsan neque + auctor vel. + , + ], + onFinishButtonClick: () => alert('You have come to the end of the wizard!'), + }} + > + {Template.bind({})} + + + +## Props + + + +## Usage + +Import and use the `Wizard` component in your components as follows. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor porttitor dolor eget tristique. Nam + elementum, quam vel varius porttitor, purus est vestibulum augue, sed suscipit ligula metus at nibh. Donec + eleifend suscipit nisi mollis sollicitudin. Vestibulum fermentum odio at maximus lacinia. Phasellus leo ipsum, + vestibulum hendrerit enim vitae, ullamcorper tincidunt erat. Sed quam nulla, pharetra non mattis non, + fringilla non massa. Donec maximus finibus dui et suscipit. Suspendisse potenti. In imperdiet hendrerit + accumsan. Vivamus lacus nunc, mollis ut elementum eget, tempus vitae lectus. Ut molestie ante quis quam + aliquam pretium. Vestibulum dignissim, odio vel volutpat porta, enim nisl auctor turpis, vel imperdiet nisl + lorem id ligula. Proin varius scelerisque ligula ac consequat. + , + + Aliquam vel ex tortor. Proin sed ullamcorper massa. Sed eu fringilla risus, a faucibus tortor. Duis euismod + enim sit amet nunc condimentum, eget tristique ex ultricies. Suspendisse potenti. Phasellus risus ligula, + imperdiet in imperdiet ac, hendrerit eu quam. Aliquam leo risus, vulputate nec auctor viverra, dictum a elit. + Curabitur a accumsan lorem. Cras nec metus sed diam vehicula luctus nec sit amet dolor. Donec ac nibh finibus, + varius arcu sit amet, dapibus neque. Morbi orci augue, commodo vitae tincidunt vel, tincidunt at justo. + Quisque sem mauris, consectetur sit amet lobortis vel, consectetur sit amet massa. + , + + Praesent varius porta tellus, ac mattis quam blandit at. Vestibulum in nisi at est rhoncus posuere ac vitae + ligula. Phasellus molestie purus ac nulla vestibulum gravida. Morbi lacinia vehicula aliquam. Praesent mollis + mollis arcu eu finibus. Morbi at nunc quam. Aliquam sed urna quis erat elementum bibendum vitae eu massa. + , + + Sed placerat molestie tristique. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac + turpis egestas. Vestibulum at libero bibendum, tempor nunc vel, luctus ipsum. Aenean ut diam ligula. Ut + auctor, justo a tincidunt fermentum, nibh dui consectetur massa, eu congue sapien eros ut nisl. Nam nec + fringilla sem. Pellentesque facilisis fermentum nibh, in volutpat ipsum porttitor ut. Quisque auctor lorem et + dolor suscipit, nec rhoncus justo aliquam. Praesent elit sapien, tempor id mi et, fermentum accumsan justo. Ut + a justo tortor. Proin in nisl vel arcu congue tristique. Proin mattis condimentum orci, quis accumsan neque + auctor vel. + , + ]} + /> + ); +}`} +/> diff --git a/packages/react/src/components/Wizard/Wizard.tsx b/packages/react/src/components/Wizard/Wizard.tsx new file mode 100644 index 00000000..b754cdb5 --- /dev/null +++ b/packages/react/src/components/Wizard/Wizard.tsx @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import LinearProgress from '@mui/material/LinearProgress'; +import clsx from 'clsx'; +import {FC, HTMLAttributes, ReactElement, useCallback, useMemo, useState} from 'react'; +import {WithWrapperProps} from '../../models'; +import {composeComponentDisplayName} from '../../utils'; +import Box from '../Box'; +import Button from '../Button'; +import Card from '../Card'; +import CardActions from '../CardActions'; +import CardContent from '../CardContent'; +import CardHeader from '../CardHeader'; +import {Stepper} from '../Stepper'; +import Typography from '../Typography'; +import './wizard.scss'; + +export interface WizardProps extends HTMLAttributes { + /** + * Allow backward navigation. This will show a button allowing you to navigate backwards. + */ + allowBackwardNavigation?: boolean; + /** + * Allow cancel. This will show a button allowing you to cancel the wizard. + */ + allowCancel?: boolean; + /** + * Animate the slide transition. + */ + animateOnSlide?: boolean; + /** + * Cancel button text. + */ + cancelButtonText?: string; + /** + * Finish button text. + */ + finishButtonText?: string; + /** + * Next button text. + */ + nextButtonText?: string; + /** + * Callback to be called when the cancel button is clicked. + */ + onCancelButtonClick: () => Promise; + /** + * Callback to be called when the finish button is clicked. + */ + onFinishButtonClick: () => Promise; + /** + * Callback to be called when the next button is clicked. + */ + onNextButtonClick: () => Promise; + /** + * Callback to be called when the previous button is clicked. + */ + onPreviousButtonClick: () => Promise; + /** + * Previous button text. + */ + previousButtonText?: string; + /** + * Steps to be rendered. + */ + steps: ReactElement[]; + /** + * Subtitle of the wizard. + */ + subtitle: string; + /** + * Title of the wizard. + */ + title: string; +} + +const COMPONENT_NAME: string = 'Wizard'; + +const Wizard: FC & WithWrapperProps = (props: WizardProps): ReactElement => { + const { + allowBackwardNavigation, + allowCancel, + animateOnSlide, + className, + title, + subtitle, + nextButtonText, + previousButtonText, + cancelButtonText, + onCancelButtonClick, + onNextButtonClick, + onPreviousButtonClick, + onFinishButtonClick, + finishButtonText, + steps, + } = props; + + const [currentStep, setCurrentStep] = useState(0); + + const classes: string = clsx('oxygen-wizard', className); + + const isLastStep: boolean = useMemo(() => currentStep === steps.length - 1, [steps, currentStep]); + const isFirstStep: boolean = useMemo(() => currentStep === 0, [currentStep]); + const displayCurrentStep: number = useMemo(() => currentStep + 1, [currentStep]); + + const handleNextButtonClick: () => Promise = useCallback(async (): Promise => { + if (isLastStep) { + if (onFinishButtonClick && onFinishButtonClick instanceof Function) { + await onFinishButtonClick(); + } + + return; + } + + if (onNextButtonClick && onNextButtonClick instanceof Function) { + await onNextButtonClick(); + } + + setCurrentStep((step: number) => step + 1); + }, [isLastStep]); + + const handlePreviousButtonClick: () => Promise = useCallback(async (): Promise => { + if (isFirstStep) { + return; + } + + if (onPreviousButtonClick && onPreviousButtonClick instanceof Function) { + await onPreviousButtonClick(); + } + setCurrentStep((step: number) => step - 1); + }, [isFirstStep]); + + return ( + + + + + + + + + {allowBackwardNavigation && !isFirstStep && ( + + )} + + + {allowCancel && ( + + )} + + + + + + {`${displayCurrentStep}/${steps.length}`} + + + + + + ); +}; + +Wizard.displayName = composeComponentDisplayName(COMPONENT_NAME); +Wizard.muiName = COMPONENT_NAME; +Wizard.defaultProps = { + allowBackwardNavigation: true, + allowCancel: false, + cancelButtonText: 'Cancel', + finishButtonText: 'Finish', + nextButtonText: 'Next', + previousButtonText: 'Previous', +}; + +export default Wizard; diff --git a/packages/react/src/components/Wizard/index.ts b/packages/react/src/components/Wizard/index.ts new file mode 100644 index 00000000..a92cd99f --- /dev/null +++ b/packages/react/src/components/Wizard/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export {default as Wizard} from './Wizard'; +export type {WizardProps} from './Wizard'; diff --git a/packages/react/src/components/Wizard/wizard.scss b/packages/react/src/components/Wizard/wizard.scss new file mode 100644 index 00000000..69de307a --- /dev/null +++ b/packages/react/src/components/Wizard/wizard.scss @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.oxygen-wizard { + .oxygen-wizard-card { + margin-bottom: var(--oxygen-customComponents-Stepper-properties-action-margin-bottom); + } + + .oxygen-wizard-actions { + justify-content: space-between; + } + + .oxygen-wizard-progress-container { + width: 40%; + margin: auto; + display: flex; + gap: var(--oxygen-customComponents-Stepper-properties-progress-gap); + + .oxygen-wizard-progress-bar-container { + width: 100%; + display: flex; + align-items: center; + + .oxygen-wizard-progress-bar { + width: 100%; + } + } + } + + .oxygen-wizard-right-aligned-buttons { + display: flex; + justify-content: flex-end; + gap: var(--oxygen-customComponents-Stepper-properties-right-button-gap); + } +}