Skip to content

Commit

Permalink
feat: add stagger component
Browse files Browse the repository at this point in the history
  • Loading branch information
saurabhdaware committed Sep 13, 2024
1 parent 6cf035f commit 9f39a33
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 32 deletions.
22 changes: 13 additions & 9 deletions packages/blade/.storybook/react/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -180,15 +181,18 @@ export const decorators = [
return (
<ErrorBoundary>
<GlobalStyle />
<BladeProvider
key={`${context.globals.themeTokenName}-${context.globals.colorScheme}`}
themeTokens={getThemeTokens()}
colorScheme={context.globals.colorScheme}
>
<StoryCanvas context={context}>
<Story />
</StoryCanvas>
</BladeProvider>
{/* strict in LazyMotion will make sure we don't use excessive `motion` component in blade components and instead use light weight `m` */}
<LazyMotion strict features={domAnimation}>
<BladeProvider
key={`${context.globals.themeTokenName}-${context.globals.colorScheme}`}
themeTokens={getThemeTokens()}
colorScheme={context.globals.colorScheme}
>
<StoryCanvas context={context}>
<Story />
</StoryCanvas>
</BladeProvider>
</LazyMotion>
</ErrorBoundary>
);
},
Expand Down
47 changes: 38 additions & 9 deletions packages/blade/src/components/BaseMotion/BaseMotion.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
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 (
<AnimatePresence>
{isVisible ? (
<MotionBox
<MotionDiv
// kinda hack to build it as enhancer component
as={children.type}
variants={motionVariants}
initial={variant === 'in' || variant === 'inout' ? 'initial' : undefined}
animate="animate"
exit={variant === 'out' || variant === 'inout' ? 'exit' : undefined}
{...animationVariables}
// We pass the props of children and not pass the children itself since the `as` prop already renders the children and we don't want to re-render it inside
{...children.props}
/>
) : null}
</AnimatePresence>
);
};

export { MotionDiv, BaseMotionBox };
6 changes: 3 additions & 3 deletions packages/blade/src/components/BaseMotion/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseEntryExitMotionProps, 'children' | 'isVisible' | 'variant'>;

export type { BaseEntryExitMotionProps, BaseMotionProps, BaseMotionBoxVariants };
export type { BaseEntryExitMotionProps, BaseMotionProps, MotionVariantsType };
2 changes: 1 addition & 1 deletion packages/blade/src/components/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
Expand Down
2 changes: 1 addition & 1 deletion packages/blade/src/components/Fade/Fade.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default {
const FadeTemplate: StoryFn<typeof Fade> = (args) => {
const [isVisible, setIsVisible] = React.useState(true);
return (
<Box>
<Box minHeight="350px">
<Button marginBottom="spacing.4" onClick={() => setIsVisible(!isVisible)}>
Toggle Fade
</Button>
Expand Down
4 changes: 2 additions & 2 deletions packages/blade/src/components/Fade/Fade.web.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
Expand Down
21 changes: 14 additions & 7 deletions packages/blade/src/components/Move/Move.web.tsx
Original file line number Diff line number Diff line change
@@ -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 <BaseMotionBox motionVariants={moveVariants} children={children} />;
return (
<BaseMotionBox
motionVariants={moveVariants}
children={children}
variant={variant}
isVisible={isVisible}
/>
);
};
88 changes: 88 additions & 0 deletions packages/blade/src/components/Stagger/Stagger.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<StoryPageWrapper
componentName="Stagger"
componentDescription="Stagger Motion Component (TODO)"
figmaURL="https://www.figma.com/proto/jubmQL9Z8V7881ayUD95ps/Blade-DSL?type=design&node-id=74864-85897&t=CvaYT53LNc4OYVKa-1&scaling=min-zoom&page-id=21689%3A381614&mode=design"
>
<Title>Usage</Title>
<Sandbox>
{`
const todo = 'todo';
`}
</Sandbox>
</StoryPageWrapper>
);
};

export default {
title: 'Motion/Stagger',
component: Stagger,
tags: ['autodocs'],
parameters: {
docs: {
page: Page,
},
},
} as Meta<StaggerProps>;

const StaggerTemplate: StoryFn<typeof Stagger> = (args) => {
const [isVisible, setIsVisible] = React.useState(true);
return (
<Box minHeight="350px">
<Button marginBottom="spacing.4" onClick={() => setIsVisible(!isVisible)}>
Toggle Stagger
</Button>
<Stagger {...args} isVisible={isVisible}>
{args.children}
</Stagger>
</Box>
);
};

export const Default = StaggerTemplate.bind({});
Default.args = {
children: (
<Box display="flex" flexDirection="row" gap="spacing.4">
<Fade>
<InternalCardExample />
</Fade>
<Fade>
<InternalCardExample />
</Fade>
<Fade>
<InternalCardExample />
</Fade>
</Box>
),
};

export const MoveStagger = StaggerTemplate.bind({});
MoveStagger.args = {
children: (
<Box display="flex" flexDirection="row" gap="spacing.4">
<Move>
<InternalCardExample />
</Move>
<Move>
<InternalCardExample />
</Move>
<Move>
<InternalCardExample />
</Move>
</Box>
),
};
36 changes: 36 additions & 0 deletions packages/blade/src/components/Stagger/Stagger.web.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<StaggerContext.Provider value={{ isInsideStaggerContainer: true }}>
<AnimatePresence>
{isVisible ? (
<MotionDiv initial="initial" animate="animate" exit="exit" variants={staggerVariants}>
{children}
</MotionDiv>
) : null}
</AnimatePresence>
</StaggerContext.Provider>
);
};
10 changes: 10 additions & 0 deletions packages/blade/src/components/Stagger/StaggerProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 2 additions & 0 deletions packages/blade/src/components/Stagger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Stagger } from './Stagger';
export type { StaggerProps } from './Stagger';

0 comments on commit 9f39a33

Please sign in to comment.