diff --git a/packages/blade/.storybook/react/preview.tsx b/packages/blade/.storybook/react/preview.tsx
index a0336b6921e..1b0f6836183 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 { domMax, 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/package.json b/packages/blade/package.json
index 7042dd38589..c02a7feebdf 100644
--- a/packages/blade/package.json
+++ b/packages/blade/package.json
@@ -239,6 +239,7 @@
"fastify": "4.28.1",
"figures": "3.2.0",
"flat": "5.0.2",
+ "motion": "11.12.0",
"globby": "14.0.1",
"ismobilejs": "1.1.1",
"jest": "29.6.1",
@@ -292,6 +293,7 @@
"react": ">=18",
"react-dom": ">=18",
"styled-components": "^5",
+ "framer-motion": ">=4",
"react-native": "^0.72",
"@floating-ui/react-native": "^0.10.0",
"react-native-reanimated": "^3.4.1",
diff --git a/packages/blade/src/components/Accordion/Accordion.tsx b/packages/blade/src/components/Accordion/Accordion.tsx
index 9c6cfcd065a..16f48851856 100644
--- a/packages/blade/src/components/Accordion/Accordion.tsx
+++ b/packages/blade/src/components/Accordion/Accordion.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import type { ReactElement } from 'react';
import { useCallback, useMemo, useState, cloneElement, Children } from 'react';
import type { AccordionContextState } from './AccordionContext';
@@ -10,6 +11,7 @@ import type { BoxProps } from '~components/Box';
import { size as sizeTokens } from '~tokens/global';
import { makeSize } from '~utils';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
const MIN_WIDTH: BoxProps['minWidth'] = {
s: makeSize(sizeTokens[200]),
@@ -61,18 +63,21 @@ const getVariantStyles = (variant: AccordionProps['variant']): BoxProps => {
* Checkout https://blade.razorpay.com/?path=/docs/components-accordion--docs
*
*/
-const Accordion = ({
- defaultExpandedIndex,
- expandedIndex,
- onExpandChange,
- showNumberPrefix = false,
- children,
- variant = 'transparent',
- size = 'large',
- maxWidth,
- testID,
- ...styledProps
-}: AccordionProps): ReactElement => {
+const _Accordion = (
+ {
+ defaultExpandedIndex,
+ expandedIndex,
+ onExpandChange,
+ showNumberPrefix = false,
+ children,
+ variant = 'transparent',
+ size = 'large',
+ maxWidth,
+ testID,
+ ...styledProps
+ }: AccordionProps,
+ ref: React.Ref,
+): ReactElement => {
const [expandedAccordionItemIndex, setExpandedAccordionItemIndex] = useState(
defaultExpandedIndex,
);
@@ -118,6 +123,7 @@ const Accordion = ({
return (
@@ -136,4 +142,6 @@ const Accordion = ({
);
};
+const Accordion = React.forwardRef(_Accordion);
+
export { Accordion };
diff --git a/packages/blade/src/components/Alert/Alert.tsx b/packages/blade/src/components/Alert/Alert.tsx
index 1eb0d4ea530..388ae15e4ed 100644
--- a/packages/blade/src/components/Alert/Alert.tsx
+++ b/packages/blade/src/components/Alert/Alert.tsx
@@ -1,5 +1,5 @@
import type { ReactChild, ReactElement } from 'react';
-import { Fragment, useState } from 'react';
+import React, { Fragment, useState, forwardRef } from 'react';
import { StyledAlert } from './StyledAlert';
import type { IconComponent } from '~components/Icons';
@@ -21,7 +21,7 @@ import BaseButton from '~components/Button/BaseButton';
import { BaseLink } from '~components/Link/BaseLink';
import type { FeedbackColors, SubtleOrIntense } from '~tokens/theme/theme';
import { useTheme } from '~components/BladeProvider';
-import type { DotNotationSpacingStringToken, TestID } from '~utils/types';
+import type { BladeElementRef, DotNotationSpacingStringToken, TestID } from '~utils/types';
import { makeAccessible } from '~utils/makeAccessible';
type PrimaryAction = {
@@ -124,19 +124,22 @@ const intentIconMap = {
notice: AlertTriangleIcon,
};
-const Alert = ({
- description,
- title,
- isDismissible = true,
- onDismiss,
- emphasis = 'subtle',
- isFullWidth = false,
- color = 'neutral',
- actions,
- testID,
- icon,
- ...styledProps
-}: AlertProps): ReactElement | null => {
+const _Alert = (
+ {
+ description,
+ title,
+ isDismissible = true,
+ onDismiss,
+ emphasis = 'subtle',
+ isFullWidth = false,
+ color = 'neutral',
+ actions,
+ testID,
+ icon,
+ ...styledProps
+ }: AlertProps,
+ ref: React.Ref,
+): ReactElement | null => {
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint({ breakpoints: theme.breakpoints });
const [isVisible, setIsVisible] = useState(true);
@@ -298,6 +301,7 @@ const Alert = ({
return (
{
+const _Amount = (
+ {
+ value,
+ suffix = 'decimals',
+ type = 'body',
+ size = 'medium',
+ weight = 'regular',
+ isAffixSubtle = true,
+ isStrikethrough = false,
+ color,
+ currencyIndicator = 'currency-symbol',
+ currency = 'INR',
+ testID,
+ ...styledProps
+ }: AmountProps,
+ ref: React.Ref,
+): ReactElement => {
if (__DEV__) {
if (typeof value !== 'number') {
throwBladeError({
@@ -326,6 +329,7 @@ const _Amount = ({
return (
{
+ throwBladeError({
+ message: 'AnimateInteractions is not yet implemented for native',
+ moduleName: 'AnimateInteractions',
+ });
+
+ return AnimateInteractions Component is not available for Native mobile apps.;
+};
+
+export { AnimateInteractions };
diff --git a/packages/blade/src/components/AnimateInteractions/AnimateInteractions.stories.tsx b/packages/blade/src/components/AnimateInteractions/AnimateInteractions.stories.tsx
new file mode 100644
index 00000000000..e5cbcb4d771
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/AnimateInteractions.stories.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { AnimateInteractions } from './';
+import type { AnimateInteractionsProps } from './';
+import { Sandbox } from '~utils/storybook/Sandbox';
+import StoryPageWrapper from '~utils/storybook/StoryPageWrapper';
+import { Button } from '~components/Button';
+import { Box } from '~components/Box';
+import { Fade } from '~components/Fade';
+import { Move } from '~components/Move';
+import { Text } from '~components/Typography';
+import { Card, CardBody } from '~components/Card';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/AnimateInteractions',
+ component: AnimateInteractions,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const AnimateInteractionsTemplate: StoryFn = (args) => {
+ return (
+
+
+ {args.children}
+
+
+ );
+};
+
+export const Default = AnimateInteractionsTemplate.bind({});
+Default.args = {
+ children: (
+
+ Hover me for magic
+
+ I appear depending on interaction on parent container
+
+
+ ),
+};
+
+export const ScaleChildOnCardHover = AnimateInteractionsTemplate.bind({});
+ScaleChildOnCardHover.args = {
+ children: (
+
+
+
+
+
+ Hi I am text inside card. Hover over this card to see magic
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/blade/src/components/AnimateInteractions/AnimateInteractions.web.tsx b/packages/blade/src/components/AnimateInteractions/AnimateInteractions.web.tsx
new file mode 100644
index 00000000000..87ef5d5a201
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/AnimateInteractions.web.tsx
@@ -0,0 +1,31 @@
+import { BaseMotionEnhancerBox } from '~components/BaseMotion';
+import { AnimateInteractionsContext } from './AnimateInteractionsProvider';
+import { useFocusWithin } from './useFocusWithin';
+import React from 'react';
+import { useAnimation } from 'framer-motion';
+import type { AnimateInteractionsProps } from './types';
+
+export const AnimateInteractions = ({
+ children,
+ motionTriggers = ['hover'],
+}: AnimateInteractionsProps) => {
+ const baseMotionRef = React.useRef(null);
+ const controls = useAnimation();
+
+ useFocusWithin(baseMotionRef, {
+ onFocusWithin: () => {
+ controls.start('animate');
+ },
+ onBlurWithin: () => {
+ controls.start('exit');
+ },
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/blade/src/components/AnimateInteractions/AnimateInteractionsProvider.tsx b/packages/blade/src/components/AnimateInteractions/AnimateInteractionsProvider.tsx
new file mode 100644
index 00000000000..0dc6d26f9d8
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/AnimateInteractionsProvider.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+const AnimateInteractionsContext = React.createContext({
+ isInsideAnimateInteractionsContainer: false,
+});
+
+const useAnimateInteractions = () => {
+ const animateInteractionsContextValue = React.useContext(AnimateInteractionsContext);
+ return animateInteractionsContextValue;
+};
+
+export { useAnimateInteractions, AnimateInteractionsContext };
diff --git a/packages/blade/src/components/AnimateInteractions/index.ts b/packages/blade/src/components/AnimateInteractions/index.ts
new file mode 100644
index 00000000000..378a7a93219
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/index.ts
@@ -0,0 +1,2 @@
+export { AnimateInteractions } from './AnimateInteractions';
+export type { AnimateInteractionsProps } from './types';
diff --git a/packages/blade/src/components/AnimateInteractions/types.ts b/packages/blade/src/components/AnimateInteractions/types.ts
new file mode 100644
index 00000000000..2af742e13ea
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/types.ts
@@ -0,0 +1,6 @@
+import type { BaseMotionBoxProps } from '~components/BaseMotion';
+
+export type AnimateInteractionsProps = {
+ children: React.ReactElement;
+ motionTriggers?: BaseMotionBoxProps['motionTriggers'];
+};
diff --git a/packages/blade/src/components/AnimateInteractions/useFocusWithin.ts b/packages/blade/src/components/AnimateInteractions/useFocusWithin.ts
new file mode 100644
index 00000000000..02cf5e2c5b5
--- /dev/null
+++ b/packages/blade/src/components/AnimateInteractions/useFocusWithin.ts
@@ -0,0 +1,35 @@
+import { useEffect } from 'react';
+
+type FocusWithinHandlers = {
+ onFocusWithin?: () => void;
+ onBlurWithin?: () => void;
+};
+
+export function useFocusWithin(
+ ref: React.RefObject,
+ { onFocusWithin, onBlurWithin }: FocusWithinHandlers,
+) {
+ useEffect(() => {
+ const element = ref.current;
+ if (!element) return;
+
+ const handleFocusIn = () => {
+ onFocusWithin?.();
+ };
+
+ const handleFocusOut = (event: FocusEvent) => {
+ // Ensure that focus is not still within the container
+ if (element && !element.contains(event.relatedTarget as Node)) {
+ onBlurWithin?.();
+ }
+ };
+
+ element.addEventListener('focusin', handleFocusIn);
+ element.addEventListener('focusout', handleFocusOut);
+
+ return () => {
+ element.removeEventListener('focusin', handleFocusIn);
+ element.removeEventListener('focusout', handleFocusOut);
+ };
+ }, [ref, onFocusWithin, onBlurWithin]);
+}
diff --git a/packages/blade/src/components/Badge/Badge.tsx b/packages/blade/src/components/Badge/Badge.tsx
index baa2763afa6..046bf90cc7f 100644
--- a/packages/blade/src/components/Badge/Badge.tsx
+++ b/packages/blade/src/components/Badge/Badge.tsx
@@ -10,11 +10,12 @@ import { Text } from '~components/Typography';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
-import type { StringChildrenType, TestID } from '~utils/types';
+import type { BladeElementRef, StringChildrenType, TestID } from '~utils/types';
import { getStringFromReactText } from '~src/utils/getStringChildren';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { isReactNative, makeSize } from '~utils';
import { throwBladeError } from '~utils/logger';
+import React from 'react';
type BadgeProps = {
/**
@@ -87,15 +88,18 @@ const getColorProps = ({
return props;
};
-const _Badge = ({
- children,
- emphasis = 'subtle',
- icon: Icon,
- size = 'medium',
- color = 'neutral',
- testID,
- ...styledProps
-}: BadgeProps): ReactElement => {
+const _Badge = (
+ {
+ children,
+ emphasis = 'subtle',
+ icon: Icon,
+ size = 'medium',
+ color = 'neutral',
+ testID,
+ ...styledProps
+ }: BadgeProps,
+ ref: React.Ref,
+): ReactElement => {
const childrenString = getStringFromReactText(children);
if (__DEV__) {
if (!childrenString?.trim()) {
@@ -128,6 +132,7 @@ const _Badge = ({
return (
,
+) => {
+ const { isInsideAnimateInteractionsContainer } = useAnimateInteractions();
+ const { isInsideStaggerContainer, staggerType } = useStagger();
+ const shouldSkipAnimationVariables =
+ (isInsideAnimateInteractionsContainer && motionTriggers.includes('on-animate-interactions')) ||
+ isInsideStaggerContainer;
+
+ const animationVariables = shouldSkipAnimationVariables
+ ? {}
+ : makeAnimationVariables(motionTriggers, {
+ animateVisibility,
+ });
+
+ const motionVariants = useMotionVariants(
+ userMotionVariants,
+ isInsideStaggerContainer ? staggerType : type,
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+/**
+ * Base motion component that handles animation variables, reduced motion, type and motionTriggers prop, etc
+ */
+const BaseMotionBox = React.forwardRef(_BaseMotionBox);
+
+const _BaseMotionEnhancerBox: React.ForwardRefRenderFunction = (
+ { children, ...motionBoxArgs },
+ ref,
+) => {
+ return (
+
+ );
+};
+
+/**
+ * Used in AnimateInteraction, Scale, etc
+ *
+ * Enhances the child to add motion support
+ */
+const BaseMotionEnhancerBox = React.forwardRef(_BaseMotionEnhancerBox);
+
+/**
+ * Base component for entry / exit animations
+ *
+ * Handles states, entry exit controls, animation variables, mount / unmount, etc
+ */
+const BaseMotionEntryExit = ({
+ children,
+ motionVariants,
+ isVisible = true,
+ type = 'inout',
+ motionTriggers = ['mount'],
+ shouldUnmountWhenHidden = false,
+}: BaseMotionEntryExitProps) => {
+ // Only need AnimatePresence when we have to unmount the component
+ const AnimateWrapper = shouldUnmountWhenHidden ? AnimatePresence : React.Fragment;
+ // keep it always mounted when shouldUnmountWhenHidden is false
+ const isMounted = shouldUnmountWhenHidden ? isVisible : true;
+
+ const motionMeta: MotionMeta = React.useMemo(() => {
+ return {
+ isEnhanced: true,
+ // @ts-expect-error: ref does exist on children prop
+ innerRef: children.ref,
+ };
+ // @ts-expect-error: ref does exist on children prop
+ }, [children.ref]);
+
+ return (
+
+ {isMounted ? (
+
+ ) : null}
+
+ );
+};
+
+export { MotionDiv, BaseMotionBox, BaseMotionEnhancerBox, BaseMotionEntryExit };
diff --git a/packages/blade/src/components/BaseMotion/baseMotionUtils.ts b/packages/blade/src/components/BaseMotion/baseMotionUtils.ts
new file mode 100644
index 00000000000..5f3c4f7b890
--- /dev/null
+++ b/packages/blade/src/components/BaseMotion/baseMotionUtils.ts
@@ -0,0 +1,83 @@
+import type { BaseMotionBoxProps, MotionTriggersType, MotionVariantsType } from './types';
+
+// This type is exported in new framer-motion versions but does not exist in earlier versions so adding it manually here
+type AnimationType = 'animate' | 'whileHover' | 'whileInView' | 'whileTap' | 'whileFocus';
+
+const motionTriggersArrayToGesturePropsMap: Record<
+ Exclude,
+ AnimationType
+> = {
+ mount: 'animate',
+ hover: 'whileHover',
+ 'in-view': 'whileInView',
+ tap: 'whileTap',
+ focus: 'whileFocus',
+};
+
+type AnimationVariablesType = Partial<
+ Record, keyof MotionVariantsType>
+> & {
+ animate?: BaseMotionBoxProps['animateVisibility'] | BaseMotionBoxProps['animate'];
+};
+
+const makeAnimationVariables = (
+ motionTriggers: MotionTriggersType[],
+ { animateVisibility }: { animateVisibility: BaseMotionBoxProps['animateVisibility'] },
+) => {
+ const interactionVariables = motionTriggers.reduce(
+ (prevProps, currentTrigger) => {
+ if (currentTrigger === 'on-animate-interactions') {
+ return prevProps;
+ }
+
+ // Sometimes animations are conditional. In those cases we use those conditional values in animate
+ if (currentTrigger === 'mount' && animateVisibility) {
+ prevProps.animate = animateVisibility;
+ return prevProps;
+ }
+
+ prevProps[motionTriggersArrayToGesturePropsMap[currentTrigger]] = 'animate';
+ return prevProps;
+ },
+ {},
+ );
+
+ return { initial: 'initial', exit: 'exit', ...interactionVariables };
+};
+
+const useMotionVariants = (
+ motionVariants: BaseMotionBoxProps['motionVariants'],
+ type: BaseMotionBoxProps['type'],
+): BaseMotionBoxProps['motionVariants'] => {
+ if (!motionVariants) {
+ return undefined;
+ }
+
+ const shouldSkipEntryAnimation = type === 'out';
+ const shouldSkipExitAnimation = type === 'in';
+
+ // We override durations to stop animations but still continue with the expected position changes
+ const newMotionVariants: BaseMotionBoxProps['motionVariants'] = {
+ initial: {
+ ...motionVariants.initial,
+ },
+ animate: {
+ ...motionVariants.animate,
+ transition: {
+ ...motionVariants.animate?.transition,
+ duration: shouldSkipEntryAnimation ? 0.0001 : motionVariants.animate?.transition?.duration,
+ },
+ },
+ exit: {
+ ...motionVariants.exit,
+ transition: {
+ ...motionVariants.exit.transition,
+ duration: shouldSkipExitAnimation ? 0.0001 : motionVariants.exit.transition?.duration,
+ },
+ },
+ };
+
+ return newMotionVariants;
+};
+
+export { makeAnimationVariables, useMotionVariants };
diff --git a/packages/blade/src/components/BaseMotion/index.ts b/packages/blade/src/components/BaseMotion/index.ts
new file mode 100644
index 00000000000..a3bfa761445
--- /dev/null
+++ b/packages/blade/src/components/BaseMotion/index.ts
@@ -0,0 +1,2 @@
+export * from './BaseMotion';
+export * from './types';
diff --git a/packages/blade/src/components/BaseMotion/types.ts b/packages/blade/src/components/BaseMotion/types.ts
new file mode 100644
index 00000000000..1aa3ca228db
--- /dev/null
+++ b/packages/blade/src/components/BaseMotion/types.ts
@@ -0,0 +1,89 @@
+import type { AnimationControls, TargetAndTransition, Tween } from 'framer-motion';
+import type React from 'react';
+import type { Delay } from '~tokens/global/motion';
+
+type MotionTriggerEntryExitType = 'mount' | 'in-view' | 'focus' | 'on-animate-interactions';
+type MotionTriggersType = MotionTriggerEntryExitType | 'hover' | 'tap';
+
+type MotionVariantsType = {
+ initial: TargetAndTransition & {
+ transition?: Tween;
+ };
+ animate: TargetAndTransition & {
+ transition?: Tween;
+ };
+ exit: TargetAndTransition & {
+ transition?: Tween;
+ };
+};
+
+type MotionDelay = keyof Delay | { enter: keyof Delay; exit: keyof Delay };
+
+type BaseMotionBoxProps = {
+ as?: React.ReactElement;
+ children: React.ReactElement;
+ type?: 'in' | 'out' | 'inout';
+ /**
+ * @default ['mount']
+ */
+ motionTriggers?: MotionTriggersType[];
+ motionVariants?: MotionVariantsType;
+ /**
+ * Option to override the animate
+ *
+ * Useful when you want to control animation custom
+ *
+ * E.g.
+ * ```js
+ * const controls = useAnimationControls();
+ *
+ * animate={controls}
+ * ```
+ */
+ animate?: AnimationControls;
+
+ /**
+ * This is for scenarios where you want to conditionally animate a component instead of it having static defined animation.
+ *
+ * E.g. In scenarios where your motion component is always mounted, you can use this to switch visibility
+ *
+ * ```js
+ * animateVisibility={isVisible ? 'animate' : 'exit'}
+ * ```
+ */
+ animateVisibility?: keyof MotionVariantsType;
+};
+
+type BaseMotionEntryExitProps = Pick & {
+ isVisible?: boolean;
+ motionTriggers?: MotionTriggerEntryExitType[];
+ shouldUnmountWhenHidden?: boolean;
+ delay?: MotionDelay;
+};
+
+type MotionMeta = {
+ innerRef: React.Ref;
+ isEnhanced: boolean;
+};
+
+type MotionMetaProp = {
+ /**
+ * @private
+ *
+ * This prop is internally injected when Motion Preset enhances some component.
+ *
+ * You only need to add this prop to component that requires you to pass ref to some internal component. E.g. in checkbox, we need ref on internal input component but we also need one ref on outer component.
+ * Use this in combination with `getOuterMotionRef` and `getInnerMotionRef` utilities
+ */
+ _motionMeta?: MotionMeta;
+};
+
+export type {
+ BaseMotionEntryExitProps,
+ MotionVariantsType,
+ MotionTriggersType,
+ BaseMotionBoxProps,
+ MotionMeta,
+ MotionMetaProp,
+ MotionDelay,
+};
diff --git a/packages/blade/src/components/BottomNav/BottomNav.web.tsx b/packages/blade/src/components/BottomNav/BottomNav.web.tsx
index ae24e4d620b..42b05cbaf9f 100644
--- a/packages/blade/src/components/BottomNav/BottomNav.web.tsx
+++ b/packages/blade/src/components/BottomNav/BottomNav.web.tsx
@@ -10,6 +10,7 @@ import { getFocusRingStyles } from '~utils/getFocusRingStyles';
import { throwBladeError } from '~utils/logger';
import { makeAccessible } from '~utils/makeAccessible';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
/**
* ### BottomNav component
@@ -51,12 +52,10 @@ import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
* Checkout {@link https://blade.razorpay.com/??path=/docs/components-bottomnav--doc BottomNav Documentation}
*/
-const BottomNav = ({
- children,
- zIndex = componentZIndices.bottomNav,
- testID,
- ...styledProps
-}: BottomNavProps): React.ReactElement => {
+const _BottomNav = (
+ { children, zIndex = componentZIndices.bottomNav, testID, ...styledProps }: BottomNavProps,
+ ref: React.Ref,
+): React.ReactElement => {
if (__DEV__) {
const childrenCount = React.Children.count(children);
if (childrenCount > 5 && childrenCount < 2) {
@@ -69,6 +68,7 @@ const BottomNav = ({
return (
((props) => {
paddingLeft: makeSpace(props.theme.spacing[0]),
paddingRight: makeSpace(props.theme.spacing[0]),
transition: `color ${makeMotionTime(props.theme.motion.duration['2xquick'])} ${
- props.theme.motion.easing.standard.effective
+ props.theme.motion.easing.standard
}`,
'&[aria-current="page"]': {
color: props.theme.colors.interactive.text.primary.subtle,
@@ -161,4 +161,6 @@ const BottomNavItem = ({
);
};
+const BottomNav = React.forwardRef(_BottomNav);
+
export { BottomNav, BottomNavItem };
diff --git a/packages/blade/src/components/BottomSheet/BottomSheet.web.tsx b/packages/blade/src/components/BottomSheet/BottomSheet.web.tsx
index 3404e4a8fc1..aa4e79807b7 100644
--- a/packages/blade/src/components/BottomSheet/BottomSheet.web.tsx
+++ b/packages/blade/src/components/BottomSheet/BottomSheet.web.tsx
@@ -378,7 +378,7 @@ const _BottomSheet = ({
}, [isReady]);
// usePresence hook waits for the animation to finish before unmounting the component
- // It's similar to framer-motions usePresence hook
+ // It's similar to motion/react's usePresence hook
// https://www.framer.com/docs/animate-presence/#usepresence
const { isMounted, isVisible } = usePresence(Boolean(_isOpen), {
transitionDuration: theme.motion.duration.moderate,
diff --git a/packages/blade/src/components/Breadcrumb/Breadcrumb.web.tsx b/packages/blade/src/components/Breadcrumb/Breadcrumb.web.tsx
index 4e3fd89c65f..6551a8bc404 100644
--- a/packages/blade/src/components/Breadcrumb/Breadcrumb.web.tsx
+++ b/packages/blade/src/components/Breadcrumb/Breadcrumb.web.tsx
@@ -7,6 +7,7 @@ import { Text } from '~components/Typography';
import { makeAccessible } from '~utils/makeAccessible';
import { getStyledProps } from '~components/Box/styledProps';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
const Separator = ({
size,
@@ -26,18 +27,22 @@ const Separator = ({
const listStyleNone = { listStyle: 'none' };
-const Breadcrumb = ({
- size = 'medium',
- color = 'primary',
- showLastSeparator = false,
- accessibilityLabel = 'Breadcrumb',
- children,
- ...styledProps
-}: BreadcrumbProps): React.ReactElement => {
+const _Breadcrumb = (
+ {
+ size = 'medium',
+ color = 'primary',
+ showLastSeparator = false,
+ accessibilityLabel = 'Breadcrumb',
+ children,
+ ...styledProps
+ }: BreadcrumbProps,
+ ref: React.Ref,
+): React.ReactElement => {
const contextValue = React.useMemo(() => ({ size, color }), [size, color]);
return (
{
+const _ButtonGroup = (
+ {
+ children,
+ isDisabled = false,
+ size = 'medium',
+ color = 'primary',
+ variant = 'primary',
+ isFullWidth = false,
+ testID,
+ ...styledProps
+ }: ButtonGroupProps,
+ ref: React.Ref,
+): React.ReactElement => {
const contextValue = {
isDisabled,
size,
@@ -72,6 +76,7 @@ const _ButtonGroup = ({
return (
{
+ return (
+
+
+ }
+ subtitle="Share payment link via an email, SMS, messenger, chatbot etc."
+ suffix={}
+ title="Payment Links"
+ />
+ NEW} />
+
+
+
+ Create Razorpay Payments Links and share them with your customers from the Razorpay
+ Dashboard or using APIs and start accepting payments. Check the advantages, payment
+ methods, international currency support and more.
+
+
+
+
+
+
+ );
+});
diff --git a/packages/blade/src/components/Card/Card.tsx b/packages/blade/src/components/Card/Card.tsx
index 61fa914a4db..203198f4ff4 100644
--- a/packages/blade/src/components/Card/Card.tsx
+++ b/packages/blade/src/components/Card/Card.tsx
@@ -10,7 +10,7 @@ import BaseBox from '~components/Box/BaseBox';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import type { Elevation } from '~tokens/global';
import type { BoxProps } from '~components/Box';
@@ -155,28 +155,31 @@ export type CardProps = {
} & TestID &
StyledPropsBlade;
-const Card = ({
- children,
- backgroundColor = 'surface.background.gray.intense',
- borderRadius = 'medium',
- elevation = 'lowRaised',
- testID,
- padding = 'spacing.7',
- width,
- height,
- minHeight,
- minWidth,
- onClick,
- isSelected = false,
- accessibilityLabel,
- shouldScaleOnHover = false,
- onHover,
- href,
- target,
- rel,
- as,
- ...styledProps
-}: CardProps): React.ReactElement => {
+const _Card: React.ForwardRefRenderFunction = (
+ {
+ children,
+ backgroundColor = 'surface.background.gray.intense',
+ borderRadius = 'medium',
+ elevation = 'lowRaised',
+ testID,
+ padding = 'spacing.7',
+ width,
+ height,
+ minHeight,
+ minWidth,
+ onClick,
+ isSelected = false,
+ accessibilityLabel,
+ shouldScaleOnHover = false,
+ onHover,
+ href,
+ target,
+ rel,
+ as,
+ ...styledProps
+ },
+ ref,
+): React.ReactElement => {
const [isFocused, setIsFocused] = React.useState(false);
useVerifyAllowedChildren({
@@ -201,6 +204,7 @@ const Card = ({
(({ isSelected, ...props }) => {
const selectedColor = isSelected
@@ -38,16 +39,10 @@ const openURL = async (href: string): Promise => {
}
};
-const CardRoot = ({
- children,
- onClick,
- isSelected,
- shouldScaleOnHover,
- href,
- as,
- accessibilityLabel,
- ...props
-}: CardRootProps): React.ReactElement => {
+const _CardRoot: React.ForwardRefRenderFunction = (
+ { children, onClick, isSelected, shouldScaleOnHover, href, as, accessibilityLabel, ...props },
+ ref,
+): React.ReactElement => {
const { theme } = useTheme();
const [isPressed, setIsPressed] = React.useState(false);
const duration = castNativeType(makeMotionTime(theme.motion.duration.xquick));
@@ -67,6 +62,7 @@ const CardRoot = ({
if (onClick || shouldScaleOnHover || href) {
return (
(
({ as, theme, isSelected, isFocused, shouldScaleOnHover, isPressed, isMobile }) => {
@@ -52,17 +53,16 @@ const StyledCardRoot = styled(BaseBox) {
+const _CardRoot: React.ForwardRefRenderFunction = (
+ { as, accessibilityLabel, children, ...props },
+ ref,
+): React.ReactElement => {
const isMobile = useIsMobile();
const [isPressed, setIsPressed] = React.useState(false);
return (
{
if (!percentage.endsWith('%')) {
@@ -83,16 +84,19 @@ const Controls = ({
);
};
-const Carousel = ({
- autoPlay,
- showIndicators = true,
- children,
- carouselItemWidth = '100%',
- accessibilityLabel,
- onChange,
- indicatorVariant = 'gray',
- navigationButtonVariant = 'filled',
-}: CarouselProps): React.ReactElement => {
+const _Carousel = (
+ {
+ autoPlay,
+ showIndicators = true,
+ children,
+ carouselItemWidth = '100%',
+ accessibilityLabel,
+ onChange,
+ indicatorVariant = 'gray',
+ navigationButtonVariant = 'filled',
+ }: CarouselProps,
+ ref: React.Ref,
+): React.ReactElement => {
const containerRef = React.useRef(null);
const [activeSlide, setActiveSlide] = React.useState(0);
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
@@ -237,4 +241,6 @@ const Carousel = ({
);
};
+const Carousel = React.forwardRef(_Carousel);
+
export { Carousel };
diff --git a/packages/blade/src/components/Carousel/Carousel.web.tsx b/packages/blade/src/components/Carousel/Carousel.web.tsx
index 96a157f50c4..7a96402c20b 100644
--- a/packages/blade/src/components/Carousel/Carousel.web.tsx
+++ b/packages/blade/src/components/Carousel/Carousel.web.tsx
@@ -29,6 +29,7 @@ import { getStyledProps } from '~components/Box/styledProps';
import { useControllableState } from '~utils/useControllable';
import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect';
import { useDidUpdate } from '~utils/useDidUpdate';
+import type { BladeElementRef } from '~utils/types';
type ControlsProp = Required<
Pick<
@@ -228,25 +229,28 @@ const CarouselBody = React.forwardRef(
},
);
-const Carousel = ({
- autoPlay,
- visibleItems = 1,
- showIndicators = true,
- navigationButtonPosition = 'bottom',
- children,
- shouldAddStartEndSpacing = false,
- carouselItemWidth,
- scrollOverlayColor,
- accessibilityLabel,
- onChange,
- indicatorVariant = 'gray',
- navigationButtonVariant = 'filled',
- carouselItemAlignment = 'start',
- height,
- defaultActiveSlide,
- activeSlide: activeSlideProp,
- ...props
-}: CarouselProps): React.ReactElement => {
+const _Carousel = (
+ {
+ autoPlay,
+ visibleItems = 1,
+ showIndicators = true,
+ navigationButtonPosition = 'bottom',
+ children,
+ shouldAddStartEndSpacing = false,
+ carouselItemWidth,
+ scrollOverlayColor,
+ accessibilityLabel,
+ onChange,
+ indicatorVariant = 'gray',
+ navigationButtonVariant = 'filled',
+ carouselItemAlignment = 'start',
+ height,
+ defaultActiveSlide,
+ activeSlide: activeSlideProp,
+ ...props
+ }: CarouselProps,
+ ref: React.Ref,
+): React.ReactElement => {
const { platform } = useTheme();
const [activeIndicator, setActiveIndicator] = React.useState(0);
const [activeSlide, setActiveSlide] = useControllableState({
@@ -485,6 +489,7 @@ const Carousel = ({
return (
{
@@ -588,4 +593,6 @@ const Carousel = ({
);
};
+const Carousel = React.forwardRef(_Carousel);
+
export { Carousel };
diff --git a/packages/blade/src/components/Checkbox/Checkbox.tsx b/packages/blade/src/components/Checkbox/Checkbox.tsx
index cd3f45389d6..e68b66328c5 100644
--- a/packages/blade/src/components/Checkbox/Checkbox.tsx
+++ b/packages/blade/src/components/Checkbox/Checkbox.tsx
@@ -20,6 +20,8 @@ import type { BladeElementRef, TestID } from '~utils/types';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { throwBladeError } from '~utils/logger';
import { makeSize, useTheme } from '~utils';
+import { getInnerMotionRef, getOuterMotionRef } from '~utils/getMotionRefs';
+import type { MotionMetaProp } from '~components/BaseMotion';
type OnChange = ({
isChecked,
@@ -110,7 +112,8 @@ type CheckboxProps = {
*/
tabIndex?: number;
} & TestID &
- StyledPropsBlade;
+ StyledPropsBlade &
+ MotionMetaProp;
const _Checkbox: React.ForwardRefRenderFunction = (
{
@@ -129,6 +132,7 @@ const _Checkbox: React.ForwardRefRenderFunction
size = 'medium',
tabIndex,
testID,
+ _motionMeta,
...styledProps
},
ref,
@@ -223,6 +227,7 @@ const _Checkbox: React.ForwardRefRenderFunction
return (
@@ -240,7 +245,7 @@ const _Checkbox: React.ForwardRefRenderFunction
hasError={_hasError}
inputProps={inputProps}
tabIndex={tabIndex}
- ref={ref}
+ ref={getInnerMotionRef({ _motionMeta, ref })}
/>
void;
const _Chip: React.ForwardRefRenderFunction = (
- { isDisabled, value, children, icon: Icon, color, testID, ...styledProps },
+ { isDisabled, value, children, icon: Icon, color, testID, _motionMeta, ...styledProps },
ref,
) => {
const { theme } = useTheme();
@@ -143,6 +144,7 @@ const _Chip: React.ForwardRefRenderFunction = (
{...metaAttribute({ name: MetaConstants.Chip, testID })}
{...getStyledProps(styledProps)}
display={(isReactNative() ? 'flex' : 'inline-flex') as never}
+ ref={getOuterMotionRef({ _motionMeta, ref })}
>
= (
isDisabled={_isDisabled}
inputProps={inputProps}
hasError={hasError}
- ref={ref}
+ ref={getInnerMotionRef({ _motionMeta, ref })}
/>
{
+const _ChipGroup = (
+ {
+ accessibilityLabel,
+ label,
+ labelPosition = 'top',
+ necessityIndicator = 'none',
+ validationState = 'none',
+ errorText,
+ helpText,
+ isRequired = false,
+ children,
+ isDisabled = false,
+ name,
+ defaultValue,
+ onChange,
+ value,
+ size = 'small',
+ color = 'primary',
+ testID,
+ selectionType = 'single',
+ ...styledProps
+ }: ChipGroupProps,
+ ref: React.Ref,
+): React.ReactElement => {
const { contextValue, ids } = useChipGroup({
defaultValue,
onChange,
@@ -66,7 +70,7 @@ const ChipGroup = ({
return (
-
+
{
+const _Collapsible = (
+ {
+ children,
+ direction = 'bottom',
+ defaultIsExpanded = false,
+ isExpanded,
+ onExpandChange,
+ testID,
+ _shouldApplyWidthRestrictions = true,
+ _dangerouslyDisableValidations = false,
+ ...styledProps
+ }: CollapsibleProps,
+ ref: React.Ref,
+): ReactElement => {
const [isBodyExpanded, setIsBodyExpanded] = useState(isExpanded ?? defaultIsExpanded);
const collapsibleBodyId = useId(MetaConstants.CollapsibleBody);
@@ -130,6 +133,7 @@ const Collapsible = ({
return (
@@ -147,5 +151,7 @@ const Collapsible = ({
);
};
+const Collapsible = forwardRef(_Collapsible);
+
export type { CollapsibleProps };
export { Collapsible };
diff --git a/packages/blade/src/components/Counter/Counter.tsx b/packages/blade/src/components/Counter/Counter.tsx
index dfb14cae235..51ea44c353f 100644
--- a/packages/blade/src/components/Counter/Counter.tsx
+++ b/packages/blade/src/components/Counter/Counter.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { StyledCounter } from './StyledCounter';
import type { StyledCounterProps } from './types';
import { counterHeight, horizontalPadding } from './counterTokens';
@@ -9,7 +10,7 @@ import type { BaseTextProps } from '~components/Typography/BaseText/types';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { isReactNative, makeSize } from '~utils';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
@@ -75,15 +76,18 @@ const getColorProps = ({
return props;
};
-const _Counter = ({
- value,
- max,
- color = 'neutral',
- emphasis = 'subtle',
- size = 'medium',
- testID,
- ...styledProps
-}: CounterProps): React.ReactElement => {
+const _Counter = (
+ {
+ value,
+ max,
+ color = 'neutral',
+ emphasis = 'subtle',
+ size = 'medium',
+ testID,
+ ...styledProps
+ }: CounterProps,
+ ref: React.Ref,
+): React.ReactElement => {
let content = `${value}`;
if (max && value > max) {
content = `${max}+`;
@@ -112,6 +116,7 @@ const _Counter = ({
return (
{
+const _Divider = (
+ {
+ orientation = 'horizontal',
+ dividerStyle = 'solid',
+ variant = 'muted',
+ thickness = 'thin',
+ height,
+ width,
+ testID,
+ ...styledProps
+ }: DividerProps,
+ ref: React.Ref,
+): React.ReactElement => {
const isDividerHorizontal = orientation === 'horizontal';
const borderPosition = isDividerHorizontal ? 'borderBottom' : 'borderLeft';
const borderColor = { [`${borderPosition}Color`]: `surface.border.gray.${variant}` };
@@ -82,6 +85,7 @@ const Divider = ({
return (
{
+const _Dropdown = (
+ {
+ children,
+ isOpen: isOpenControlled,
+ onOpenChange,
+ selectionType = 'single',
+ testID,
+ _width,
+ ...styledProps
+ }: DropdownProps,
+ ref: React.Ref,
+): React.ReactElement => {
const [options, setOptions] = React.useState([]);
const [filteredValues, setFilteredValues] = React.useState([]);
const [selectedIndices, setSelectedIndices] = React.useState<
@@ -228,7 +232,7 @@ const _Dropdown = ({
{
+ throwBladeError({
+ message: 'Fade is not yet implemented for native',
+ moduleName: 'Fade',
+ });
+
+ return Fade Component is not available for Native mobile apps.;
+};
+
+export { Fade };
+export type { BaseMotionEntryExitProps as FadeProps };
diff --git a/packages/blade/src/components/Fade/Fade.stories.tsx b/packages/blade/src/components/Fade/Fade.stories.tsx
new file mode 100644
index 00000000000..74cb66ef901
--- /dev/null
+++ b/packages/blade/src/components/Fade/Fade.stories.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { Fade } from './';
+import type { FadeProps } 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 { TextInput } from '~components/Input/TextInput';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/Fade',
+ component: Fade,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const FadeTemplate: StoryFn = (args) => {
+ const [isVisible, setIsVisible] = React.useState(true);
+
+ return (
+
+
+
+
+ );
+};
+
+export const Default = FadeTemplate.bind({});
+Default.args = {
+ children: ,
+};
+
+export const InitialFade = () => {
+ return (
+
+
+
+ );
+};
+
+export const WithRef = (args: typeof Fade): React.ReactElement => {
+ const [isVisible, setIsVisible] = React.useState(true);
+
+ const inputRef = React.useRef(null);
+ React.useEffect(() => {
+ if (isVisible) {
+ inputRef.current?.focus();
+ }
+ }, [isVisible]);
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/blade/src/components/Fade/Fade.web.tsx b/packages/blade/src/components/Fade/Fade.web.tsx
new file mode 100644
index 00000000000..d55e52f45ca
--- /dev/null
+++ b/packages/blade/src/components/Fade/Fade.web.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { BaseMotionEntryExit } from '~components/BaseMotion';
+import type { BaseMotionEntryExitProps, MotionVariantsType } from '~components/BaseMotion';
+import { castWebType, useTheme } from '~utils';
+import { cssBezierToArray } from '~utils/cssBezierToArray';
+import { msToSeconds } from '~utils/msToSeconds';
+
+export type FadeProps = BaseMotionEntryExitProps;
+
+export const Fade = ({
+ children,
+ isVisible,
+ type = 'inout',
+ motionTriggers = ['mount'],
+ shouldUnmountWhenHidden,
+ delay,
+}: FadeProps) => {
+ const { theme } = useTheme();
+ const enterDelay = typeof delay === 'object' ? delay.enter : delay;
+ const exitDelay = typeof delay === 'object' ? delay.exit : delay;
+
+ const fadeVariants: MotionVariantsType = {
+ initial: {
+ opacity: 0,
+ },
+ animate: {
+ opacity: 1,
+ transition: {
+ // We have to make sure we don't add delay prop because if we define it, it takes precedence in stagger.
+ // Even setting `undefined` would break the stagger
+ ...(enterDelay ? { delay: msToSeconds(theme.motion.delay[enterDelay]) } : {}),
+ duration: msToSeconds(theme.motion.duration.xquick),
+ ease: cssBezierToArray(castWebType(theme.motion.easing.entrance)),
+ },
+ },
+ exit: {
+ opacity: 0,
+ transition: {
+ ...(exitDelay ? { delay: msToSeconds(theme.motion.delay[exitDelay]) } : {}),
+ duration: msToSeconds(theme.motion.duration.xquick),
+ ease: cssBezierToArray(castWebType(theme.motion.easing.exit)),
+ },
+ },
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/blade/src/components/Fade/index.ts b/packages/blade/src/components/Fade/index.ts
new file mode 100644
index 00000000000..9407d81011d
--- /dev/null
+++ b/packages/blade/src/components/Fade/index.ts
@@ -0,0 +1,2 @@
+export { Fade } from './Fade';
+export type { FadeProps } from './Fade';
diff --git a/packages/blade/src/components/FileUpload/FileUpload.web.tsx b/packages/blade/src/components/FileUpload/FileUpload.web.tsx
index 687e24516bb..9fc07d5cba0 100644
--- a/packages/blade/src/components/FileUpload/FileUpload.web.tsx
+++ b/packages/blade/src/components/FileUpload/FileUpload.web.tsx
@@ -26,6 +26,7 @@ import { makeAccessible } from '~utils/makeAccessible';
import { formHintLeftLabelMarginLeft } from '~components/Input/BaseInput/baseInputTokens';
import { useMergeRefs } from '~utils/useMergeRefs';
import { useControllableState } from '~utils/useControllable';
+import { getInnerMotionRef, getOuterMotionRef } from '~utils/getMotionRefs';
import { fireNativeEvent } from '~utils/fireNativeEvent';
const _FileUpload: React.ForwardRefRenderFunction = (
@@ -53,6 +54,7 @@ const _FileUpload: React.ForwardRefRenderFunction setIsActive(false),
...accessibilityProps,
}}
- ref={mergedRef}
+ ref={getInnerMotionRef({ _motionMeta, ref: mergedRef })}
/>
{
+const _Indicator = (
+ {
+ accessibilityLabel,
+ children,
+ size = 'medium',
+ color = 'neutral',
+ emphasis = 'subtle',
+ testID,
+ ...styledProps
+ }: IndicatorProps,
+ ref: Ref,
+): ReactElement => {
const { theme } = useTheme();
const childrenString = getStringFromReactText(children);
const isIntense = emphasis === 'intense';
@@ -82,6 +86,7 @@ const _Indicator = ({
return (
&
- StyledPropsBlade;
+ StyledPropsBlade &
+ MotionMetaProp;
/*
Mandatory accessibilityLabel prop when label is not provided
@@ -828,6 +831,7 @@ const _BaseInput: React.ForwardRefRenderFunction
+
{
- if (ref && !isReactNative && 'current' in ref) {
- ref.current?.focus();
+ const innerRef = getInnerMotionRef({ _motionMeta, ref });
+ if (innerRef && !isReactNative && 'current' in innerRef) {
+ innerRef.current?.focus();
}
}}
labelPrefix={isLabelInsideInput ? label : undefined}
@@ -1013,7 +1022,7 @@ const _BaseInput: React.ForwardRefRenderFunction
* ```
*/
-const _List = ({
- variant = 'unordered',
- size,
- children,
- icon,
- testID,
- iconColor,
- ...styledProps
-}: ListProps): React.ReactElement => {
+const _List = (
+ { variant = 'unordered', size, children, icon, testID, iconColor, ...styledProps }: ListProps,
+ ref: React.Ref,
+): React.ReactElement => {
const ListElement = variant === 'unordered' ? StyledUnorderedList : StyledOrderedList;
const { level, size: listContextSize } = useListContext();
const listContextValue = useMemo(
@@ -120,7 +115,7 @@ const _List = ({
return (
-
+
{
+ throwBladeError({
+ message: 'Morph is not yet implemented for native',
+ moduleName: 'Morph',
+ });
+
+ return Morph Component is not available for Native mobile apps.;
+};
+
+export { Morph };
diff --git a/packages/blade/src/components/Morph/Morph.stories.tsx b/packages/blade/src/components/Morph/Morph.stories.tsx
new file mode 100644
index 00000000000..f7f1fdc442e
--- /dev/null
+++ b/packages/blade/src/components/Morph/Morph.stories.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { Morph } from './';
+import type { MorphProps } from './';
+import { Sandbox } from '~utils/storybook/Sandbox';
+import StoryPageWrapper from '~utils/storybook/StoryPageWrapper';
+import { Button } from '~components/Button';
+import { Box } from '~components/Box';
+import { AnimatePresence } from 'framer-motion';
+import { TextInput } from '~components/Input/TextInput';
+import { Link } from '~components/Link';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/Morph',
+ component: Morph,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const MorphTemplate: StoryFn = (args) => {
+ const [showNameButton, setShowNameButton] = React.useState(true);
+ return (
+
+
+ {showNameButton ? (
+
+
+
+ ) : (
+
+
+ setShowNameButton(true)} variant="button">
+ Submit
+
+ }
+ />
+
+
+ )}
+
+
+ );
+};
+
+export const Default = MorphTemplate.bind({});
diff --git a/packages/blade/src/components/Morph/Morph.web.tsx b/packages/blade/src/components/Morph/Morph.web.tsx
new file mode 100644
index 00000000000..b6bc0057d68
--- /dev/null
+++ b/packages/blade/src/components/Morph/Morph.web.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { MotionDiv } from '~components/BaseMotion';
+import { useMemoizedStyles } from '~components/Box/BaseBox/useMemoizedStyles.web';
+import { useTheme } from '~utils';
+import { MorphProps } from './types';
+
+const Morph = ({ children, layoutId }: MorphProps) => {
+ // Apart from framer-motion's layout morphing, we also support morph of backgroundColor and borderRadius
+ const { borderRadius, backgroundColor, ...rest } = children.props;
+ const { theme } = useTheme();
+ const cssProps = useMemoizedStyles({ borderRadius, backgroundColor, theme });
+
+ return (
+
+ );
+};
+
+export { Morph };
diff --git a/packages/blade/src/components/Morph/index.ts b/packages/blade/src/components/Morph/index.ts
new file mode 100644
index 00000000000..9bafa81d40f
--- /dev/null
+++ b/packages/blade/src/components/Morph/index.ts
@@ -0,0 +1,2 @@
+export { Morph } from './Morph';
+export type { MorphProps } from './types';
diff --git a/packages/blade/src/components/Morph/types.ts b/packages/blade/src/components/Morph/types.ts
new file mode 100644
index 00000000000..2edc5784ac4
--- /dev/null
+++ b/packages/blade/src/components/Morph/types.ts
@@ -0,0 +1,6 @@
+type MorphProps = {
+ children: React.ReactElement;
+ layoutId: string;
+};
+
+export type { MorphProps };
diff --git a/packages/blade/src/components/Move/Move.native.tsx b/packages/blade/src/components/Move/Move.native.tsx
new file mode 100644
index 00000000000..80900869269
--- /dev/null
+++ b/packages/blade/src/components/Move/Move.native.tsx
@@ -0,0 +1,15 @@
+import { BaseMotionEntryExitProps } from '~components/BaseMotion';
+import { Text } from '~components/Typography';
+import { throwBladeError } from '~utils/logger';
+
+const Move = (_props: BaseMotionEntryExitProps): React.ReactElement => {
+ throwBladeError({
+ message: 'Move is not yet implemented for native',
+ moduleName: 'Move',
+ });
+
+ return Move Component is not available for Native mobile apps.;
+};
+
+export { Move };
+export type { BaseMotionEntryExitProps as MoveProps };
diff --git a/packages/blade/src/components/Move/Move.stories.tsx b/packages/blade/src/components/Move/Move.stories.tsx
new file mode 100644
index 00000000000..d0aadc27d4e
--- /dev/null
+++ b/packages/blade/src/components/Move/Move.stories.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { Move } from '.';
+import type { MoveProps } 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 { Text } from '~components/Typography';
+import {
+ Card,
+ CardBody,
+ CardFooter,
+ CardFooterTrailing,
+ CardHeader,
+ CardHeaderBadge,
+ CardHeaderCounter,
+ CardHeaderIcon,
+ CardHeaderLeading,
+ CardHeaderTrailing,
+} from '~components/Card';
+import { CheckCircleIcon } from '~components/Icons';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/Move',
+ component: Move,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const MoveTemplate: StoryFn = (args) => {
+ const [isVisible, setIsVisible] = React.useState(true);
+ return (
+
+
+
+
+ );
+};
+
+export const Default = MoveTemplate.bind({});
+Default.args = {
+ children: ,
+};
diff --git a/packages/blade/src/components/Move/Move.web.tsx b/packages/blade/src/components/Move/Move.web.tsx
new file mode 100644
index 00000000000..8ee1a0a9169
--- /dev/null
+++ b/packages/blade/src/components/Move/Move.web.tsx
@@ -0,0 +1,61 @@
+import { BaseMotionEntryExit } from '~components/BaseMotion';
+import type { BaseMotionEntryExitProps, MotionVariantsType } from '~components/BaseMotion';
+import React from 'react';
+import { msToSeconds } from '~utils/msToSeconds';
+import { cssBezierToArray } from '~utils/cssBezierToArray';
+import { castWebType, makeSpace, useTheme } from '~utils';
+
+export type MoveProps = BaseMotionEntryExitProps;
+
+export const Move = ({
+ children,
+ type = 'inout',
+ isVisible,
+ motionTriggers,
+ shouldUnmountWhenHidden,
+ delay,
+}: MoveProps) => {
+ const { theme } = useTheme();
+ const enterDelay = typeof delay === 'object' ? delay.enter : delay;
+ const exitDelay = typeof delay === 'object' ? delay.exit : delay;
+
+ const movePx = makeSpace(theme.spacing[5]);
+
+ const moveVariants: MotionVariantsType = {
+ initial: {
+ opacity: 0,
+ transform: `translateY(${movePx})`,
+ },
+ animate: {
+ opacity: 1,
+ transform: `translateY(${makeSpace(theme.spacing[0])})`,
+ transition: {
+ // We have to make sure we don't add delay prop because if we define it, it takes precedence in stagger.
+ // Even setting `undefined` would break the stagger
+ ...(enterDelay ? { delay: msToSeconds(theme.motion.delay[enterDelay]) } : {}),
+ duration: msToSeconds(theme.motion.duration.xmoderate),
+ ease: cssBezierToArray(castWebType(theme.motion.easing.entrance)),
+ },
+ },
+ exit: {
+ opacity: 0,
+ transform: `translateY(${movePx})`,
+ transition: {
+ ...(exitDelay ? { delay: msToSeconds(theme.motion.delay[exitDelay]) } : {}),
+ duration: msToSeconds(theme.motion.duration.quick),
+ ease: cssBezierToArray(castWebType(theme.motion.easing.exit)),
+ },
+ },
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/blade/src/components/Move/index.ts b/packages/blade/src/components/Move/index.ts
new file mode 100644
index 00000000000..b6de64f645f
--- /dev/null
+++ b/packages/blade/src/components/Move/index.ts
@@ -0,0 +1,2 @@
+export { Move } from './Move';
+export type { MoveProps } from './Move';
diff --git a/packages/blade/src/components/ProgressBar/ProgressBar.tsx b/packages/blade/src/components/ProgressBar/ProgressBar.tsx
index c762039b62c..965343faa36 100644
--- a/packages/blade/src/components/ProgressBar/ProgressBar.tsx
+++ b/packages/blade/src/components/ProgressBar/ProgressBar.tsx
@@ -1,4 +1,5 @@
-import type { ReactElement } from 'react';
+import React from 'react';
+import type { ReactElement, Ref } from 'react';
import { ProgressBarFilled } from './ProgressBarFilled';
import { CircularProgressBarFilled } from './CircularProgressBar';
import clamp from '~utils/lodashButBetter/clamp';
@@ -12,7 +13,7 @@ import type { BaseBoxProps } from '~components/Box/BaseBox';
import BaseBox from '~components/Box/BaseBox';
import type { FeedbackColors } from '~tokens/theme/theme';
import { size } from '~tokens/global';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { makeSize } from '~utils/makeSize';
import type { AccessibilityProps } from '~utils/makeAccessible';
import { makeAccessible } from '~utils/makeAccessible';
@@ -107,21 +108,24 @@ const progressBarHeight: Record, 2 | 4 | 0
large: size[0],
};
-const ProgressBar = ({
- accessibilityLabel,
- color,
- type,
- isIndeterminate = false,
- label,
- showPercentage = true,
- size = 'small',
- value = 0,
- variant = 'progress',
- min = 0,
- max = 100,
- testID,
- ...styledProps
-}: ProgressBarProps): ReactElement => {
+const _ProgressBar = (
+ {
+ accessibilityLabel,
+ color,
+ type,
+ isIndeterminate = false,
+ label,
+ showPercentage = true,
+ size = 'small',
+ value = 0,
+ variant = 'progress',
+ min = 0,
+ max = 100,
+ testID,
+ ...styledProps
+ }: ProgressBarProps,
+ ref: Ref,
+): ReactElement => {
const { theme } = useTheme();
const progressType = !type && (variant === 'meter' || variant === 'progress') ? variant : type;
const progressVariant = variant === 'meter' || variant === 'progress' ? 'linear' : variant;
@@ -198,6 +202,7 @@ const ProgressBar = ({
return (
@@ -276,5 +281,7 @@ const ProgressBar = ({
);
};
+const ProgressBar = React.forwardRef(_ProgressBar);
+
export type { ProgressBarProps, ProgressBarVariant };
export { ProgressBar };
diff --git a/packages/blade/src/components/Radio/Radio.tsx b/packages/blade/src/components/Radio/Radio.tsx
index 81381618136..902aaf51c40 100644
--- a/packages/blade/src/components/Radio/Radio.tsx
+++ b/packages/blade/src/components/Radio/Radio.tsx
@@ -19,6 +19,8 @@ import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { getPlatformType, makeSize, useTheme } from '~utils';
import { MetaConstants } from '~utils/metaAttribute';
import { throwBladeError } from '~utils/logger';
+import { MotionMetaProp } from '~components/BaseMotion';
+import { getInnerMotionRef, getOuterMotionRef } from '~utils/getMotionRefs';
type RadioProps = {
/**
@@ -47,10 +49,11 @@ type RadioProps = {
*/
size?: 'small' | 'medium' | 'large';
} & TestID &
- StyledPropsBlade;
+ StyledPropsBlade &
+ MotionMetaProp;
const _Radio: React.ForwardRefRenderFunction = (
- { value, children, helpText, isDisabled, size = 'medium', testID, ...styledProps },
+ { value, children, helpText, isDisabled, size = 'medium', testID, _motionMeta, ...styledProps },
ref,
) => {
const { theme } = useTheme();
@@ -101,7 +104,7 @@ const _Radio: React.ForwardRefRenderFunction = (
const helpTextLeftSpacing = makeSize(radioSizes.icon[size].width + theme.spacing[3]);
return (
-
+
= (
isDisabled={_isDisabled}
hasError={hasError}
inputProps={inputProps}
- ref={ref}
+ ref={getInnerMotionRef({ _motionMeta, ref })}
/>
{
`;
// usePresence hook waits for the animation to finish before unmounting the component
- // It's similar to framer-motions usePresence hook
+ // It's similar to motion/react's usePresence hook
// https://www.framer.com/docs/animate-presence/#usepresence
const { isMounted, isVisible } = usePresence(!!show, {
transitionDuration: duration,
diff --git a/packages/blade/src/components/Scale/Scale.native.tsx b/packages/blade/src/components/Scale/Scale.native.tsx
new file mode 100644
index 00000000000..6ae4541a5a3
--- /dev/null
+++ b/packages/blade/src/components/Scale/Scale.native.tsx
@@ -0,0 +1,14 @@
+import { Text } from '~components/Typography';
+import { throwBladeError } from '~utils/logger';
+import { ScaleProps } from './types';
+
+const Scale = (_props: ScaleProps): React.ReactElement => {
+ throwBladeError({
+ message: 'Scale is not yet implemented for native',
+ moduleName: 'Scale',
+ });
+
+ return Scale Component is not available for Native mobile apps.;
+};
+
+export { Scale };
diff --git a/packages/blade/src/components/Scale/Scale.stories.tsx b/packages/blade/src/components/Scale/Scale.stories.tsx
new file mode 100644
index 00000000000..064e465d5d3
--- /dev/null
+++ b/packages/blade/src/components/Scale/Scale.stories.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { Scale } from './';
+import type { ScaleProps } 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';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/Scale',
+ component: Scale,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const ControlledScaleTemplate: StoryFn = (args) => {
+ const [isHighlighted, setIsHighlighted] = React.useState(false);
+ return (
+
+
+
+
+ );
+};
+
+const ScaleTemplate: StoryFn = (args) => {
+ return ;
+};
+
+export const Default = ScaleTemplate.bind({});
+Default.args = {
+ children: ,
+};
+
+export const Controlled = ControlledScaleTemplate.bind({});
+Controlled.args = {
+ children: ,
+};
diff --git a/packages/blade/src/components/Scale/Scale.web.tsx b/packages/blade/src/components/Scale/Scale.web.tsx
new file mode 100644
index 00000000000..2a66492fbd8
--- /dev/null
+++ b/packages/blade/src/components/Scale/Scale.web.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { BaseMotionEnhancerBox } from '~components/BaseMotion';
+import type { MotionVariantsType } from '~components/BaseMotion';
+import { msToSeconds } from '~utils/msToSeconds';
+import { cssBezierToArray } from '~utils/cssBezierToArray';
+import { castWebType, useTheme } from '~utils';
+import type { ScaleProps } from './types';
+
+export const Scale = ({
+ children,
+ isHighlighted,
+ type = 'inout',
+ variant = 'scale-up',
+ motionTriggers,
+}: ScaleProps) => {
+ const isControlledHighlighted = typeof isHighlighted === 'boolean';
+ const defaultMotionTriggers = isControlledHighlighted ? ['mount' as const] : ['hover' as const];
+ const { theme } = useTheme();
+
+ const fadeVariants: MotionVariantsType = {
+ initial: {},
+ animate: {
+ scale:
+ isHighlighted || !isControlledHighlighted
+ ? variant === 'scale-up'
+ ? 1.05
+ : 0.98
+ : undefined,
+ transition: {
+ duration: msToSeconds(theme.motion.duration.moderate),
+ ease: cssBezierToArray(castWebType(theme.motion.easing.standard)),
+ },
+ },
+ exit: {},
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/blade/src/components/Scale/index.ts b/packages/blade/src/components/Scale/index.ts
new file mode 100644
index 00000000000..21abb05e71f
--- /dev/null
+++ b/packages/blade/src/components/Scale/index.ts
@@ -0,0 +1,2 @@
+export { Scale } from './Scale';
+export type { ScaleProps } from './types';
diff --git a/packages/blade/src/components/Scale/types.ts b/packages/blade/src/components/Scale/types.ts
new file mode 100644
index 00000000000..46be65de8db
--- /dev/null
+++ b/packages/blade/src/components/Scale/types.ts
@@ -0,0 +1,11 @@
+import type { BaseMotionBoxProps } from '~components/BaseMotion';
+
+type ScaleProps = {
+ isHighlighted?: boolean;
+ variant?: 'scale-up' | 'scale-down';
+ type?: BaseMotionBoxProps['type'];
+ motionTriggers?: BaseMotionBoxProps['motionTriggers'];
+ children: BaseMotionBoxProps['children'];
+};
+
+export type { ScaleProps };
diff --git a/packages/blade/src/components/SideNav/SideNav.web.tsx b/packages/blade/src/components/SideNav/SideNav.web.tsx
index cfa5ed7533b..dae3d98ccad 100644
--- a/packages/blade/src/components/SideNav/SideNav.web.tsx
+++ b/packages/blade/src/components/SideNav/SideNav.web.tsx
@@ -19,6 +19,7 @@ import { SkipNavContent, SkipNavLink } from '~components/SkipNav/SkipNav';
import { useIsMobile } from '~utils/useIsMobile';
import { getStyledProps } from '~components/Box/styledProps';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
const {
COLLAPSED,
@@ -103,14 +104,10 @@ const getL1MenuClassName = ({
* SideNav requires handling active state with React Router, Checkout Usage with React Router v6 at - [SideNav Documentation](https://blade.razorpay.com/?path=/docs/components-sidenav--docs)
*
*/
-const SideNav = ({
- children,
- isOpen,
- onDismiss,
- banner,
- testID,
- ...styledProps
-}: SideNavProps): React.ReactElement => {
+const _SideNav = (
+ { children, isOpen, onDismiss, banner, testID, ...styledProps }: SideNavProps,
+ ref: React.Ref,
+): React.ReactElement => {
const l2PortalContainerRef = React.useRef(null);
const l1ContainerRef = React.useRef(null);
const timeoutIdsRef = React.useRef([]);
@@ -235,6 +232,7 @@ const SideNav = ({
>
) : (
{
+const _PulseAnimation = (
+ props: SkeletonProps,
+ ref: React.Ref,
+): React.ReactElement => {
const { theme } = useTheme();
const durationPluseOff = theme.motion.duration.xmoderate;
const durationPluseOn = theme.motion.duration['2xgentle'];
@@ -72,7 +76,9 @@ const PulseAnimation = (props: SkeletonProps): React.ReactElement => {
};
});
- return ;
+ return ;
};
+const PulseAnimation = React.forwardRef(_PulseAnimation);
+
export { PulseAnimation };
diff --git a/packages/blade/src/components/Skeleton/Skeleton.tsx b/packages/blade/src/components/Skeleton/Skeleton.tsx
index bc66ed9330d..e4cd79741a2 100644
--- a/packages/blade/src/components/Skeleton/Skeleton.tsx
+++ b/packages/blade/src/components/Skeleton/Skeleton.tsx
@@ -1,37 +1,43 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
+import React from 'react';
import { PulseAnimation } from './PulseAnimation';
import type { SkeletonProps } from './types';
import { getStyledProps } from '~components/Box/styledProps';
import { makeAccessible } from '~utils/makeAccessible';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
-const Skeleton = ({
- width,
- maxWidth,
- minWidth,
- height,
- maxHeight,
- minHeight,
- borderRadius,
- flexWrap,
- flexDirection,
- flexGrow,
- flexShrink,
- flexBasis,
- alignItems,
- alignContent,
- alignSelf,
- justifyItems,
- justifyContent,
- justifySelf,
- placeSelf,
- placeItems,
- order,
- testID,
- ...props
-}: SkeletonProps): React.ReactElement => {
+const _Skeleton = (
+ {
+ width,
+ maxWidth,
+ minWidth,
+ height,
+ maxHeight,
+ minHeight,
+ borderRadius,
+ flexWrap,
+ flexDirection,
+ flexGrow,
+ flexShrink,
+ flexBasis,
+ alignItems,
+ alignContent,
+ alignSelf,
+ justifyItems,
+ justifyContent,
+ justifySelf,
+ placeSelf,
+ placeItems,
+ order,
+ testID,
+ ...props
+ }: SkeletonProps,
+ ref: React.Ref,
+): React.ReactElement => {
return (
{
+ throwBladeError({
+ message: 'Slide is not yet implemented for native',
+ moduleName: 'Slide',
+ });
+
+ return Slide Component is not available for Native mobile apps.;
+};
+
+export { Slide };
diff --git a/packages/blade/src/components/Slide/Slide.stories.tsx b/packages/blade/src/components/Slide/Slide.stories.tsx
new file mode 100644
index 00000000000..98e88d0bf1f
--- /dev/null
+++ b/packages/blade/src/components/Slide/Slide.stories.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { Title } from '@storybook/addon-docs';
+import { Slide } from '.';
+import type { SlideProps } 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';
+
+const Page = (): React.ReactElement => {
+ return (
+
+ Usage
+
+ {`
+ const todo = 'todo';
+ `}
+
+
+ );
+};
+
+export default {
+ title: 'Motion/Slide',
+ component: Slide,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ page: Page,
+ },
+ },
+} as Meta;
+
+const SlideTemplate: StoryFn = (args) => {
+ const [isVisible, setIsVisible] = React.useState(true);
+
+ return (
+
+
+
+
+ );
+};
+
+export const Default = SlideTemplate.bind({});
+Default.args = {
+ children: ,
+ direction: { enter: 'bottom', exit: 'left' },
+};
diff --git a/packages/blade/src/components/Slide/Slide.web.tsx b/packages/blade/src/components/Slide/Slide.web.tsx
new file mode 100644
index 00000000000..87ca052d452
--- /dev/null
+++ b/packages/blade/src/components/Slide/Slide.web.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { BaseMotionEntryExit } from '~components/BaseMotion';
+import type { MotionVariantsType } from '~components/BaseMotion';
+import { msToSeconds } from '~utils/msToSeconds';
+import { cssBezierToArray } from '~utils/cssBezierToArray';
+import { castWebType, useTheme } from '~utils';
+import type { SlideProps } from './types';
+
+const getFromTransform = (
+ direction: SlideProps['direction'],
+ fromOffset: SlideProps['fromOffset'],
+): `translate${string}` => {
+ if (direction === 'top') {
+ return `translateY(-${fromOffset})`;
+ }
+
+ if (direction === 'left') {
+ return `translateX(-${fromOffset})`;
+ }
+
+ if (direction === 'right') {
+ return `translateX(${fromOffset})`;
+ }
+
+ return `translateY(${fromOffset})`;
+};
+
+export const Slide = ({
+ children,
+ type = 'inout',
+ direction = 'bottom',
+ isVisible,
+ motionTriggers,
+ shouldUnmountWhenHidden,
+ fromOffset,
+ delay,
+}: SlideProps) => {
+ const { theme } = useTheme();
+
+ const enterDirection = typeof direction === 'object' ? direction.enter : direction;
+ const exitDirection = typeof direction === 'object' ? direction.exit : direction;
+ const isEnterDirectionHorizontal = ['left', 'right'].includes(enterDirection);
+ const isExitDirectionHorizontal = ['left', 'right'].includes(exitDirection);
+
+ const defaultOffset: SlideProps['fromOffset'] = isEnterDirectionHorizontal ? '100vw' : '100vh';
+
+ const enterTransform = getFromTransform(enterDirection, fromOffset ?? defaultOffset);
+ const exitTransform = getFromTransform(exitDirection, fromOffset ?? defaultOffset);
+
+ const enterDelay = typeof delay === 'object' ? delay.enter : delay;
+ const exitDelay = typeof delay === 'object' ? delay.exit : delay;
+
+ const moveVariants: MotionVariantsType = React.useMemo(
+ () => ({
+ initial: {
+ // We keep element in view with opacity 0 initially so that it works with `in-view` trigger as expected
+ opacity: 0,
+ },
+ animate: {
+ transform: [enterTransform, 'translateY(0%)'],
+ opacity: 1,
+ transition: {
+ // We have to make sure we don't add delay prop because if we define it, it takes precedence in stagger.
+ // Even setting `undefined` would break the stagger
+ ...(enterDelay ? { delay: msToSeconds(theme.motion.delay[enterDelay]) } : {}),
+ duration: msToSeconds(
+ isEnterDirectionHorizontal
+ ? theme.motion.duration.xmoderate
+ : theme.motion.duration['2xgentle'],
+ ),
+ ease: cssBezierToArray(
+ isEnterDirectionHorizontal
+ ? castWebType(theme.motion.easing.entrance)
+ : castWebType(theme.motion.easing.emphasized),
+ ),
+ },
+ },
+ exit: {
+ opacity: 0,
+ transform: exitTransform,
+ transitionEnd: {
+ transform: enterTransform,
+ },
+ transition: {
+ // We have to make sure we don't add delay prop because if we define it, it takes precedence in stagger.
+ // Even setting `undefined` would break the stagger
+ ...(exitDelay ? { delay: msToSeconds(theme.motion.delay[exitDelay]) } : {}),
+ duration: msToSeconds(
+ isExitDirectionHorizontal
+ ? theme.motion.duration.moderate
+ : theme.motion.duration.xgentle,
+ ),
+ ease: cssBezierToArray(
+ isExitDirectionHorizontal
+ ? castWebType(theme.motion.easing.exit)
+ : castWebType(theme.motion.easing.emphasized),
+ ),
+ },
+ },
+ }),
+ [
+ enterDirection,
+ exitDirection,
+ isEnterDirectionHorizontal,
+ isExitDirectionHorizontal,
+ theme.name,
+ ],
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/blade/src/components/Slide/index.ts b/packages/blade/src/components/Slide/index.ts
new file mode 100644
index 00000000000..47f8828feb1
--- /dev/null
+++ b/packages/blade/src/components/Slide/index.ts
@@ -0,0 +1,2 @@
+export { Slide } from './Slide';
+export type { SlideProps } from './types';
diff --git a/packages/blade/src/components/Slide/types.ts b/packages/blade/src/components/Slide/types.ts
new file mode 100644
index 00000000000..e3c14b4d4a4
--- /dev/null
+++ b/packages/blade/src/components/Slide/types.ts
@@ -0,0 +1,14 @@
+import type { BaseMotionEntryExitProps } from '~components/BaseMotion';
+
+type SlideDirections = 'top' | 'right' | 'bottom' | 'left';
+
+export type SlideProps = BaseMotionEntryExitProps & {
+ direction?:
+ | SlideDirections
+ | {
+ enter: SlideDirections;
+ exit: SlideDirections;
+ };
+
+ fromOffset?: `100vh` | `100vw` | `${number}%`;
+};
diff --git a/packages/blade/src/components/Spinner/BaseSpinner/BaseSpinner.tsx b/packages/blade/src/components/Spinner/BaseSpinner/BaseSpinner.tsx
index c39c2e5f22a..e9cf19df110 100644
--- a/packages/blade/src/components/Spinner/BaseSpinner/BaseSpinner.tsx
+++ b/packages/blade/src/components/Spinner/BaseSpinner/BaseSpinner.tsx
@@ -11,7 +11,7 @@ import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import type { FeedbackColors } from '~tokens/theme/theme';
import BaseBox from '~components/Box/BaseBox';
import { Text } from '~components/Typography';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { makeSize } from '~utils/makeSize';
import { makeAccessible } from '~utils/makeAccessible';
@@ -57,18 +57,22 @@ const getColor = ({ color, theme }: { color: BaseSpinnerProps['color']; theme: T
return getIn(theme.colors, 'interactive.icon.gray.subtle');
};
-const BaseSpinner = ({
- label,
- labelPosition = 'right',
- accessibilityLabel,
- color = 'neutral',
- size = 'medium',
- testID,
- ...styledProps
-}: BaseSpinnerProps): React.ReactElement => {
+const _BaseSpinner = (
+ {
+ label,
+ labelPosition = 'right',
+ accessibilityLabel,
+ color = 'neutral',
+ size = 'medium',
+ testID,
+ ...styledProps
+ }: BaseSpinnerProps,
+ ref: React.Ref,
+): React.ReactElement => {
const { theme } = useTheme();
return (
@@ -99,5 +103,7 @@ const BaseSpinner = ({
);
};
+const BaseSpinner = React.forwardRef(_BaseSpinner);
+
export type { BaseSpinnerProps };
export { BaseSpinner };
diff --git a/packages/blade/src/components/Spinner/Spinner/Spinner.tsx b/packages/blade/src/components/Spinner/Spinner/Spinner.tsx
index b7a0186117a..aff84731fa5 100644
--- a/packages/blade/src/components/Spinner/Spinner/Spinner.tsx
+++ b/packages/blade/src/components/Spinner/Spinner/Spinner.tsx
@@ -1,5 +1,7 @@
+import React from 'react';
import { BaseSpinner } from '../BaseSpinner';
import type { BaseSpinnerProps } from '../BaseSpinner';
+import type { BladeElementRef } from '~utils/types';
type SpinnerProps = BaseSpinnerProps & {
/**
@@ -10,17 +12,21 @@ type SpinnerProps = BaseSpinnerProps & {
color?: 'primary' | 'neutral' | 'white';
};
-const Spinner = ({
- label,
- labelPosition,
- accessibilityLabel,
- color = 'neutral',
- size = 'medium',
- testID,
- ...styledProps
-}: SpinnerProps): React.ReactElement => {
+const _Spinner = (
+ {
+ label,
+ labelPosition,
+ accessibilityLabel,
+ color = 'neutral',
+ size = 'medium',
+ testID,
+ ...styledProps
+ }: SpinnerProps,
+ ref: React.Ref,
+): React.ReactElement => {
return (
{
+ throwBladeError({
+ message: 'Stagger is not yet implemented for native',
+ moduleName: 'Stagger',
+ });
+
+ return Stagger Component is not available for Native mobile apps.;
+};
+
+export { Stagger };
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..e4d8fbc01e8
--- /dev/null
+++ b/packages/blade/src/components/Stagger/Stagger.web.tsx
@@ -0,0 +1,60 @@
+import { BaseMotionBox } from '~components/BaseMotion';
+import type { MotionVariantsType } from '~components/BaseMotion';
+import { AnimatePresence } from 'framer-motion';
+import { StaggerContext } from './StaggerProvider';
+import { StaggerProps } from './types';
+import React from 'react';
+import { msToSeconds } from '~utils/msToSeconds';
+import { useTheme } from '~utils';
+
+export const Stagger = ({
+ children,
+ isVisible = true,
+ type = 'inout',
+ shouldUnmountWhenHidden = false,
+ delay = 'none',
+}: StaggerProps) => {
+ const { theme } = useTheme();
+
+ // Only need AnimatePresence when we have to unmount the component
+ const AnimateWrapper = shouldUnmountWhenHidden ? AnimatePresence : React.Fragment;
+ // keep it always mounted when shouldUnmountWhenHidden is false
+ const isMounted = shouldUnmountWhenHidden ? isVisible : true;
+
+ const enterDelay = typeof delay === 'object' ? delay.enter : delay;
+ const exitDelay = typeof delay === 'object' ? delay.exit : delay;
+
+ const staggerVariants: MotionVariantsType = {
+ initial: {},
+ animate: {
+ transition: {
+ delayChildren: msToSeconds(theme.motion.delay[enterDelay]),
+ staggerChildren: msToSeconds(theme.motion.duration['2xquick']),
+ },
+ },
+ exit: {
+ transition: {
+ delayChildren: msToSeconds(theme.motion.delay[exitDelay]),
+ staggerChildren: msToSeconds(theme.motion.duration['2xquick']),
+ },
+ },
+ };
+
+ return (
+
+ {isMounted ? (
+
+
+ {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..b30d8915010
--- /dev/null
+++ b/packages/blade/src/components/Stagger/StaggerProvider.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import type { BaseMotionBoxProps } from '~components/BaseMotion';
+
+type StaggerContextType = {
+ isInsideStaggerContainer: boolean;
+ staggerType: BaseMotionBoxProps['type'];
+};
+
+const StaggerContext = React.createContext({
+ isInsideStaggerContainer: false,
+ staggerType: 'inout',
+});
+
+const useStagger = (): StaggerContextType => {
+ 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..8797e0c053e
--- /dev/null
+++ b/packages/blade/src/components/Stagger/index.ts
@@ -0,0 +1,2 @@
+export { Stagger } from './Stagger';
+export type { StaggerProps } from './types';
diff --git a/packages/blade/src/components/Stagger/types.ts b/packages/blade/src/components/Stagger/types.ts
new file mode 100644
index 00000000000..5854eedfd0c
--- /dev/null
+++ b/packages/blade/src/components/Stagger/types.ts
@@ -0,0 +1,7 @@
+import type { BaseMotionEntryExitProps } from '~components/BaseMotion';
+
+type StaggerProps = BaseMotionEntryExitProps & {
+ children: React.ReactElement[] | React.ReactElement;
+};
+
+export type { StaggerProps };
diff --git a/packages/blade/src/components/StepGroup/StepGroup.web.tsx b/packages/blade/src/components/StepGroup/StepGroup.web.tsx
index 0deae579651..63ee21b80fe 100644
--- a/packages/blade/src/components/StepGroup/StepGroup.web.tsx
+++ b/packages/blade/src/components/StepGroup/StepGroup.web.tsx
@@ -7,6 +7,7 @@ import { getStyledProps } from '~components/Box/styledProps';
import { getComponentId } from '~utils/isValidAllowedChildren';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import type { BladeElementRef } from '~utils/types';
const useChildrenWithIndexes = ({
_nestingLevel,
@@ -59,17 +60,20 @@ const useChildrenWithIndexes = ({
return { childrenWithIndex, totalItemsInParentGroupCount };
};
-const _StepGroup = ({
- size = 'medium',
- orientation = 'vertical',
- children,
- testID,
- _nestingLevel = 0,
- width,
- minWidth,
- maxWidth,
- ...styledProps
-}: StepGroupProps): React.ReactElement => {
+const _StepGroup = (
+ {
+ size = 'medium',
+ orientation = 'vertical',
+ children,
+ testID,
+ _nestingLevel = 0,
+ width,
+ minWidth,
+ maxWidth,
+ ...styledProps
+ }: StepGroupProps,
+ ref: React.Ref,
+): React.ReactElement => {
const itemsInGroupCount = React.Children.count(children);
const { childrenWithIndex, totalItemsInParentGroupCount } = useChildrenWithIndexes({
children,
@@ -93,6 +97,7 @@ const _StepGroup = ({
return (
= (
{
@@ -30,6 +31,7 @@ const _Switch: React.ForwardRefRenderFunction = (
value,
accessibilityLabel,
testID,
+ _motionMeta,
...styledProps
},
ref,
@@ -83,6 +85,7 @@ const _Switch: React.ForwardRefRenderFunction = (
return (
= (
>
['selectionType']>,
@@ -477,6 +478,7 @@ const _Table = - ({
) : (
routes.find((route) => route.index === index)?.value!;
-const Tabs = ({
+const _Tabs = ({
children,
defaultValue,
value,
@@ -243,4 +243,6 @@ const Tabs = ({
);
};
+const Tabs = React.forwardRef(_Tabs);
+
export { Tabs };
diff --git a/packages/blade/src/components/Tabs/Tabs.web.tsx b/packages/blade/src/components/Tabs/Tabs.web.tsx
index cdb28b93cbb..b887f583e47 100644
--- a/packages/blade/src/components/Tabs/Tabs.web.tsx
+++ b/packages/blade/src/components/Tabs/Tabs.web.tsx
@@ -5,6 +5,7 @@ import { useControllableState } from '~utils/useControllable';
import { useId } from '~utils/useId';
import BaseBox from '~components/Box/BaseBox';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
+import { BladeElementRef } from '~utils/types';
/**
* ### Tabs
@@ -36,17 +37,20 @@ import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
*
* ```
*/
-const Tabs = ({
- children,
- defaultValue,
- value,
- onChange,
- orientation = 'horizontal',
- size = 'medium',
- variant = 'bordered',
- isFullWidthTabItem = false,
- isLazy = false,
-}: TabsProps): React.ReactElement => {
+const _Tabs = (
+ {
+ children,
+ defaultValue,
+ value,
+ onChange,
+ orientation = 'horizontal',
+ size = 'medium',
+ variant = 'bordered',
+ isFullWidthTabItem = false,
+ isLazy = false,
+ }: TabsProps,
+ ref: React.Ref,
+): React.ReactElement => {
const baseId = useId('tabs');
const [selectedValue, setSelectedValue] = useControllableState({
defaultValue,
@@ -83,6 +87,7 @@ const Tabs = ({
return (
(
(props) => {
@@ -28,44 +29,20 @@ const FocussableTag = styled(BaseBox)<{ _isVirtuallyFocused: TagProps['_isVirtua
},
);
-/**
- * ## Tags
- *
- * Tag component can be used to display selected items on UI.
- *
- * ### Usage
- *
- * ***Note:*** _Make sure to handle state when using Tag_
- *
- * ```jsx
- * const [showTag, setShowTag] = React.useState(true);
- *
- * // ...
- *
- * {showTag && (
- * setShowTag(false)}
- * >
- * Transactions
- *
- * )}
- * ```
- *
- * Checkout [Tags Documentation](https://blade.razorpay.com/?path=/story/components-tag--default) for more info.
- *
- */
-const Tag = ({
- size = 'medium',
- icon: Icon,
- onDismiss,
- children,
- isDisabled,
- testID,
- _isVirtuallyFocused,
- _isTagInsideInput,
- ...styledProps
-}: TagProps): React.ReactElement | null => {
+const _Tag = (
+ {
+ size = 'medium',
+ icon: Icon,
+ onDismiss,
+ children,
+ isDisabled,
+ testID,
+ _isVirtuallyFocused,
+ _isTagInsideInput,
+ ...styledProps
+ }: TagProps,
+ ref: React.Ref,
+): React.ReactElement | null => {
const isMobile = useIsMobile();
const textColor = isDisabled ? 'interactive.text.gray.disabled' : 'interactive.text.gray.subtle';
@@ -94,6 +71,7 @@ const Tag = ({
return (
setShowTag(false)}
+ * >
+ * Transactions
+ *
+ * )}
+ * ```
+ *
+ * Checkout [Tags Documentation](https://blade.razorpay.com/?path=/story/components-tag--default) for more info.
+ *
+ */
+const Tag = React.forwardRef(_Tag);
+
export { Tag };
diff --git a/packages/blade/src/components/TopNav/TopNav.web.tsx b/packages/blade/src/components/TopNav/TopNav.web.tsx
index 2ca8cf962ce..bba36fd4ca5 100644
--- a/packages/blade/src/components/TopNav/TopNav.web.tsx
+++ b/packages/blade/src/components/TopNav/TopNav.web.tsx
@@ -11,6 +11,7 @@ import { makeSize } from '~utils';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { componentZIndices } from '~utils/componentZIndices';
+import type { BladeElementRef } from '~utils/types';
const TOP_NAV_HEIGHT = size[56];
const CONTENT_RIGHT_GAP = size[80];
@@ -37,9 +38,13 @@ type TopNavProps = {
> &
StyledPropsBlade;
-const TopNav = ({ children, ...boxProps }: TopNavProps): React.ReactElement => {
+const _TopNav = (
+ { children, ...boxProps }: TopNavProps,
+ ref: React.Ref,
+): React.ReactElement => {
return (
{
);
};
+const TopNav = React.forwardRef(_TopNav);
+
const TopNavBrand = ({ children }: { children: React.ReactNode }): React.ReactElement => {
return (
- * Lorem ipsum SENTRY_TOKEN
normal text
- *
- * ```
- *
- * ### In React Native
- *
- * In React Native, you would have to align it using flex to make sure the Code and the surrounding text is correctly aligned
- *
- * ```tsx
- *
- * Lorem ipsum
- * SENTRY_TOKEN
- * normal text
- *
- * ```
- */
-const Code = ({
- children,
- size = 'small',
- weight = 'regular',
- isHighlighted = true,
- color,
- testID,
- ...styledProps
-}: CodeProps): React.ReactElement => {
+
+const _Code = (
+ {
+ children,
+ size = 'small',
+ weight = 'regular',
+ isHighlighted = true,
+ color,
+ testID,
+ ...styledProps
+ }: CodeProps,
+ ref: React.Ref,
+): React.ReactElement => {
const { fontSize, lineHeight } = getCodeFontSizeAndLineHeight(size)!;
const codeTextColor = React.useMemo(
() => getCodeColor({ isHighlighted, color }),
@@ -166,6 +144,7 @@ const Code = ({
return (
+ * Lorem ipsum SENTRY_TOKEN
normal text
+ *
+ * ```
+ *
+ * ### In React Native
+ *
+ * In React Native, you would have to align it using flex to make sure the Code and the surrounding text is correctly aligned
+ *
+ * ```tsx
+ *
+ * Lorem ipsum
+ * SENTRY_TOKEN
+ * normal text
+ *
+ * ```
+ */
+const Code = React.forwardRef(_Code);
+
export { Code };
diff --git a/packages/blade/src/components/Typography/Display/Display.tsx b/packages/blade/src/components/Typography/Display/Display.tsx
index c66a18a8084..0a2ae43f204 100644
--- a/packages/blade/src/components/Typography/Display/Display.tsx
+++ b/packages/blade/src/components/Typography/Display/Display.tsx
@@ -1,10 +1,11 @@
+import React from 'react';
import type { ReactElement } from 'react';
import { BaseText } from '../BaseText';
import type { BaseTextProps, BaseTextSizes } from '../BaseText/types';
import { useValidateAsProp } from '../utils';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { getPlatformType } from '~utils';
const validAsValues = ['span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
@@ -69,23 +70,27 @@ const getProps = ({
return props;
};
-export const Display = ({
- as,
- size = 'small',
- weight = 'semibold',
- color = 'surface.text.gray.normal',
- children,
- testID,
- textAlign,
- textDecorationLine,
- ...styledProps
-}: DisplayProps): ReactElement => {
+const _Display = (
+ {
+ as,
+ size = 'small',
+ weight = 'semibold',
+ color = 'surface.text.gray.normal',
+ children,
+ testID,
+ textAlign,
+ textDecorationLine,
+ ...styledProps
+ }: DisplayProps,
+ ref: React.Ref,
+): ReactElement => {
useValidateAsProp({ componentName: 'Display', as, validAsValues });
const props = getProps({ as, size, color, weight, testID });
return (
);
};
+
+const Display = React.forwardRef(_Display);
+export { Display };
diff --git a/packages/blade/src/components/Typography/Heading/Heading.tsx b/packages/blade/src/components/Typography/Heading/Heading.tsx
index 1db595c3571..8d76ad2850d 100644
--- a/packages/blade/src/components/Typography/Heading/Heading.tsx
+++ b/packages/blade/src/components/Typography/Heading/Heading.tsx
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
+import React from 'react';
import type { ReactElement } from 'react';
import { BaseText } from '../BaseText';
import type { BaseTextProps, BaseTextSizes } from '../BaseText/types';
@@ -6,7 +7,7 @@ import { useValidateAsProp } from '../utils';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { isReactNative } from '~utils';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
const validAsValues = ['span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
export type HeadingProps = {
@@ -74,17 +75,20 @@ export const getHeadingProps = ({
return props;
};
-export const Heading = ({
- as,
- size = 'small',
- weight = 'semibold',
- color = 'surface.text.gray.normal',
- children,
- testID,
- textAlign,
- textDecorationLine,
- ...styledProps
-}: HeadingProps): ReactElement => {
+const _Heading = (
+ {
+ as,
+ size = 'small',
+ weight = 'semibold',
+ color = 'surface.text.gray.normal',
+ children,
+ testID,
+ textAlign,
+ textDecorationLine,
+ ...styledProps
+ }: HeadingProps,
+ ref: React.Ref,
+): ReactElement => {
useValidateAsProp({ componentName: 'Heading', as, validAsValues });
const props = getHeadingProps({ as, size, weight, color, testID });
@@ -92,6 +96,7 @@ export const Heading = ({
return (
);
};
+
+const Heading = React.forwardRef(_Heading);
+
+export { Heading };
diff --git a/packages/blade/src/components/Typography/Text/Text.tsx b/packages/blade/src/components/Typography/Text/Text.tsx
index 1e4f5921ebd..b1af011fcd0 100644
--- a/packages/blade/src/components/Typography/Text/Text.tsx
+++ b/packages/blade/src/components/Typography/Text/Text.tsx
@@ -6,7 +6,7 @@ import type { BaseTextProps, BaseTextSizes } from '../BaseText/types';
import { useValidateAsProp } from '../utils';
import { getStyledProps } from '~components/Box/styledProps';
import type { StyledPropsBlade } from '~components/Box/styledProps';
-import type { TestID } from '~utils/types';
+import type { BladeElementRef, TestID } from '~utils/types';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { throwBladeError } from '~utils/logger';
@@ -40,13 +40,7 @@ type TextCaptionVariant = TextCommonProps & {
size?: Extract;
};
-/**
- * Conditionally changing props based on variant.
- * Overloads or union gives wrong intellisense.
- */
-export type TextProps = T extends {
- variant: infer Variant;
-}
+export type TextProps = T extends { variant: infer Variant }
? Variant extends 'caption'
? TextCaptionVariant
: Variant extends 'body'
@@ -133,20 +127,23 @@ const getTextProps = ({
return props;
};
-const _Text = ({
- as = 'p',
- variant = 'body',
- weight = 'regular',
- size,
- truncateAfterLines,
- children,
- color,
- testID,
- textAlign,
- textDecorationLine,
- wordBreak,
- ...styledProps
-}: TextProps): ReactElement => {
+const _Text = (
+ {
+ as = 'p',
+ variant = 'body',
+ weight = 'regular',
+ size,
+ truncateAfterLines,
+ children,
+ color,
+ testID,
+ textAlign,
+ textDecorationLine,
+ wordBreak,
+ ...styledProps
+ }: TextProps,
+ ref: React.Ref,
+): ReactElement => {
const props: Omit = {
as,
truncateAfterLines,
@@ -165,13 +162,13 @@ const _Text = ({
useValidateAsProp({ componentName: 'Text', as, validAsValues });
return (
-
+
{children}
);
};
-const Text = assignWithoutSideEffects(_Text, {
+const Text = assignWithoutSideEffects(React.forwardRef(_Text), {
displayName: 'Text',
componentId: 'Text',
});
diff --git a/packages/blade/src/components/Typography/Text/__tests__/Text.native.test.tsx b/packages/blade/src/components/Typography/Text/__tests__/Text.native.test.tsx
index af93a72e35e..65d8261ed58 100644
--- a/packages/blade/src/components/Typography/Text/__tests__/Text.native.test.tsx
+++ b/packages/blade/src/components/Typography/Text/__tests__/Text.native.test.tsx
@@ -91,11 +91,12 @@ describe('', () => {
const displayText = 'Displaying some text';
expect(() =>
renderWithTheme(
+ // TODO: something changed on types after forwarding refs. Check this once
+ // @ts-expect-error testing failure case when size='medium' is passed with variant='caption'
{displayText}
diff --git a/packages/blade/src/components/Typography/Text/__tests__/Text.web.test.tsx b/packages/blade/src/components/Typography/Text/__tests__/Text.web.test.tsx
index c423cd0b81b..29fcc597d34 100644
--- a/packages/blade/src/components/Typography/Text/__tests__/Text.web.test.tsx
+++ b/packages/blade/src/components/Typography/Text/__tests__/Text.web.test.tsx
@@ -85,11 +85,12 @@ describe('', () => {
const displayText = 'Displaying some text';
expect(() =>
renderWithTheme(
+ // TODO: something broke on ts after changing types. Check with Anurag on why this is happening
+ // @ts-expect-error testing failure case when size='medium' is passed with variant='caption'
{displayText}
diff --git a/packages/blade/src/tokens/global/motion.ts b/packages/blade/src/tokens/global/motion.ts
index aab4bf6c6c2..4cd6ab66c12 100644
--- a/packages/blade/src/tokens/global/motion.ts
+++ b/packages/blade/src/tokens/global/motion.ts
@@ -20,7 +20,7 @@ type Duration = {
'2xgentle': 960;
};
-type Delay = {
+export type Delay = {
/** `80` milliseconds */
'2xquick': 80;
/** `160` milliseconds */
diff --git a/packages/blade/src/utils/cssBezierToArray.ts b/packages/blade/src/utils/cssBezierToArray.ts
new file mode 100644
index 00000000000..ab54b353b6c
--- /dev/null
+++ b/packages/blade/src/utils/cssBezierToArray.ts
@@ -0,0 +1,24 @@
+const cssBezierToArray = <
+ X1 extends number,
+ Y1 extends number,
+ X2 extends number,
+ Y2 extends number
+>(
+ cssCubicBezierString: `cubic-bezier(${X1}, ${Y1}, ${X2}, ${Y2})`,
+): [X1, Y1, X2, Y2] => {
+ const indexOfFirstBracket = cssCubicBezierString.indexOf('(');
+ const indexOfLastBracket = cssCubicBezierString.lastIndexOf(')');
+ const bezierValuesString = cssCubicBezierString.slice(
+ indexOfFirstBracket + 1,
+ indexOfLastBracket,
+ );
+ const bezierValuesArray = bezierValuesString.split(',').map((val) => Number(val)) as [
+ X1,
+ Y1,
+ X2,
+ Y2,
+ ];
+ return bezierValuesArray;
+};
+
+export { cssBezierToArray };
diff --git a/packages/blade/src/utils/getMotionRefs.ts b/packages/blade/src/utils/getMotionRefs.ts
new file mode 100644
index 00000000000..33ecb13ede2
--- /dev/null
+++ b/packages/blade/src/utils/getMotionRefs.ts
@@ -0,0 +1,87 @@
+import type React from 'react';
+import type { MotionMetaProp } from '~components/BaseMotion';
+import type { BladeElementRef } from './types';
+
+type MotionRefsType = {
+ ref: React.Ref;
+} & MotionMetaProp;
+
+/**
+ * ## What are `getOuterMotionRef` and `getInnerMotionRef` functions?
+ *
+ * - Motion react requires refs on outermost element to animate components
+ * - But we have certain components like input where we already pass refs on inner elements with certain expectation (e.g. in input to focus on the input)
+ *
+ * Using these 2 utility functions, we can use 2 refs from same forwardRef. One on outer element and one on inner element
+ *
+ * ---
+ *
+ * ### Examples
+ *
+ * ```jsx
+ *
+ * const MyComponent = ({ _motionMeta }: MotionMetaProp, ref: React.Ref) => {
+ *
+ * return (
+ *
+ * Something something
+ *
+ *
+ * )
+ * }
+ * ```
+ *
+ * ### What does it internally do?
+ *
+ * - On BaseMotion.tsx, we inject _motionMeta prop with ref that is passed to child.
+ * - i.e. we pick the `ref` prop of child, and pass it as `_motionMeta.innerRef`. This way in our components, we have access to both the refs. The motion/react ref that we get from forwardRef, and `_motionMeta.innerRef` (ref that you pass on child component of motion)
+ * - Both `getOuterMotionRef` and `getInnerMotionRef` are simple one line utility functions to use the correct ref on correct element.
+ *
+ * ### When to use this?
+ *
+ * - When you want to forward ref to component on inner element instead of outer element (e.g. passing down the ref to input in checkbox)
+ */
+const getOuterMotionRef = ({ _motionMeta, ref }: MotionRefsType): any => {
+ return _motionMeta?.isEnhanced ? ref : null;
+};
+
+/**
+ * ## What are `getOuterMotionRef` and `getInnerMotionRef` functions?
+ *
+ * - Motion react requires refs on outermost element to animate components
+ * - But we have certain components like input where we already pass refs on inner elements with certain expectation (e.g. in input to focus on the input)
+ *
+ * Using these 2 utility functions, we can use 2 refs from same forwardRef. One on outer element and one on inner element
+ *
+ * ---
+ *
+ * ### Examples
+ *
+ * ```jsx
+ *
+ * const MyComponent = ({ _motionMeta }: MotionMetaProp, ref: React.Ref) => {
+ *
+ * return (
+ *
+ * Something something
+ *
+ *
+ * )
+ * }
+ * ```
+ *
+ * ### What does it internally do?
+ *
+ * - On BaseMotion.tsx, we inject _motionMeta prop with ref that is passed to child.
+ * - i.e. we pick the `ref` prop of child, and pass it as `_motionMeta.innerRef`. This way in our components, we have access to both the refs. The motion/react ref that we get from forwardRef, and `_motionMeta.innerRef` (ref that you pass on child component of motion)
+ * - Both `getOuterMotionRef` and `getInnerMotionRef` are simple one line utility functions to use the correct ref on correct element.
+ *
+ * ### When to use this?
+ *
+ * - When you want to forward ref to component on inner element instead of outer element (e.g. passing down the ref to input in checkbox)
+ */
+const getInnerMotionRef = ({ _motionMeta, ref }: MotionRefsType): any => {
+ return _motionMeta?.isEnhanced ? _motionMeta.innerRef : ref;
+};
+
+export { getOuterMotionRef, getInnerMotionRef };
diff --git a/packages/blade/src/utils/msToSeconds.ts b/packages/blade/src/utils/msToSeconds.ts
new file mode 100644
index 00000000000..2958c0d902b
--- /dev/null
+++ b/packages/blade/src/utils/msToSeconds.ts
@@ -0,0 +1,5 @@
+const msToSeconds = (durationInMS: number): number => {
+ return durationInMS / 1000;
+};
+
+export { msToSeconds };
diff --git a/yarn.lock b/yarn.lock
index 8139ef058fe..c1c982ada92 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14607,6 +14607,13 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
+framer-motion@^11.12.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.12.0.tgz#565d09b869c0d2cd2f4ad86a77363bfbe41518d2"
+ integrity sha512-gZaZeqFM6pX9kMVti60hYAa75jGpSsGYWAHbBfIkuHN7DkVHVkxSxeNYnrGmHuM0zPkWTzQx10ZT+fDjn7N4SA==
+ dependencies:
+ tslib "^2.4.0"
+
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -20742,6 +20749,14 @@ moniker@0.1.2:
resolved "https://registry.yarnpkg.com/moniker/-/moniker-0.1.2.tgz#872dfba575dcea8fa04a5135b13d5f24beccc97e"
integrity sha512-Uj9iV0QYr6281G+o0TvqhKwHHWB2Q/qUTT4LPQ3qDGc0r8cbMuqQjRXPZuVZ+gcL7APx+iQgE8lcfWPrj1LsLA==
+motion@11.12.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/motion/-/motion-11.12.0.tgz#20180cf00fe4c30105659a02ad92a93560211dc4"
+ integrity sha512-BFH9vwCs4dI9t1W1/1HonahOCnTxcKfzBR8D310wHFdx7oIwlP/51OqLNGO74lxOdCpTLf5BLe233k6yRqJo9Q==
+ dependencies:
+ framer-motion "^11.12.0"
+ tslib "^2.4.0"
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"