feat(rn): add React Native support for Drawer - #3645
Conversation
Implements .native.tsx files for the Drawer component, replacing the stub implementations with real native rendering using styled-components/native and react-native-reanimated. - Add AnimatedDrawerContainer.native.tsx (reanimated slide-in/overlay) - Implement Drawer.native.tsx via conditional @gorhom/portal mount (mirrors the working Popover/Tooltip native pattern) - Implement DrawerSubcomponents.native.tsx (header/body) - Register a BladeBottomSheetPortal PortalHost in the RN Storybook preview so portal-based components render in Storybook, and add flex:1 sizing - Fix invalid bare string children in Drawer.stories.tsx (native crash) - Add native tests + snapshot Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 43f2425 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
🤖 Slash AI Review has been triggered. View execution logs |
🛡️ Coverage ReportSummaryFull Coverage Details |
|
(Review Cancelled - Superseded by a new run) |
- Remove empty noop arrow function, guard onExitComplete call directly - Rename unused isLazy to ignoredIsLazy to satisfy no-unused-vars rule Co-authored-by: admin <admin>
|
🤖 Slash AI Review has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
❌ 2 failed
| Check | Problem |
|---|---|
| iOS Drawer visual verification | No screenshots or videos are embedded in the PR body to visually verify the iOS Drawer implementation. Only a textual description of manual testing is provided, which is insufficient for ui-critique verification of React Native components. |
| Android Drawer visual verification | Android visual verification was not performed. No screenshots or videos demonstrating Drawer behavior on Android are provided. |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
const MyDrawer = () => {
const [showDrawer, setShowDrawer] = React.useState(false);
return (
<Box>
<Button onClick={() => setShowDrawer(true)}>Open Drawer</Button>
<Drawer
isOpen={showDrawer}
onDismiss={() => setShowDrawer(false)}
>
<DrawerHeader title="Announcements" />
<DrawerBody>
<Text>Content</Text>
</DrawerBody>
</Drawer>
</Box>
);
};Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
❌ 1 failed
| Check | Problem |
|---|---|
| React Native Drawer - visual verification | No screenshots / videos found in PR_BODY (required for react native components) |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
const MyDrawer = () => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<Box>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
>
<DrawerHeader title="Announcements" />
<DrawerBody>
<Text>Content</Text>
</DrawerBody>
</Drawer>
</Box>
);
};| accessibilityLabel, | ||
| showOverlay = true, | ||
| initialFocusRef, | ||
| isLazy: _isLazy = true, |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 7/10
Problem: isLazy prop is silently ignored on native
Suggestion: The prop is destructured as _isLazy and never used, so isLazy={false} (keep drawer mounted when closed, a documented API contract) has no effect on native. Either implement the behaviour — keep isMounted true when isLazy=false even after close — or throw/warn so callers are not silently misled.
|
|
||
| React.useEffect(() => { | ||
| if (isOpen) { | ||
| addToDrawerStack({ elementId: drawerId, onDismiss }); |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: closeButtonRef not provided in native DrawerContext value
Suggestion: DrawerSubcomponents.native.tsx reads closeButtonRef from DrawerContext and forwards it to BaseHeader, but Drawer.native.tsx never populates it. Create a local const closeButtonRef = React.useRef(null) in _Drawer and include it in contextValue, mirroring the web implementation, so BaseHeader can attach the ref to its close button.
…een width [resolved by agent] Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
✅ 1 passed · ❌ 1 failed
| Check | Problem |
|---|---|
| React Native Drawer - Visual Verification | No screenshots / videos found in PR_BODY (required for react native components) |
Passing checks (1)
| Check |
|---|
| ✅ Web Storybook - Drawer Story Fix |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
const MyDrawer = () => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Drawer</Button>
<Drawer
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
showOverlay={true}
accessibilityLabel="My Drawer"
isLazy={true}
testID="my-drawer"
>
<DrawerHeader
title="Announcements"
subtitle="What's new"
leading={<AnnouncementIcon size="large" />}
trailing={<Button icon={DownloadIcon} />}
titleSuffix={<Badge color="positive">NEW</Badge>}
showDivider={true}
/>
<DrawerBody>
<Text>Drawer content here</Text>
</DrawerBody>
<DrawerFooter showDivider={true}>
<Button variant="primary" isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>
</>
);
};- Initialize the surface shared values at the closed/off-screen state so the slide-in has a frame to animate from (enter was snapping into place) - Mirror web's Drawer transition timings (xmoderate/entrance, moderate/exit) - Use Dimensions.get for screen width and a noop exit callback fallback - Refine DrawerSubcomponents native layout and update the Drawer stories - Regenerate Drawer native snapshots Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Slash AI Review has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
❌ 1 failed
| Check | Problem |
|---|---|
| Visual Verification | No screenshots / videos found in PR_BODY (required for react native components) |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
const MyDrawer = () => {
const [showDrawer, setShowDrawer] = React.useState(false);
return (
<Box>
<Button onPress={() => setShowDrawer(true)}>Open Drawer</Button>
<Drawer
isOpen={showDrawer}
onDismiss={() => setShowDrawer(false)}
accessibilityLabel="Announcements drawer"
showOverlay={true}
isLazy={true}
testID="my-drawer"
>
<DrawerHeader
title="Announcements"
subtitle="What's new"
leading={<DrawerHeaderIcon icon={AnnouncementIcon} />}
trailing={<Button icon={DownloadIcon} />}
titleSuffix={<DrawerHeaderBadge>New</DrawerHeaderBadge>}
color="information"
showDivider={true}
/>
<DrawerBody>
<Text>Content here</Text>
</DrawerBody>
<DrawerFooter showDivider={true}>
<Button isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>
</Box>
);
};There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
✅ 2 passed · ❌ 1 failed
| Check | Problem | Screenshot |
|---|---|---|
| React Native Drawer - Visual Verification |
Passing checks (2)
| Check | Screenshot |
|---|---|
| ✅ Web Drawer Stories - Simple Drawer (flexDirection fix) | ![]() |
| ✅ Web Drawer Stories - Drawer Stacking (flexDirection fix) | ![]() |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
const MyDrawer = () => {
const [showDrawer, setShowDrawer] = React.useState(false);
return (
<Box>
<Button onClick={() => setShowDrawer(true)}>Open Drawer</Button>
<Drawer
isOpen={showDrawer}
onDismiss={() => setShowDrawer(false)}
>
<DrawerHeader title="Announcements" />
<DrawerBody>
<Text>Content</Text>
</DrawerBody>
<DrawerFooter>
<Button isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>
</Box>
);
}- Add closeButtonRef useRef and include it in DrawerContext - Fall back to closeButtonRef when focusing on open - Honor isLazy: initialize isMounted with isOpen || !isLazy - Guard onUnmount behind hasEverOpenedRef - Update isLazy test for web parity
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
🔗 Storybook: Preview
✅ 7 passed · ❌ 5 failed · ⏭️ 1 skipped
| Check | Problem |
|---|---|
| Stacking Offset Visual Parity | Missing isFirstDrawerInStack translateX offset in native AnimatedDrawerContainer — stacked drawers will look visually different from web Suggestion: Pass stackingLevel/isFirstDrawerInStack from Drawer.native.tsx into AnimatedDrawerContainer and apply a translateX offset when a drawer is the first in a stack of 2+. |
| Overlay Animation Timing Parity | Overlay animation durations differ between web and native Suggestion: Use separate timing configs for overlay vs surface to match web's DrawerOverlay styled component. |
| DrawerHeaderGradient hsla Color Parsing | Fragile regex-based hsla color parsing in DrawerHeaderGradient — silently degrades to full opacity if token format changes Suggestion: Add a DEV warning when the regex doesn't match, or use a structured color parser. |
| Screen Dimension Change Handling | No Dimensions change listener — screenWidth is captured once at mount and never updated for rotation/resize Suggestion: Use useWindowDimensions() hook from 'react-native' which re-renders on dimension changes. |
| Scroll Lock Parity | No scroll lock on native — background content can scroll while drawer is open Suggestion: Consider intercepting touch events on the overlay or using a modal-level interaction lock. |
Passing checks (7)
| Check |
|---|
| ✅ Storybook Publish |
| ✅ Chromatic Deployment |
| ✅ DrawerFooter Sticky Position Parity |
| ✅ Surface Animation Duration Parity |
| ✅ Shadow/Elevation Handling |
| ✅ Portal and Context Re-provision |
| ✅ Accessibility Parity |
CI / Sanity
✅ 12 passed
Passing checks (12)
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade';
const MyDrawer = () => {
const [showDrawer, setShowDrawer] = React.useState(false);
return (
<>
<Button onClick={() => setShowDrawer(true)}>Open Drawer</Button>
<Drawer
isOpen={showDrawer}
onDismiss={() => setShowDrawer(false)}
onUnmount={() => console.log('Drawer closed and unmounted')}
showOverlay={true}
accessibilityLabel="Announcements drawer"
>
<DrawerHeader title="Announcements" subtitle="What's new" />
<DrawerBody>
<Text>Drawer content goes here</Text>
</DrawerBody>
<DrawerFooter>
<Button variant="primary" isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>
</>
);
};|
|
||
| const shadow = (getElevationValue('highRaised', theme) as unknown) as ElevationStyles; | ||
|
|
||
| React.useEffect(() => { |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 9/10
Problem: The useEffect that drives withTiming animations has no cleanup function. Every other reanimated component in the codebase (Spinner, ProgressBar, Skeleton) calls cancelAnimation on each shared value in the useEffect cleanup to prevent the UI-thread worklet from continuing to animate after unmount. If AnimatedDrawerContainer unmounts mid-animation (e.g. rapid open/close toggle), the withTiming worklets on translateX, surfaceOpacity, and overlayOpacity continue running on the UI thread with no component to update, risking memory leaks or native crashes.
Suggestion: Add a cleanup return to the useEffect that cancels all three shared values: cancelAnimation(translateX); cancelAnimation(surfaceOpacity); cancelAnimation(overlayOpacity); Also add cancelAnimation to the import from 'react-native-reanimated'.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: Added cancelAnimation to the reanimated import and a cleanup useEffect that cancels all three shared values (translateX, surfaceOpacity, overlayOpacity) on unmount, matching the pattern used by other reanimated components (Spinner, ProgressBar, Skeleton).
React.useEffect(() => {
return () => {
cancelAnimation(translateX);
cancelAnimation(surfaceOpacity);
cancelAnimation(overlayOpacity);
};
}, [translateX, surfaceOpacity, overlayOpacity]);[resolved by agent]
| // 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. | ||
| setIsMounted((prevIsMounted) => { |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 8/10
Problem: handleExitComplete calls onUnmount?.() inside the setIsMounted state updater function. React state updater functions should be pure — in StrictMode (development), React intentionally double-invokes updaters to detect side effects, which would cause onUnmount to fire twice.
Suggestion: Move the onUnmount call outside the updater. Use a ref to track the previous mounted state, then call onUnmount and setIsMounted separately.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: Moved the onUnmount?.() call outside the setIsMounted state updater. Now uses a wasMountedRef (synced via a useEffect) to track the previous mounted state, and calls onUnmount and setIsMounted(false) as separate statements. The state updater is now pure, avoiding the StrictMode double-invocation issue.
const wasMountedRef = React.useRef(false);
React.useEffect(() => {
wasMountedRef.current = isMounted;
}, [isMounted]);
const handleExitComplete = React.useCallback(() => {
if (wasMountedRef.current && hasEverOpenedRef.current) {
onUnmount?.();
}
setIsMounted(false);
}, [onUnmount]);[resolved by agent]
| }; | ||
| }, [drawerId, drawerStack]); | ||
|
|
||
| React.useEffect(() => { |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 7/10
Problem: There is no hardware back button (Android BackHandler) handling. The web version handles Escape key to dismiss the drawer. On Android, users expect the hardware back button to close an open drawer. Without a BackHandler subscription, pressing back while a drawer is open will navigate away from the screen instead of closing the drawer.
Suggestion: Add a BackHandler subscription inside the isOpen effect: const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { onDismiss(); return true; }); return () => backHandler.remove();
| }); | ||
| }, [onUnmount]); | ||
|
|
||
| const { stackingLevel } = React.useMemo(() => { |
There was a problem hiding this comment.
🔵 [MINOR] · api-decision-critique · confidence: 8/10
Problem: Missing isFirstDrawerInStack computation. Web computes this and passes it to AnimatedDrawerContainer to apply different translateX offsets for stacked drawers. Native omits it entirely, so stacked drawers will have identical positioning to single drawers — losing the visual offset that distinguishes stacked drawers on web.
Suggestion: Compute isFirstDrawerInStack the same way as web and pass it to AnimatedDrawerContainer to adjust translateX target for stacked drawer positioning parity.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: Added isFirstDrawerInStack computation in Drawer.native.tsx (matching web's logic exactly: level === 1 && Object.keys(drawerStack).length > 1) and passed it as a new prop to AnimatedDrawerContainer. In the animated container, when isFirstDrawerInStack is true and the drawer is visible, the resting translateX target is offset by -theme.spacing[5] (16px, matching web's base breakpoint offset) so the first drawer peeks out behind the stacked drawer.
// Drawer.native.tsx
const { stackingLevel, isFirstDrawerInStack } = React.useMemo(() => {
const level = Object.keys(drawerStack).indexOf(drawerId) + 1;
return {
stackingLevel: level,
isFirstDrawerInStack: level === 1 && Object.keys(drawerStack).length > 1,
};
}, [drawerId, drawerStack]);
// AnimatedDrawerContainer.native.tsx
const stackOffset = isFirstDrawerInStack ? -theme.spacing[5] : 0;
translateX.value = withTiming(isVisible ? stackOffset : screenWidth, config);[resolved by agent]
| bottom: 0, | ||
| right: 0, | ||
| width: '90%', | ||
| flexDirection: 'column' as const, |
There was a problem hiding this comment.
🔵 [MINOR] · api-decision-critique · confidence: 7/10
Problem: Drawer width is hardcoded to '90%' with no responsive breakpoints. Web uses width={{ base: '90%', s: '375px', m: '420px' }} to cap the drawer width on larger screens. On RN tablet or wide-window contexts, the native drawer will be significantly wider than its web counterpart.
Suggestion: Use the theme's breakpoint system or Dimensions-based logic to apply responsive width matching web's responsive width tokens.
| children, | ||
| }: AnimatedDrawerContainerProps): React.ReactElement => { | ||
| const { theme } = useTheme(); | ||
| const screenWidth = Dimensions.get('window').width; |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 7/10
Problem: Dimensions.get('window').width is captured once at mount and stored in screenWidth. If the device orientation changes (portrait↔landscape) or the window is resized, the drawer will use a stale screen width for its slide animation distance.
Suggestion: Replace with useWindowDimensions() hook from 'react-native' which re-renders on dimension changes.
| // 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*\)$/); |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 7/10
Problem: The hsla color parsing uses regex to extract the alpha channel from the theme token string. This is fragile — if the theme format ever changes, the regex silently fails and subtleAlpha defaults to 1, producing a fully opaque gradient instead of the intended subtle tint.
Suggestion: Add a __DEV__ warning when the regex doesn't match, so theme format changes are caught early.
| return <Text>Drawer Component is not available for Native mobile apps.</Text>; | ||
| const drawerPadding = 'spacing.6'; | ||
|
|
||
| const _DrawerBody = ({ children }: { children: React.ReactNode }): React.ReactElement => { |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: DrawerBody uses a plain ScrollView without keyboardDismissMode. When a user has focused a TextInput inside the drawer body and then scrolls, the keyboard remains open on iOS, covering the content below.
Suggestion: Add keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled" to the ScrollView.
|
|
||
| const isStackedDrawer = stackingLevel && stackingLevel > 1; | ||
|
|
||
| const isAtleastOneDrawerOpen = Object.keys(drawerStack).length > 0; |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 9/10
Problem: Variable name isAtleastOneDrawerOpen has a capitalization typo — should be isAtLeastOneDrawerOpen (capitalize the 'L' in 'Least').
Suggestion: Rename to isAtLeastOneDrawerOpen.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: Renamed isAtleastOneDrawerOpen to isAtLeastOneDrawerOpen (capitalized the 'L' in 'Least').
[resolved by agent]
| removeFromDrawerStack({ elementId: drawerId }); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isOpen]); |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: The effect that adds/removes the drawer from the stack has no cleanup function. If the Drawer component unmounts while isOpen is true (e.g. parent conditionally renders), removeFromDrawerStack is never called, leaving a stale entry in drawerStack.
Suggestion: Add a cleanup return to the effect: return () => { removeFromDrawerStack({ elementId: drawerId }); };
Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
🔗 Storybook: Preview
✅ 5 passed
Passing checks (5)
| Check |
|---|
| ✅ Drawer Default story (Simple Drawer) |
| ✅ Drawer Stacking story |
| ✅ Initial Focus story |
| ✅ With Custom Header story |
| ✅ With Footer story |
CI / Sanity
✅ 6 passed · ❌ 4 failed · ⏭️ 1 skipped
| Check | Problem |
|---|---|
| Run Tests (1) | Test shard 1 failed. Logs not yet available (run still in progress). Suggestion: Re-run the failed job or check logs once the run completes. |
| Validate Source Code | |
| Knowledgebase Lint | |
| Chromatic Deployment |
Passing checks (6)
| Check |
|---|
| ✅ Run Tests (2) |
| ✅ Run Tests (3) |
| ✅ Run Tests (4) |
| ✅ Storybook Publish: blade |
| ✅ PR Title Check |
| ✅ semgrep-cloud-platform/scan |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={true} onDismiss={() => {}}>
<DrawerHeader title="Title" />
<DrawerBody>Content</DrawerBody>
</Drawer>| children, | ||
| }: AnimatedDrawerContainerProps): React.ReactElement => { | ||
| const { theme } = useTheme(); | ||
| const screenWidth = Dimensions.get('window').width; |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 7/10
Problem: Dimensions.get('window').width is not reactive to screen dimension changes (rotation, fold/unfold, split-screen). If the screen rotates while the drawer is open, the exit animation translates to the stale portrait width.
Suggestion: Replace with useWindowDimensions() for reactivity.
| // 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*\)$/); |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: The hsla regex matches both hsl() and hsla() formats. For an hsl() color without alpha, the regex captures lightness as alpha. Currently safe because feedback.background tokens are always hsla().
Suggestion: Require the 'a' in the regex: /hsla([^)],\s([\d.]+)\s*)$/
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 1 passed
Passing checks (1)
| Check |
|---|
| ✅ Drawer native - basic open/close (from PR video) |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={showDrawer} onDismiss={() => setShowDrawer(false)}>
<DrawerHeader title="Title" />
<DrawerBody>Content</DrawerBody>
<DrawerFooter>Footer</DrawerFooter>
</Drawer>
it was intentional as there is not enough space in mobile for giving slight visibility to the first drawer , had this discussion with @rkdotxyz as well |
Resolve comment conflict in RN Storybook preview to mention both Drawer and BottomSheet portal content. Co-authored-by: Cursor <cursoragent@cursor.com>
41150b3
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={true} onDismiss={() => {}} accessibilityLabel="My Drawer">
<DrawerHeader title="Title" subtitle="Subtitle" />
<DrawerBody>Content</DrawerBody>
<DrawerFooter>
<Button variant="primary" isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
⏭️ 5 skipped
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={true} onDismiss={() => {}} accessibilityLabel="My Drawer">
<DrawerHeader title="Title" leading={<Icon />} />
<DrawerBody>Content</DrawerBody>
<DrawerFooter><Button>Save</Button></DrawerFooter>
</Drawer>|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
❌ 5 failed
| Check | Problem |
|---|---|
| DrawerTemplate (default Drawer story) | agent-browser could not launch Chrome (exit code 127: posix_fallocate64 symbol not found). Unable to visually verify the Drawer default story renders correctly after the flexDirection and flex-button layout changes. |
| DrawerStacking | agent-browser could not launch Chrome (exit code 127: posix_fallocate64 symbol not found). Unable to visually verify the DrawerStacking story. |
| InitialFocus | agent-browser could not launch Chrome (exit code 127: posix_fallocate64 symbol not found). Unable to visually verify the InitialFocus story. |
| WithCustomHeader | agent-browser could not launch Chrome (exit code 127: posix_fallocate64 symbol not found). Unable to visually verify the WithCustomHeader story. |
| WithFooter | agent-browser could not launch Chrome (exit code 127: posix_fallocate64 symbol not found). Unable to visually verify the WithFooter story. |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={true} onDismiss={() => {}} accessibilityLabel="My Drawer">
<DrawerHeader title="Title" />
<DrawerBody>Content</DrawerBody>
<DrawerFooter>Footer</DrawerFooter>
</Drawer>Resolve Storybook preview comment conflict keeping Drawer, BottomSheet, and DatePicker mentions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
(Review Cancelled - Superseded by a new run) |
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/blade/.storybook/react-native/preview.tsx
|
🤖 Slash AI Review has been triggered. View execution logs |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
✅ 5 passed
Passing checks (5)
| Check | Screenshot |
|---|---|
| ✅ DrawerTemplate (Simple Drawer / Default) | ![]() |
| ✅ DrawerStacking | ![]() |
| ✅ InitialFocus | ![]() |
| ✅ WithCustomHeader | ![]() |
| ✅ WithFooter | ![]() |
Usage
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@razorpay/blade/components';
<Drawer isOpen={showDrawer} onDismiss={() => setShowDrawer(false)}>
<DrawerHeader title="Announcements" />
<DrawerBody>Content</DrawerBody>
<DrawerFooter>
<Button variant="primary" isFullWidth>Continue</Button>
</DrawerFooter>
</Drawer>| * 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. | ||
| */ |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 8/10
Problem: The animation useEffect captures onExitComplete in the withTiming callback but excludes it from the dependency array [isVisible, screenWidth]. If onExitComplete (which wraps handleExitComplete → onUnmount) changes identity between renders, the animation's completion callback will invoke a stale closure. This can result in onUnmount never being called or being called with outdated state after the exit animation finishes.
Suggestion: Store onExitComplete in a ref to avoid re-triggering the animation on every identity change, or add it to the dependency array. The ref approach is preferable to avoid restarting the animation mid-flight.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: stored onExitComplete in a ref (onExitCompleteRef) that is kept in sync via a separate useEffect. The reanimated withTiming callback now reads onExitCompleteRef.current instead of the captured closure, so it always invokes the latest identity without re-triggering the animation. See PR #3754. [resolved by agent]
| */ | ||
| showOverlay: boolean; | ||
| /** | ||
| * Called when the user presses the overlay to dismiss the drawer. |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 7/10
Problem: Dimensions.get('window').width is called during render but never updated. If the device rotates or the window resizes (e.g., multitasking on iPad), screenWidth becomes stale and the slide-in/slide-out animation will use the wrong off-screen position.
Suggestion: Subscribe to dimension changes via Dimensions.addEventListener('change', ...) and update screenWidth in state, or use the useWindowDimensions hook from react-native which is reactive.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: replaced Dimensions.get('window').width with the reactive useWindowDimensions() hook from react-native. Now screenWidth updates automatically on device rotation or multitasking resize, and the animation effect re-runs with the correct off-screen position. See PR #3754. [resolved by agent]
| return { | ||
| stackingLevel: level, | ||
| isFirstDrawerInStack: level === 1 && Object.keys(drawerStack).length > 1, | ||
| }; |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: The z-index useEffect depends only on [isMounted] but reads stackingLevel. If stackingLevel changes while isMounted stays the same (e.g., another drawer is added/removed from the stack changing this drawer's index), the z-index won't update.
Suggestion: Add stackingLevel to the dependency array: }, [isMounted, stackingLevel]);
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: added stackingLevel to the dependency array of the z-index useEffect so it re-runs when the drawer stack changes (e.g. another drawer is added/removed changing this drawer's index). See PR #3754. [resolved by agent]
| * equivalent gradient with react-native-svg using the same status-driven feedback token. | ||
| */ | ||
| const DrawerHeaderGradient = ({ | ||
| color, |
There was a problem hiding this comment.
🔵 [MINOR] · code-quality-critique · confidence: 6/10
Problem: The HSL color parsing in DrawerHeaderGradient uses regex to extract the alpha channel and strip it. If theme.colors.feedback.background[color].subtle is not in hsla(...) format (e.g., hsl(...) without alpha, or hex), the opaqueColor transform will produce an invalid color string.
Suggestion: Guard the alpha-stripping logic: only apply the hsla→hsl + alpha removal when the regex match succeeds. If no alpha is found, use subtleColor as-is for opaqueColor.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: guarded the opaqueColor transform so the hsla-to-hsl + alpha removal only applies when the regex match succeeds (alphaMatch is truthy). If no alpha is found, subtleColor is used as-is for opaqueColor, preventing invalid color strings for non-hsla formats. See PR #3754. [resolved by agent]
| onDismiss={() => setShowDrawer(false)} | ||
| > | ||
| <DrawerHeader title="Announcements" /> | ||
| <DrawerBody> |
There was a problem hiding this comment.
🔵 [MINOR] · api-decision-critique · confidence: 7/10
Problem: The web Drawer uses React.forwardRef (typed as React.ForwardRefRenderFunction) and exports a forwardRef component, but the native Drawer does not. This means ref is a valid prop on web but silently ignored on native. BladeElementRef is defined as a Platform.Select type specifically to support cross-platform ref forwarding.
Suggestion: Wrap the native _Drawer in React.forwardRef and attach the ref to the AnimatedDrawerContainer's StyledDrawerWrapper, matching the web component's ref attachment point.
There was a problem hiding this comment.
✨ Agentic Resolution ✨: Auto Comment Resolution Triggered (View Logs)
There was a problem hiding this comment.
Fixed: wrapped the native _Drawer in React.forwardRef (typed as React.ForwardRefRenderFunction<BladeElementRef, DrawerProps>) and attached the ref to AnimatedDrawerContainer's StyledDrawerWrapper via a React.forwardRef on AnimatedDrawerContainer itself. This matches the web component's ref forwarding pattern. The assignWithoutSideEffects call now wraps React.forwardRef(_Drawer). See PR #3754. [resolved by agent]









Summary
.native.tsx) implementation for the Drawer component, replacing the previousthrowBladeErrorstubs with real platform-specific rendering.styled-components/native+react-native-reanimatedfor the slide-in panel and dimmed overlay.@gorhom/portalusing a conditional Portal mount (mirrors the workingPopover/Tooltipnative pattern), so the drawer mounts fresh on open and animates to the visible resting state.Screen.Recording.2026-07-09.at.12.53.02.PM.mov
Changes
AnimatedDrawerContainer.native.tsx(reanimated slide-in + overlay),__tests__/Drawer.native.test.tsx(+ snapshot)Drawer.native.tsx,DrawerSubcomponents.native.tsxStackProvider.tsx,BladeProvider.native.tsx.storybook/react-native/preview.tsx— registers aBladeBottomSheetPortalPortalHostin the story decorator (+flex: 1) so portal-based components (Drawer/BottomSheet/Popover/Tooltip/Modal) render inside RN Storybook. Without this,@storybook/react-native-ui's own nestedPortalProvidershadows Blade's host and portal content is silently dropped.docs/Drawer.stories.tsx— removed two invalid bare{' '}string children of aBoxthat crash on native ("Text strings must be rendered within a<Text>").@razorpay/blade.Verification
yarn types:typecheck:native)Notes / Warnings (P2, non-blocking)
Node of type rule not supported as an inline stylestyled-components warning appears while the drawer is mounted; does not affect rendering.🤖 Generated with Claude Code
Made with Cursor