From 9f39a3364b47478b6e3a6bac62844f6e32774431 Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Fri, 13 Sep 2024 15:48:51 +0530 Subject: [PATCH] feat: add stagger component --- packages/blade/.storybook/react/preview.tsx | 22 +++-- .../src/components/BaseMotion/BaseMotion.tsx | 47 ++++++++-- .../blade/src/components/BaseMotion/types.ts | 6 +- .../src/components/Card/Card.stories.tsx | 2 +- .../src/components/Fade/Fade.stories.tsx | 2 +- .../blade/src/components/Fade/Fade.web.tsx | 4 +- .../blade/src/components/Move/Move.web.tsx | 21 +++-- .../components/Stagger/Stagger.stories.tsx | 88 +++++++++++++++++++ .../src/components/Stagger/Stagger.web.tsx | 36 ++++++++ .../components/Stagger/StaggerProvider.tsx | 10 +++ .../blade/src/components/Stagger/index.ts | 2 + 11 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 packages/blade/src/components/Stagger/Stagger.stories.tsx create mode 100644 packages/blade/src/components/Stagger/Stagger.web.tsx create mode 100644 packages/blade/src/components/Stagger/StaggerProvider.tsx create mode 100644 packages/blade/src/components/Stagger/index.ts diff --git a/packages/blade/.storybook/react/preview.tsx b/packages/blade/.storybook/react/preview.tsx index 0bb9dcf89d7..dd78c1beeed 100644 --- a/packages/blade/.storybook/react/preview.tsx +++ b/packages/blade/.storybook/react/preview.tsx @@ -11,6 +11,7 @@ import { DocsContainer } from '@storybook/addon-docs'; import React from 'react'; import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; import './global.css'; +import { domAnimation, LazyMotion } from 'framer-motion'; export const parameters = { // disable snapshot by default and then enable it only for kitchen sink @@ -180,15 +181,18 @@ export const decorators = [ return ( - - - - - + {/* strict in LazyMotion will make sure we don't use excessive `motion` component in blade components and instead use light weight `m` */} + + + + + + + ); }, diff --git a/packages/blade/src/components/BaseMotion/BaseMotion.tsx b/packages/blade/src/components/BaseMotion/BaseMotion.tsx index 3a534022cf4..716b6eec558 100644 --- a/packages/blade/src/components/BaseMotion/BaseMotion.tsx +++ b/packages/blade/src/components/BaseMotion/BaseMotion.tsx @@ -1,25 +1,52 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import BaseBox from '~components/Box/BaseBox'; +import { AnimatePresence, m as motion } from 'framer-motion'; +import React from 'react'; +import styled from 'styled-components'; +import { useStagger } from '~components/Stagger/StaggerProvider'; import type { BaseMotionProps } from './types'; -const MotionBox = motion(BaseBox); +// Creating empty styled component so that the final component supports `as` prop +const StyledDiv = styled.div``; +const MotionDiv = motion(StyledDiv); -export const BaseMotionBox = ({ +const useAnimationVariables = ({ + variant, + isInsideStaggerContainer, +}: { + variant: BaseMotionProps['variant']; + isInsideStaggerContainer: boolean; +}) => { + const animationVariables = React.useMemo(() => { + // When component is rendered inside stagger, we remove the initial, animate, exit props + // otherwise they override the stagger behaviour and stagger does not work + return isInsideStaggerContainer + ? {} + : { + initial: variant === 'in' || variant === 'inout' ? 'initial' : undefined, + animate: 'animate', + exit: variant === 'out' || variant === 'inout' ? 'exit' : undefined, + }; + }, [variant, isInsideStaggerContainer]); + + return animationVariables; +}; + +const BaseMotionBox = ({ children, motionVariants, isVisible = true, - variant, + variant = 'inout', }: BaseMotionProps) => { + const { isInsideStaggerContainer } = useStagger(); + const animationVariables = useAnimationVariables({ variant, isInsideStaggerContainer }); + return ( {isVisible ? ( - @@ -27,3 +54,5 @@ export const BaseMotionBox = ({ ); }; + +export { MotionDiv, BaseMotionBox }; diff --git a/packages/blade/src/components/BaseMotion/types.ts b/packages/blade/src/components/BaseMotion/types.ts index aeac336c201..98fd094a16b 100644 --- a/packages/blade/src/components/BaseMotion/types.ts +++ b/packages/blade/src/components/BaseMotion/types.ts @@ -7,14 +7,14 @@ type BaseEntryExitMotionProps = { variant?: 'in' | 'out' | 'inout'; }; -type BaseMotionBoxVariants = { +type MotionVariantsType = { initial: Variant; animate: Variant; exit: Variant; }; type BaseMotionProps = { - motionVariants: BaseMotionBoxVariants; + motionVariants: MotionVariantsType; } & Pick; -export type { BaseEntryExitMotionProps, BaseMotionProps, BaseMotionBoxVariants }; +export type { BaseEntryExitMotionProps, BaseMotionProps, MotionVariantsType }; diff --git a/packages/blade/src/components/Card/Card.stories.tsx b/packages/blade/src/components/Card/Card.stories.tsx index 2f4856cbe0a..52728a10dc4 100644 --- a/packages/blade/src/components/Card/Card.stories.tsx +++ b/packages/blade/src/components/Card/Card.stories.tsx @@ -476,7 +476,7 @@ export const InternalCardExample = React.forwardRef((_, ref) => { borderRadius="medium" elevation="lowRaised" padding="spacing.7" - width="500px" + width="300px" marginRight="spacing.6" href="https://razorpay.com" > diff --git a/packages/blade/src/components/Fade/Fade.stories.tsx b/packages/blade/src/components/Fade/Fade.stories.tsx index a9e32381cdd..ea013aead40 100644 --- a/packages/blade/src/components/Fade/Fade.stories.tsx +++ b/packages/blade/src/components/Fade/Fade.stories.tsx @@ -40,7 +40,7 @@ export default { const FadeTemplate: StoryFn = (args) => { const [isVisible, setIsVisible] = React.useState(true); return ( - + diff --git a/packages/blade/src/components/Fade/Fade.web.tsx b/packages/blade/src/components/Fade/Fade.web.tsx index b1ea11a4eee..f76495dadae 100644 --- a/packages/blade/src/components/Fade/Fade.web.tsx +++ b/packages/blade/src/components/Fade/Fade.web.tsx @@ -1,10 +1,10 @@ import { BaseMotionBox } from '~components/BaseMotion'; -import type { BaseEntryExitMotionProps, BaseMotionBoxVariants } from '~components/BaseMotion'; +import type { BaseEntryExitMotionProps, MotionVariantsType } from '~components/BaseMotion'; export type FadeProps = BaseEntryExitMotionProps; export const Fade = ({ children, isVisible, variant = 'inout' }: FadeProps) => { - const fadeVariants: BaseMotionBoxVariants = { + const fadeVariants: MotionVariantsType = { initial: { opacity: 0, }, diff --git a/packages/blade/src/components/Move/Move.web.tsx b/packages/blade/src/components/Move/Move.web.tsx index 97df1be4f3f..170ba1b9c7c 100644 --- a/packages/blade/src/components/Move/Move.web.tsx +++ b/packages/blade/src/components/Move/Move.web.tsx @@ -1,23 +1,30 @@ import { BaseMotionBox } from '~components/BaseMotion'; -import type { BaseEntryExitMotionProps, BaseMotionBoxVariants } from '~components/BaseMotion'; +import type { BaseEntryExitMotionProps, MotionVariantsType } from '~components/BaseMotion'; export type MoveProps = BaseEntryExitMotionProps; -export const Move = ({ children }: MoveProps) => { - const moveVariants: BaseMotionBoxVariants = { +export const Move = ({ children, variant = 'inout', isVisible }: MoveProps) => { + const moveVariants: MotionVariantsType = { initial: { opacity: 0, - y: '-10px', + transform: 'translateY(20px)', }, animate: { opacity: 1, - y: '0px', + transform: 'translateY(0px)', }, exit: { opacity: 0, - y: '-10px', + transform: 'translateY(20px)', }, }; - return ; + return ( + + ); }; diff --git a/packages/blade/src/components/Stagger/Stagger.stories.tsx b/packages/blade/src/components/Stagger/Stagger.stories.tsx new file mode 100644 index 00000000000..7acfd1eb13f --- /dev/null +++ b/packages/blade/src/components/Stagger/Stagger.stories.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import type { Meta, StoryFn } from '@storybook/react'; +import { Title } from '@storybook/addon-docs'; +import { Stagger } from './'; +import type { StaggerProps } from './'; +import { Sandbox } from '~utils/storybook/Sandbox'; +import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; +import { Button } from '~components/Button'; +import { Box } from '~components/Box'; +import { InternalCardExample } from '../Card/Card.stories'; +import { Fade } from '~components/Fade'; +import { Move } from '~components/Move'; + +const Page = (): React.ReactElement => { + return ( + + Usage + + {` + const todo = 'todo'; + `} + + + ); +}; + +export default { + title: 'Motion/Stagger', + component: Stagger, + tags: ['autodocs'], + parameters: { + docs: { + page: Page, + }, + }, +} as Meta; + +const StaggerTemplate: StoryFn = (args) => { + const [isVisible, setIsVisible] = React.useState(true); + return ( + + + + {args.children} + + + ); +}; + +export const Default = StaggerTemplate.bind({}); +Default.args = { + children: ( + + + + + + + + + + + + ), +}; + +export const MoveStagger = StaggerTemplate.bind({}); +MoveStagger.args = { + children: ( + + + + + + + + + + + + ), +}; diff --git a/packages/blade/src/components/Stagger/Stagger.web.tsx b/packages/blade/src/components/Stagger/Stagger.web.tsx new file mode 100644 index 00000000000..16e3a816293 --- /dev/null +++ b/packages/blade/src/components/Stagger/Stagger.web.tsx @@ -0,0 +1,36 @@ +import { MotionDiv } from '~components/BaseMotion'; +import type { BaseEntryExitMotionProps, MotionVariantsType } from '~components/BaseMotion'; +import { AnimatePresence } from 'framer-motion'; +import { StaggerContext } from './StaggerProvider'; + +export type StaggerProps = BaseEntryExitMotionProps & { + children: React.ReactElement[] | React.ReactElement; +}; + +export const Stagger = ({ children, isVisible, variant = 'inout' }: StaggerProps) => { + const staggerVariants: MotionVariantsType = { + initial: {}, + animate: { + transition: { + staggerChildren: 0.1, + }, + }, + exit: { + transition: { + staggerChildren: 0.1, + }, + }, + }; + + return ( + + + {isVisible ? ( + + {children} + + ) : null} + + + ); +}; diff --git a/packages/blade/src/components/Stagger/StaggerProvider.tsx b/packages/blade/src/components/Stagger/StaggerProvider.tsx new file mode 100644 index 00000000000..fc89c5d4799 --- /dev/null +++ b/packages/blade/src/components/Stagger/StaggerProvider.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +const StaggerContext = React.createContext({ isInsideStaggerContainer: false }); + +const useStagger = () => { + const staggerContextValue = React.useContext(StaggerContext); + return staggerContextValue; +}; + +export { useStagger, StaggerContext }; diff --git a/packages/blade/src/components/Stagger/index.ts b/packages/blade/src/components/Stagger/index.ts new file mode 100644 index 00000000000..0e8ca7e8417 --- /dev/null +++ b/packages/blade/src/components/Stagger/index.ts @@ -0,0 +1,2 @@ +export { Stagger } from './Stagger'; +export type { StaggerProps } from './Stagger';