diff --git a/.changeset/drawer-react-native-support.md b/.changeset/drawer-react-native-support.md new file mode 100644 index 0000000000..d913ffa594 --- /dev/null +++ b/.changeset/drawer-react-native-support.md @@ -0,0 +1,5 @@ +--- +"@razorpay/blade": minor +--- + +feat(rn): add React Native support for Drawer component diff --git a/packages/blade/src/components/BladeProvider/BladeProvider.native.tsx b/packages/blade/src/components/BladeProvider/BladeProvider.native.tsx index e814347716..28eadb2e8d 100644 --- a/packages/blade/src/components/BladeProvider/BladeProvider.native.tsx +++ b/packages/blade/src/components/BladeProvider/BladeProvider.native.tsx @@ -6,6 +6,7 @@ import { ThemeContext } from './useTheme'; import { useBladeProvider } from './useBladeProvider'; import type { BladeProviderProps } from './types'; import { BottomSheetStackProvider } from '~components/BottomSheet/BottomSheetStack'; +import { DrawerStackProvider } from '~components/Drawer/StackProvider'; const gestureHandlerStyle = { flex: 1, @@ -23,7 +24,9 @@ const BladeProvider = ({ - {children} + + {children} + diff --git a/packages/blade/src/components/Drawer/AnimatedDrawerContainer.native.tsx b/packages/blade/src/components/Drawer/AnimatedDrawerContainer.native.tsx new file mode 100644 index 0000000000..ece96c56fc --- /dev/null +++ b/packages/blade/src/components/Drawer/AnimatedDrawerContainer.native.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import styled from 'styled-components/native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, + runOnJS, + cancelAnimation, +} from 'react-native-reanimated'; +import { Dimensions, Pressable } from 'react-native'; +import type { ElevationStyles } from '~tokens/global/elevation'; +import BaseBox from '~components/Box/BaseBox'; +import { getElevationValue } from '~components/Box/BaseBox/baseBoxStyles'; +import { useTheme } from '~components/BladeProvider'; +import { makeAccessible } from '~utils/makeAccessible'; + +const fillStyle = { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, +}; + +// Outer animated wrapper carries the slide/opacity animation + elevation shadow. +// Shadow lives here (NOT on the inner surface) because the inner surface uses +// `overflow: 'hidden'` for the Android border-radius clip fix, which would clip the shadow. +const StyledDrawerWrapper = styled(Animated.View)(() => { + return { + position: 'absolute' as const, + top: 0, + bottom: 0, + right: 0, + width: '90%', + flexDirection: 'column' as const, + }; +}); + +const StyledDrawerSurface = styled(BaseBox)(({ theme }) => { + return { + flex: 1, + backgroundColor: theme.colors.popup.background.gray.subtle, + // base breakpoint renders the drawer edge-to-edge with no radius (matches web's phone view) + borderRadius: 0, + overflow: 'hidden' as const, + flexDirection: 'column' as const, + }; +}); + +const StyledOverlay = styled(Animated.View)(({ theme }) => { + return { + ...fillStyle, + backgroundColor: theme.colors.overlay.background.subtle, + }; +}); + +type AnimatedDrawerContainerProps = { + /** + * Drives the enter/exit animation. When `true` the drawer slides in and the + * overlay fades in; when `false` it slides out and fades away. + */ + isVisible: boolean; + /** + * Whether to render the dismissible overlay behind the drawer surface. + */ + showOverlay: boolean; + /** + * Called when the user presses the overlay to dismiss the drawer. + */ + onOverlayPress: () => void; + /** + * Called after the exit animation completes (parity with web `onUnmount`). + */ + onExitComplete?: () => void; + /** + * Accessibility label announced for the drawer dialog. + */ + accessibilityLabel?: string; + /** + * Whether this drawer is the first (bottom-most) in a stack of 2+ open drawers. + * When true, the resting translateX is offset slightly so the first drawer peeks + * out behind the stacked drawer — matching web's stacked-drawer positioning. + */ + isFirstDrawerInStack?: boolean; + /** + * Drawer content (DrawerHeader / DrawerBody / DrawerFooter). + */ + children: React.ReactNode; +}; + +/** + * Encapsulates the reanimated slide-in surface + fading overlay for the native + * Drawer. Mirrors the web `AnimatedDrawerContainer` styled component but drives + * `translateX`/`opacity` via reanimated shared values instead of CSS transitions. + */ +const AnimatedDrawerContainer = ({ + isVisible, + showOverlay, + onOverlayPress, + onExitComplete, + accessibilityLabel, + isFirstDrawerInStack = false, + children, +}: AnimatedDrawerContainerProps): React.ReactElement => { + const { theme } = useTheme(); + const screenWidth = Dimensions.get('window').width; + // Always initialize the shared values at the CLOSED / off-screen state + // (translateX = screenWidth, opacity = 0) so there is a "from" frame to animate FROM + // when the drawer mounts open. The Portal mounts fresh on open, so this container + // mounts with `isVisible = true`; the mount effect below then drives `withTiming` to + // the open state, producing the slide-in. + // + // Initializing from the current visibility instead (open → translateX 0) meant that on + // open the surface was already at its resting position, so the enter `withTiming(0)` + // had nothing to animate and the panel snapped into place instantly. The exit still + // animated (0 → screenWidth) which is why only the enter transition looked broken. + const translateX = useSharedValue(screenWidth); + const surfaceOpacity = useSharedValue(0); + const overlayOpacity = useSharedValue(0); + + const shadow = (getElevationValue('highRaised', theme) as unknown) as ElevationStyles; + + React.useEffect(() => { + // Mirror web's Drawer surface transition (Drawer.web.tsx): enter uses + // `duration.xmoderate` + `easing.entrance`, exit uses `duration.moderate` + + // `easing.exit`. Previously enter used the slower `gentle` (480ms), which made the + // native open feel sluggish compared to web's 360ms slide-in. + const enterConfig = { + duration: theme.motion.duration.xmoderate, + easing: theme.motion.easing.entrance, + }; + const exitConfig = { + duration: theme.motion.duration.moderate, + easing: theme.motion.easing.exit, + }; + const config = isVisible ? enterConfig : exitConfig; + + // When this drawer is the first in a stack (another drawer is open on top), + // offset the resting position slightly to the left so the first drawer peeks + // out behind the stacked drawer — matching web's stacked-drawer positioning. + const stackOffset = isFirstDrawerInStack ? -theme.spacing[5] : 0; + translateX.value = withTiming(isVisible ? stackOffset : screenWidth, config); + surfaceOpacity.value = withTiming(isVisible ? 1 : 0, config); + overlayOpacity.value = withTiming(isVisible ? 1 : 0, config, (finished) => { + if (finished && !isVisible && onExitComplete) { + runOnJS(onExitComplete)(); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible, screenWidth]); + + // Cancel all in-flight animations on unmount to prevent UI-thread worklets + // from continuing to animate shared values after the component is gone. + React.useEffect(() => { + return () => { + cancelAnimation(translateX); + cancelAnimation(surfaceOpacity); + cancelAnimation(overlayOpacity); + }; + }, [translateX, surfaceOpacity, overlayOpacity]); + + const surfaceAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: surfaceOpacity.value, + transform: [{ translateX: translateX.value }], + }; + }); + + const overlayAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: overlayOpacity.value, + }; + }); + + return ( + <> + {showOverlay ? ( + + + + ) : null} + + + {children} + + + + ); +}; + +export { AnimatedDrawerContainer }; diff --git a/packages/blade/src/components/Drawer/Drawer.native.tsx b/packages/blade/src/components/Drawer/Drawer.native.tsx index c7b128bd74..4b0ba43aaf 100644 --- a/packages/blade/src/components/Drawer/Drawer.native.tsx +++ b/packages/blade/src/components/Drawer/Drawer.native.tsx @@ -1,14 +1,234 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react'; +import { Portal } from '@gorhom/portal'; +import { AccessibilityInfo, findNodeHandle } from 'react-native'; +import { AnimatedDrawerContainer } from './AnimatedDrawerContainer.native'; +import { drawerComponentIds } from './drawerComponentIds'; +import { DrawerContext } from './DrawerContext'; import type { DrawerProps } from './types'; -import { Text } from '~components/Typography'; -import { throwBladeError } from '~utils/logger'; +import BaseBox from '~components/Box/BaseBox'; +import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; +import { componentZIndices } from '~utils/componentZIndices'; +import { useDrawerStack, StackingContext } from '~components/Drawer/StackProvider'; +import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import { useId } from '~utils/useId'; +import { useVerifyAllowedChildren } from '~utils/useVerifyAllowedChildren'; +import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; -const Drawer = (_props: DrawerProps): React.ReactElement => { - throwBladeError({ - message: 'Drawer is not yet implemented for native', - moduleName: 'Drawer', +const focusOnElement = (element: React.Component | null): void => { + if (!element) return; + const reactTag = findNodeHandle(element); + if (reactTag) { + AccessibilityInfo.setAccessibilityFocus(reactTag); + } +}; + +const _Drawer = ({ + isOpen, + onDismiss, + onUnmount, + zIndex = componentZIndices.drawer, + children, + accessibilityLabel, + showOverlay = true, + initialFocusRef, + isLazy = true, + testID, + ...rest +}: DrawerProps): React.ReactElement | null => { + const [zIndexState, setZIndexState] = React.useState(zIndex); + const closeButtonRef = React.useRef(null); + + useVerifyAllowedChildren({ + children, + componentName: 'Drawer', + allowedComponents: [ + drawerComponentIds.DrawerHeader, + drawerComponentIds.DrawerBody, + drawerComponentIds.DrawerFooter, + ], }); - return Drawer Component is not available for Native mobile apps.; + const drawerId = useId('drawer'); + const { drawerStack, addToDrawerStack, removeFromDrawerStack } = useDrawerStack(); + + // Native presence handling: `use-presence` (web) relies on `document`, so we + // gate mount/unmount locally instead. + // - isMounted stays true through the exit animation, then flips false via onExitComplete. + // - isVisible mirrors isOpen and drives the reanimated slide/opacity. + // - isExiting is true while the drawer is animating out (mounted but not open). + const [isMounted, setIsMounted] = React.useState(isOpen || !isLazy); + const isVisible = isOpen; + const isExiting = isMounted && !isOpen; + const hasEverOpenedRef = React.useRef(isOpen); + + React.useEffect(() => { + if (isOpen) { + hasEverOpenedRef.current = true; + setIsMounted(true); + } + }, [isOpen]); + + const wasMountedRef = React.useRef(false); + + React.useEffect(() => { + wasMountedRef.current = isMounted; + }, [isMounted]); + + const handleExitComplete = React.useCallback(() => { + // Flip `isMounted` false so the Portal unmounts after the exit animation. Guard + // the `onUnmount` side-effect behind the previous-mounted flag so it only fires + // when the drawer had actually been opened and is now finishing its exit — never + // on the initial closed render. + // + // The onUnmount call is intentionally OUTSIDE the state updater to keep the + // updater pure (React StrictMode double-invokes updaters in development). + if (wasMountedRef.current && hasEverOpenedRef.current) { + onUnmount?.(); + } + setIsMounted(false); + }, [onUnmount]); + + const { stackingLevel, isFirstDrawerInStack } = React.useMemo(() => { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + const level = Object.keys(drawerStack).indexOf(drawerId) + 1; + return { + stackingLevel: level, + isFirstDrawerInStack: level === 1 && Object.keys(drawerStack).length > 1, + }; + }, [drawerId, drawerStack]); + + React.useEffect(() => { + if (isOpen) { + addToDrawerStack({ elementId: drawerId, onDismiss }); + // Move accessibility focus to the requested element (parity with web initialFocus) + focusOnElement(initialFocusRef?.current ?? closeButtonRef.current ?? null); + } else { + removeFromDrawerStack({ elementId: drawerId }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + // When z-index is not defined by user, we use default drawer z index and add stackingLevel to ensure + // new drawer that opens, always opens on top of previous one. + React.useEffect(() => { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + setZIndexState(zIndex + stackingLevel); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isMounted]); + + const contextValue = React.useMemo( + () => ({ + close: onDismiss, + closeButtonRef, + stackingLevel, + isExiting, + }), + [isExiting, onDismiss, stackingLevel], + ); + + // `@gorhom/portal` teleports children into `PortalHost`, which lives OUTSIDE this + // component's React subtree. Teleported children therefore lose access to the + // contexts provided here. We re-provide both the DrawerStack context and the + // Drawer context INSIDE the Portal so DrawerHeader (rendered via the portal) can + // read the up-to-date `drawerStack`, `close`, and `stackingLevel`. + // (BottomSheet.native uses the same re-provide-inside-Portal workaround.) + const stackContextValue = React.useMemo( + () => ({ + drawerStack, + addToDrawerStack, + removeFromDrawerStack, + }), + [drawerStack, addToDrawerStack, removeFromDrawerStack], + ); + + // `@gorhom/portal` teleports children into the host via an effect that runs when the + // `Portal` element itself mounts. The reliable, proven pattern (used by the working + // `Popover.native` / `Tooltip.native` against this same `BladeBottomSheetPortal` host) + // is to conditionally MOUNT the `Portal` while the drawer is present rather than keep + // it always mounted and flip a visibility prop. Mounting fresh on open guarantees: + // - the add-portal effect fires so the subtree actually teleports and renders, and + // - `AnimatedDrawerContainer` re-mounts with `isVisible = true`, so its shared values + // initialize at the CLOSED / off-screen state (translateX = screenWidth, opacity 0) + // and its mount effect animates them to the open state — producing the slide-in. + // Presence is driven by `isMounted`: set true on open, flipped false by + // `handleExitComplete` after the exit animation so the slide-out still plays before + // the Portal unmounts. `children` render whenever the drawer is mounted. + return isMounted || !isLazy ? ( + + + + + + {children} + + + + + + ) : null; }; +/** + * ### Drawer Component + * + * A drawer is a panel that slides in mostly from right side of the screen over the existing content in the viewport. + * It helps in providing additional details or context and can also be used to promote product features or new products. + * + * --- + * + * #### Usage + * + * ```jsx + const MyDrawer = () => { + const [showDrawer, setShowDrawer] = React.useState(false); + return ( + + + setShowDrawer(false)} + > + + + + + + + + ) + } + * ``` + * + * --- + * + * Checkout {@link https://blade.razorpay.com/?path=/docs/components-drawer Drawer Documentation} + * + * + */ +const Drawer = assignWithoutSideEffects(_Drawer, { + displayName: 'Drawer', + componentId: drawerComponentIds.Drawer, +}); + export { Drawer }; diff --git a/packages/blade/src/components/Drawer/Drawer.web.tsx b/packages/blade/src/components/Drawer/Drawer.web.tsx index 3fb2399a78..711a6238f2 100644 --- a/packages/blade/src/components/Drawer/Drawer.web.tsx +++ b/packages/blade/src/components/Drawer/Drawer.web.tsx @@ -254,7 +254,7 @@ const _Drawer: React.ForwardRefRenderFunction = ( - + ) } diff --git a/packages/blade/src/components/Drawer/DrawerSubcomponents.native.tsx b/packages/blade/src/components/Drawer/DrawerSubcomponents.native.tsx index e57ee02e9f..7276596c8b 100644 --- a/packages/blade/src/components/Drawer/DrawerSubcomponents.native.tsx +++ b/packages/blade/src/components/Drawer/DrawerSubcomponents.native.tsx @@ -1,33 +1,174 @@ import React from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import Svg, { Defs, RadialGradient, Stop, Rect } from 'react-native-svg'; +import { drawerComponentIds } from './drawerComponentIds'; +import { DrawerContext } from './DrawerContext'; import type { DrawerHeaderProps, DrawerFooterProps } from './types'; -import { Text } from '~components/Typography'; -import { throwBladeError } from '~utils/logger'; +import { useDrawerStack } from './StackProvider'; +import { BaseHeader } from '~components/BaseHeaderFooter/BaseHeader'; +import { BaseFooter } from '~components/BaseHeaderFooter/BaseFooter'; +import { Box } from '~components/Box'; +import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; +import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; +import { useId } from '~utils/useId'; +import { useTheme } from '~utils'; -const DrawerHeader = (_props: DrawerHeaderProps): React.ReactElement => { - throwBladeError({ - message: 'DrawerHeader is not yet implemented for native', - moduleName: 'DrawerHeader', - }); +/** + * Replicates the web DrawerHeader's radial-gradient background on native. + * + * Web uses a CSS `radial-gradient(150% 100% at 50% 100%, transparent 0%, subtle 100%)` + * driven by the `color` prop. React Native has no CSS radial-gradient, so we draw an + * equivalent gradient with react-native-svg using the same status-driven feedback token. + */ +const DrawerHeaderGradient = ({ + color, +}: { + color: NonNullable; +}): React.ReactElement => { + const { theme } = useTheme(); + const uniqueId = useId('drawer-header-gradient'); + // Web's `feedback.background[color].subtle` token is a low-opacity hsla() (e.g. 0.18 alpha). + // Web applies it as the far stop of a radial-gradient that fades from `transparent`, so the + // visible tint never exceeds that alpha (hence it looks very light). react-native-svg's + // does not honor the alpha channel embedded in a color string, so we split the token into its + // opaque hue + explicit `stopOpacity` to reproduce web's light tint exactly (no magic values). + const subtleColor = theme.colors.feedback.background[color].subtle; + const alphaMatch = subtleColor.match(/hsla?\([^)]*,\s*([\d.]+)\s*\)$/); + const subtleAlpha = alphaMatch ? Number(alphaMatch[1]) : 1; + const opaqueColor = subtleColor.replace(/^hsla/, 'hsl').replace(/,\s*[\d.]+\s*\)$/, ')'); + const gradientId = `${uniqueId}-${color}`; - return Drawer Component is not available for Native mobile apps.; + return ( + + + + + + + + + + + + ); }; -const DrawerBody = (_props: { children: React.ReactNode }): React.ReactElement => { - throwBladeError({ - message: 'DrawerBody is not yet implemented for native', - moduleName: 'DrawerBody', - }); +const _DrawerHeader = ({ + title, + subtitle, + leading, + trailing, + titleSuffix, + children, + // `color` drives the radial-gradient background on web. Native has no CSS + // radial-gradient, so we replicate it with an equivalent react-native-svg gradient + // (see DrawerHeaderGradient) using the same status-driven feedback token. + color = 'information', + showDivider = true, + ...rest +}: DrawerHeaderProps): React.ReactElement => { + const { close, closeButtonRef, stackingLevel, isExiting } = React.useContext(DrawerContext); + const { drawerStack } = useDrawerStack(); - return Drawer Component is not available for Native mobile apps.; + const closeAllDrawers = (): void => { + for (const onDismiss of Object.values(drawerStack)) { + onDismiss(); + } + }; + + const isStackedDrawer = stackingLevel && stackingLevel > 1; + + const isAtLeastOneDrawerOpen = Object.keys(drawerStack).length > 0; + + // This condition is to avoid back button disappear while stacked drawer is in the exiting transition + const isDrawerExiting = isAtLeastOneDrawerOpen && isExiting && stackingLevel !== 1; + + return ( + + + closeAllDrawers()} + onBackButtonClick={() => close()} + title={title} + size="xlarge" + titleSuffix={titleSuffix} + subtitle={subtitle} + leading={leading} + trailing={trailing} + showDivider={showDivider} + {...makeAnalyticsAttribute(rest)} + > + {children} + + + ); }; -const DrawerFooter = (_props: DrawerFooterProps): React.ReactElement => { - throwBladeError({ - message: 'DrawerFooter is not yet implemented for native', - moduleName: 'DrawerFooter', - }); +/** + * #### Usage + * + * ```jsx + * New} + * leading={} + * trailing={ + * + * ``` + * + */ +const DrawerFooter = assignWithoutSideEffects(_DrawerFooter, { + componentId: drawerComponentIds.DrawerFooter, +}); + +export { DrawerHeader, DrawerBody, DrawerFooter, drawerPadding }; diff --git a/packages/blade/src/components/Drawer/StackProvider.tsx b/packages/blade/src/components/Drawer/StackProvider.tsx index 0270dccdb9..5a75284a8e 100644 --- a/packages/blade/src/components/Drawer/StackProvider.tsx +++ b/packages/blade/src/components/Drawer/StackProvider.tsx @@ -70,4 +70,4 @@ const useDrawerStack = (): GlobalStackStateType => { return React.useContext(StackingContext); }; -export { DrawerStackProvider, useDrawerStack }; +export { DrawerStackProvider, useDrawerStack, StackingContext }; diff --git a/packages/blade/src/components/Drawer/__tests__/Drawer.native.test.tsx b/packages/blade/src/components/Drawer/__tests__/Drawer.native.test.tsx new file mode 100644 index 0000000000..42c1dffa29 --- /dev/null +++ b/packages/blade/src/components/Drawer/__tests__/Drawer.native.test.tsx @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import type { DrawerProps } from '../'; +import { Drawer, DrawerBody, DrawerHeader, DrawerFooter } from '../'; +import renderWithTheme from '~utils/testing/renderWithTheme.native'; +import { Badge } from '~components/Badge'; +import { Button } from '~components/Button'; +import { Text } from '~components/Typography'; +import { AnnouncementIcon, DownloadIcon } from '~components/Icons'; + +jest.mock('~utils/useId', () => ({ + useId: (prefix?: string) => (prefix ? `${prefix}-0` : '0'), +})); + +jest.useFakeTimers(); + +beforeAll(() => jest.spyOn(console, 'error').mockImplementation()); +afterAll(() => jest.restoreAllMocks()); + +const BasicDrawer = (props: Partial): React.ReactElement => { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + { + setIsOpen(false); + props.onDismiss?.(); + }} + accessibilityLabel="Test Drawer" + > + + + Test Content + + + + + ); +}; + +describe(' (native)', () => { + it('renders a Drawer', () => { + const { toJSON } = renderWithTheme( + {}} accessibilityLabel="Test Drawer"> + } + title="Address Details" + subtitle="Saving addresses will improve your checkout experience" + trailing={ + + , + ); + expect(getByText('Footer Button')).toBeTruthy(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/blade/src/components/Drawer/__tests__/__snapshots__/Drawer.native.test.tsx.snap b/packages/blade/src/components/Drawer/__tests__/__snapshots__/Drawer.native.test.tsx.snap new file mode 100644 index 0000000000..584304a38f --- /dev/null +++ b/packages/blade/src/components/Drawer/__tests__/__snapshots__/Drawer.native.test.tsx.snap @@ -0,0 +1,2905 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` (native) renders a Drawer 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Address Details + + + + + + + + NEW + + + + + + + + + Saving addresses will improve your checkout experience + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Content + + + + + + + + + +`; + +exports[` (native) renders a Drawer with footer 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom Header + + + + + + + + + + + + + Custom Content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Footer Button + + + + + + + + + + +`; + +exports[` (native) should render a Drawer with a custom header 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom Header + + + + + + + + + + + + + Custom Content + + + + + + + + + +`; diff --git a/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx b/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx index b3695ca30b..46be1d1841 100644 --- a/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx +++ b/packages/blade/src/components/Drawer/docs/Drawer.stories.tsx @@ -71,7 +71,7 @@ const DrawerTemplate: StoryFn = (args) => { trailing={{' '} - + + + + + + + @@ -119,7 +123,7 @@ export const DrawerStacking = (args: DrawerProps): React.ReactElement => { trailing={{' '} - + + + + + + + @@ -260,7 +270,13 @@ export const WithCustomHeader = (args: DrawerProps): React.ReactElement => { suffix="decimals" /> - + Captured @@ -280,14 +296,20 @@ export const WithCustomHeader = (args: DrawerProps): React.ReactElement => { - - + + Created on Jan 11, 2025 - + Starters{"'"} CFP Private Limited Vendor @@ -305,13 +327,17 @@ export const WithCustomHeader = (args: DrawerProps): React.ReactElement => { - - - + + + + + + + @@ -373,7 +399,7 @@ export const WithFooter = (args: DrawerProps): React.ReactElement => { Personal Information - + John Doe Individual @@ -566,13 +592,17 @@ export const WithFooter = (args: DrawerProps): React.ReactElement => { {showFooter && ( - - - + + + + + + + )}