From 1c001a782534bc4355cfe606ef5a0da765951b62 Mon Sep 17 00:00:00 2001 From: Thivi Date: Wed, 22 Feb 2023 17:57:46 +0530 Subject: [PATCH] feat(react): add Carousel component --- .../primitives/src/icons/chevron-left-16.svg | 21 ++ .../primitives/src/icons/chevron-right-16.svg | 21 ++ .../assets/images/carousel-illustration.svg | 8 + packages/react/.storybook/story-config.ts | 4 + .../__snapshots__/ActionCard.test.tsx.snap | 2 +- .../__snapshots__/Card.test.tsx.snap | 2 +- .../components/CardContent/CardContent.tsx | 14 +- .../components/Carousel/Carousel.stories.mdx | 82 ++++++++ .../src/components/Carousel/Carousel.tsx | 188 ++++++++++++++++++ .../src/components/Carousel/carousel.scss | 53 +++++ .../react/src/components/Carousel/index.ts | 20 ++ packages/react/src/theme/default-theme.ts | 1 + 12 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 packages/primitives/src/icons/chevron-left-16.svg create mode 100644 packages/primitives/src/icons/chevron-right-16.svg create mode 100644 packages/react/.storybook/static/assets/images/carousel-illustration.svg create mode 100644 packages/react/src/components/Carousel/Carousel.stories.mdx create mode 100644 packages/react/src/components/Carousel/Carousel.tsx create mode 100644 packages/react/src/components/Carousel/carousel.scss create mode 100644 packages/react/src/components/Carousel/index.ts diff --git a/packages/primitives/src/icons/chevron-left-16.svg b/packages/primitives/src/icons/chevron-left-16.svg new file mode 100644 index 00000000..17d18e9b --- /dev/null +++ b/packages/primitives/src/icons/chevron-left-16.svg @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/primitives/src/icons/chevron-right-16.svg b/packages/primitives/src/icons/chevron-right-16.svg new file mode 100644 index 00000000..522d5906 --- /dev/null +++ b/packages/primitives/src/icons/chevron-right-16.svg @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/react/.storybook/static/assets/images/carousel-illustration.svg b/packages/react/.storybook/static/assets/images/carousel-illustration.svg new file mode 100644 index 00000000..3d0ec0aa --- /dev/null +++ b/packages/react/.storybook/static/assets/images/carousel-illustration.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/react/.storybook/story-config.ts b/packages/react/.storybook/story-config.ts index 58aae017..4b10366a 100644 --- a/packages/react/.storybook/story-config.ts +++ b/packages/react/.storybook/story-config.ts @@ -44,6 +44,7 @@ export type Stories = | 'CardActions' | 'CardContent' | 'CardHeader' + | 'Carousel' | 'CircularProgressAvatar' | 'Chip' | 'ColorModeToggle' @@ -133,6 +134,9 @@ const StoryConfig: StorybookConfig = { CardHeader: { hierarchy: `${StorybookCategories.Surfaces}/CardHeader`, }, + Carousel: { + hierarchy: `${StorybookCategories.Patterns}/Carousel`, + }, CircularProgressAvatar: { hierarchy: `${StorybookCategories.DataDisplay}/Circular Progress Avatar`, }, diff --git a/packages/react/src/components/ActionCard/__tests__/__snapshots__/ActionCard.test.tsx.snap b/packages/react/src/components/ActionCard/__tests__/__snapshots__/ActionCard.test.tsx.snap index 1d46dd55..233d9223 100644 --- a/packages/react/src/components/ActionCard/__tests__/__snapshots__/ActionCard.test.tsx.snap +++ b/packages/react/src/components/ActionCard/__tests__/__snapshots__/ActionCard.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ActionCard should match the snapshot 1`] = `
diff --git a/packages/react/src/components/CardContent/CardContent.tsx b/packages/react/src/components/CardContent/CardContent.tsx index 5e8bdfa0..3f2a55d8 100644 --- a/packages/react/src/components/CardContent/CardContent.tsx +++ b/packages/react/src/components/CardContent/CardContent.tsx @@ -18,7 +18,7 @@ import MuiCardContent, {CardContentProps as MuiCardContentProps} from '@mui/material/CardContent'; import clsx from 'clsx'; -import {FC, ReactElement} from 'react'; +import {forwardRef, ForwardRefExoticComponent, MutableRefObject, ReactElement} from 'react'; import {WithWrapperProps} from '../../models'; import {composeComponentDisplayName} from '../../utils'; import './card-content.scss'; @@ -27,13 +27,15 @@ export type CardContentProps = MuiCardContentProps; const COMPONENT_NAME: string = 'CardContent'; -const CardContent: FC & WithWrapperProps = (props: CardContentProps): ReactElement => { - const {className, ...rest} = props; +const CardContent: ForwardRefExoticComponent & WithWrapperProps = forwardRef( + (props: CardContentProps, ref: MutableRefObject): ReactElement => { + const {className, ...rest} = props; - const classes: string = clsx('oxygen-card-content', className); + const classes: string = clsx('oxygen-card-content', className); - return ; -}; + return ; + }, +) as ForwardRefExoticComponent & WithWrapperProps; CardContent.displayName = composeComponentDisplayName(COMPONENT_NAME); CardContent.muiName = COMPONENT_NAME; diff --git a/packages/react/src/components/Carousel/Carousel.stories.mdx b/packages/react/src/components/Carousel/Carousel.stories.mdx new file mode 100644 index 00000000..773b564d --- /dev/null +++ b/packages/react/src/components/Carousel/Carousel.stories.mdx @@ -0,0 +1,82 @@ +import {ArgsTable, Source, Story, Canvas, Meta} from '@storybook/addon-docs'; +import Carousel from './Carousel.tsx'; +import dedent from 'ts-dedent'; +import Typography from '../Typography'; +import StoryConfig from '../../../.storybook/story-config.ts'; + +export const meta = { + component: Carousel, + title: StoryConfig.Carousel.hierarchy, +}; + + + +export const Template = args => ; + +# ActionCard + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) + +## Overview + +Carousel can be used to slide through content. + + + , + buttonText: "Add first name", + onButtonClick: ()=>{} + }, + { + title: "What is your last name?", + description: "Start theming journey with Oxygen UI", + illustration: carousel illustration, + buttonText: "Add last name", + onButtonClick: ()=>{} + } + ], + title: "Complete your profile. It’s at 60%" + }} + > + {Template.bind({})} + + + +## Props + + + +## Usage + +Import and use the `Carousel` component in your components as follows. + +, + buttonText: "Add first name", + onButtonClick: ()=>{} + } + ]} + /> + ); +}`} +/> diff --git a/packages/react/src/components/Carousel/Carousel.tsx b/packages/react/src/components/Carousel/Carousel.tsx new file mode 100644 index 00000000..1bb0310e --- /dev/null +++ b/packages/react/src/components/Carousel/Carousel.tsx @@ -0,0 +1,188 @@ +/** + * 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 {ChevronLeftIcon, ChevronRightIcon} from '@oxygen-ui/react-icons'; +import clsx from 'clsx'; +import {FC, HTMLAttributes, ReactElement, useEffect, 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 CardContent from '../CardContent'; +import {Stepper} from '../Stepper'; +import Typography from '../Typography'; +import './carousel.scss'; + +interface CarouselStep { + /** + * The text to be displayed in the button. + */ + buttonText: string; + /** + * The description of the step. + */ + description: string; + /** + * The illustration to be displayed in the step. + */ + illustration: ReactElement; + /** + * Callback method to be called when the button is clicked. + */ + onButtonClick: () => void; + /** + * The title of the step. + */ + title: string; +} + +export interface CarouselProps extends HTMLAttributes { + /** + * Specifies whether to auto play the carousel. + */ + autoPlay?: boolean; + /** + * Specifies the interval between slide transitions. + */ + autoPlayInterval?: number; + /** + * The text to be displayed in the next button. + */ + nextButtonText?: string; + /** + * The text to be displayed in the previous button. + */ + previousButtonText?: string; + /** + * The steps to be displayed in the carousel. + */ + steps: CarouselStep[]; + /** + * The title of the carousel. + */ + title?: string; +} + +const COMPONENT_NAME: string = 'Carousel'; + +const Carousel: FC & WithWrapperProps = (props: CarouselProps): ReactElement => { + const {autoPlay, autoPlayInterval, className, nextButtonText, previousButtonText, steps, title, ...rest} = props; + const [currentStep, setCurrentStep] = useState(1); + + const isLastStep: boolean = useMemo(() => currentStep === steps.length - 1, [steps, currentStep]); + const isFirstStep: boolean = useMemo(() => currentStep === 0, [currentStep]); + + const classes: string = clsx('oxygen-carousel', className); + + useEffect(() => { + if (!autoPlay) { + return () => {}; + } + + const interval: NodeJS.Timer = setInterval(() => { + if (isLastStep) { + setCurrentStep(0); + } else { + setCurrentStep(currentStep + 1); + } + }, autoPlayInterval); + + return () => clearInterval(interval); + }, [autoPlay, autoPlayInterval, currentStep, isLastStep]); + + const handleNextButtonClick = (): void => { + if (isLastStep) { + return; + } + setCurrentStep(currentStep + 1); + }; + + const handlePreviousButtonClick = (): void => { + if (isFirstStep) { + return; + } + setCurrentStep(currentStep - 1); + }; + + const generateCarouselSteps = (): ReactElement[] => + steps.map((step: CarouselStep) => ( + + + + {step.illustration} + + + {step.title} + + + {step.description} + + + + + + + + + )); + + return ( + + + {title && {title}} + + + + + + + + + + ); +}; + +Carousel.displayName = composeComponentDisplayName(COMPONENT_NAME); +Carousel.muiName = COMPONENT_NAME; +Carousel.defaultProps = { + autoPlay: false, + autoPlayInterval: 5000, + nextButtonText: 'Next', + previousButtonText: 'Previous', +}; + +export default Carousel; diff --git a/packages/react/src/components/Carousel/carousel.scss b/packages/react/src/components/Carousel/carousel.scss new file mode 100644 index 00000000..0727c934 --- /dev/null +++ b/packages/react/src/components/Carousel/carousel.scss @@ -0,0 +1,53 @@ +/** + * 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-carousel { + .oxygen-carousel-top-bar { + display: flex; + justify-content: space-between; + + .oxygen-carousel-title { + flex-grow: 1; + align-items: center; + display: flex; + } + + .oxygen-carousel-button-group { + flex-grow: 0; + display: flex; + gap: var(--oxygen-customComponents-Stepper-properties-button-gap); + justify-content: flex-end; + } + } + + .oxygen-carousel-step-card { + padding: 0; + } + + .oxygen-carousel-step-card-content { + display: flex; + justify-content: flex-start; + gap: 1.5em; + } + + .oxygen-carousel-step-button-bar { + display: flex; + justify-content: flex-end; + padding-top: 1em; + } +} diff --git a/packages/react/src/components/Carousel/index.ts b/packages/react/src/components/Carousel/index.ts new file mode 100644 index 00000000..ffd62680 --- /dev/null +++ b/packages/react/src/components/Carousel/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 Carousel} from './Carousel'; +export type {CarouselProps} from './Carousel'; diff --git a/packages/react/src/theme/default-theme.ts b/packages/react/src/theme/default-theme.ts index da2b123d..25687ad7 100644 --- a/packages/react/src/theme/default-theme.ts +++ b/packages/react/src/theme/default-theme.ts @@ -99,6 +99,7 @@ export const generateDefaultThemeOptions = (baseTheme: Theme): RecursivePartial< MuiCard: { styleOverrides: { root: { + borderColor: '#d7d9e3', padding: baseTheme.spacing(3), }, },