From 49b5a902d658385a7285256ea433cd57622b3972 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:36:45 +0530 Subject: [PATCH 1/3] fix(drawer): scroll area owns the L/16 padding so button shadows never clip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: DrawerContent's overflow-auto scroll wrapper clips painting at its own box edge. Consumers put the horizontal padding on the panel AROUND that wrapper (or on the panel via className), so a w-full button's 4px offset shadow fell outside the clip box and was cut in a straight vertical line — the avatar drawer, KYC status drawer, token selector and QR drawer all hit or worked around it locally. Fix: the scroll wrapper now carries the design.md container padding (px-4, L/16) INSIDE the overflow box, so shadows always land in the padded, unclipped region. DrawerHeader/DrawerFooter drop their own horizontal padding (py-4) to match. Every consumer's now-redundant horizontal padding is removed — content width is unchanged everywhere (16px moved inside the clip box, not added). Local workarounds retired: AvatarPicker's scrollAreaClassName px-4, QRBottomDrawer's pr-1 gutter. KycStatusDrawer's pb-12 moved into the scroll area so the retry button's bottom shadow paints too. --- .../(mobile-ui)/dev/ds/patterns/drawer/page.tsx | 4 ++-- .../components/OnrampConfirmationModal.tsx | 2 +- src/components/Avatar/AvatarPicker.tsx | 10 +++++----- src/components/Badges/BadgeStatusDrawer.tsx | 2 +- src/components/Card/CardUnlockDrawer.tsx | 2 +- .../Global/CancelSendLinkDrawer/index.tsx | 2 +- src/components/Global/Drawer/index.tsx | 14 +++++++++++--- src/components/Global/QRBottomDrawer/index.tsx | 10 +++++----- .../Global/TokenSelector/TokenSelector.tsx | 2 +- .../IdentityVerification/UnlockMethodModal.tsx | 2 +- src/components/Kyc/InitiateKycModal.tsx | 4 ++-- src/components/Kyc/KycStatusDrawer.tsx | 2 +- .../Kyc/modals/KycActionRequiredModal.tsx | 2 +- src/components/Kyc/modals/KycFailedModal.tsx | 2 +- src/components/Kyc/modals/KycProcessingModal.tsx | 2 +- .../Kyc/modals/KycRegionRestrictedModal.tsx | 2 +- .../Profile/components/PublicProfile.tsx | 2 +- .../TransactionDetailsDrawer.tsx | 2 +- .../provider-actions/CancelDepositActions.tsx | 2 +- src/dev/surfaces/options/OnrampOptions.tsx | 2 +- src/features/home/components/HomeActionDrawers.tsx | 2 +- .../components/ContributorsDrawer.tsx | 2 +- 22 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx b/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx index 36ab305db9..1f4d2ab550 100644 --- a/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx +++ b/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx @@ -41,7 +41,7 @@ export default function DrawerPage() { This is a vaul-based bottom sheet. Swipe down to dismiss. -
+

The Drawer component wraps vaul and provides a consistent bottom-sheet experience. It includes an overlay, drag handle, and max-height constraint (80vh). @@ -83,7 +83,7 @@ export default function DrawerPage() { Title Description -

+
{/* Content */}
diff --git a/src/components/AddMoney/components/OnrampConfirmationModal.tsx b/src/components/AddMoney/components/OnrampConfirmationModal.tsx index e4364e1286..725ee355bb 100644 --- a/src/components/AddMoney/components/OnrampConfirmationModal.tsx +++ b/src/components/AddMoney/components/OnrampConfirmationModal.tsx @@ -32,7 +32,7 @@ export const OnrampConfirmationModal = ({ }} > -
+
{/* the head carries M/12 beneath it; the slide keeps the L/16 of the outer stack */}
diff --git a/src/components/Avatar/AvatarPicker.tsx b/src/components/Avatar/AvatarPicker.tsx index cfb12d2a7c..4c44602aa4 100644 --- a/src/components/Avatar/AvatarPicker.tsx +++ b/src/components/Avatar/AvatarPicker.tsx @@ -165,11 +165,11 @@ export function AvatarPicker({ open, onOpenChange }: AvatarPickerProps) { return ( - {/* The horizontal padding belongs to the SCROLL AREA, not to the panel - around it: the panel's padding sits outside the overflow-auto box, - so a w-full button's 4px offset shadow fell past the scroll edge - and got clipped. The matching pb-2 below covers the bottom. */} - + {/* DrawerContent's scroll area owns the horizontal padding now + (shadow-clip fix) — only the vertical padding is ours. The + pb-2 on the content below keeps the last button's bottom + shadow inside the scroll box. */} + {t('title')} {t('description')} diff --git a/src/components/Badges/BadgeStatusDrawer.tsx b/src/components/Badges/BadgeStatusDrawer.tsx index 30fb4a9f91..8a7f8da475 100644 --- a/src/components/Badges/BadgeStatusDrawer.tsx +++ b/src/components/Badges/BadgeStatusDrawer.tsx @@ -61,7 +61,7 @@ export const BadgeStatusDrawer = ({ isOpen, onClose, badge }: BadgeStatusDrawerP py-4 + p-4 here stacked 32px of extra head room (PR #2813 review, Jota). */} -
+
{/* centered head per the TX Details chrome (board 17490:115877): badge art → one-line title. Tapping it opens the detail modal — close the unlock drawer (z-50) first so the modal diff --git a/src/components/Card/CardUnlockDrawer.tsx b/src/components/Card/CardUnlockDrawer.tsx index b36d92efc5..ff3c503d02 100644 --- a/src/components/Card/CardUnlockDrawer.tsx +++ b/src/components/Card/CardUnlockDrawer.tsx @@ -72,7 +72,7 @@ export const CardUnlockDrawer: FC = ({ isOpen, onClose, entry, username, doesn't blow up to 432px tall at the drawer's full xl width and force a scrollbar. Centred so the buttons below it span the same content column. */} -
+
-
+
{/* destructive-confirm anatomy: red icon bubble (design.md nested-drawer recipe) */} diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx index ef558b4867..4c9eb26714 100644 --- a/src/components/Global/Drawer/index.tsx +++ b/src/components/Global/Drawer/index.tsx @@ -116,10 +116,17 @@ const DrawerContent = React.forwardRef{accessibleTitle}}
+ {/* The scroll wrapper owns the horizontal L/16 container + * padding (design.md spacing table). It must live HERE, + * inside the overflow box: overflow-auto clips painting at + * its own edge, so padding on the panel around it leaves a + * w-full button's 4px offset shadow outside the clip box — + * cut off in a straight line. Consumers must not re-add + * horizontal padding on the panel or on their content. */}
@@ -138,7 +145,8 @@ DrawerContent.displayName = 'DrawerContent' // own head instead — see CancelSendLinkDrawer and KycRegionRestrictedModal. const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
@@ -146,7 +154,7 @@ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
+
) DrawerFooter.displayName = 'DrawerFooter' diff --git a/src/components/Global/QRBottomDrawer/index.tsx b/src/components/Global/QRBottomDrawer/index.tsx index 1d6ce0c8d3..079811c1e8 100644 --- a/src/components/Global/QRBottomDrawer/index.tsx +++ b/src/components/Global/QRBottomDrawer/index.tsx @@ -105,7 +105,7 @@ const QRBottomDrawer = ({ url, title, text, buttonText, className }: QRBottomDra reaches the cap through a CSS variable because Tailwind only emits an arbitrary value it can read literally in the source. */}

{title}

- {/* the button's shadow is offset 4px right AND 4px down, so the - drawer's overflow-auto scroll wrapper clips it on both edges - without a gutter on each */} -
+ {/* the scroll wrapper's default px-4 covers the button + shadow's right edge now; pb-1 still covers the 4px + bottom offset */} +
{text}
diff --git a/src/components/Global/TokenSelector/TokenSelector.tsx b/src/components/Global/TokenSelector/TokenSelector.tsx index f30759af1f..cbcf0b5573 100644 --- a/src/components/Global/TokenSelector/TokenSelector.tsx +++ b/src/components/Global/TokenSelector/TokenSelector.tsx @@ -477,7 +477,7 @@ const TokenSelector: React.FC = ({ classNameButton, viewT - +
{showNetworkList ? ( -
+
{/* the head owns the M/12 beneath it; everything after it keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/InitiateKycModal.tsx b/src/components/Kyc/InitiateKycModal.tsx index 2c70015332..fb85128d45 100644 --- a/src/components/Kyc/InitiateKycModal.tsx +++ b/src/components/Kyc/InitiateKycModal.tsx @@ -218,7 +218,7 @@ export const InitiateKycModal = ({ }} > -
+
{t('degraded.title')} @@ -333,7 +333,7 @@ export const InitiateKycModal = ({ }} > -
+
{/* the head owns the M/12 beneath it; everything after keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 2e815c12ce..2e3cffa8d7 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -95,7 +95,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, onKeepMounted }: KycStatusDra return ( <> - + {renderContent()} {sumsubFlow.error && (

{sumsubFlow.error}

diff --git a/src/components/Kyc/modals/KycActionRequiredModal.tsx b/src/components/Kyc/modals/KycActionRequiredModal.tsx index ecb11f522c..476a9902a4 100644 --- a/src/components/Kyc/modals/KycActionRequiredModal.tsx +++ b/src/components/Kyc/modals/KycActionRequiredModal.tsx @@ -31,7 +31,7 @@ export const KycActionRequiredModal = ({ }} > -
+
{/* the head owns the M/12 beneath it; everything after it keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycFailedModal.tsx b/src/components/Kyc/modals/KycFailedModal.tsx index 020785ff6b..5b5c3f68e6 100644 --- a/src/components/Kyc/modals/KycFailedModal.tsx +++ b/src/components/Kyc/modals/KycFailedModal.tsx @@ -44,7 +44,7 @@ export const KycFailedModal = ({ }} > -
+
{/* the head owns the M/12 beneath it; everything after it keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycProcessingModal.tsx b/src/components/Kyc/modals/KycProcessingModal.tsx index 833433cc13..ef5bcd4786 100644 --- a/src/components/Kyc/modals/KycProcessingModal.tsx +++ b/src/components/Kyc/modals/KycProcessingModal.tsx @@ -21,7 +21,7 @@ export const KycProcessingModal = ({ visible, onClose }: KycProcessingModalProps }} > -
+
{/* the head owns the M/12 beneath it; everything after it keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycRegionRestrictedModal.tsx b/src/components/Kyc/modals/KycRegionRestrictedModal.tsx index 337dcc0cd0..7be9e88075 100644 --- a/src/components/Kyc/modals/KycRegionRestrictedModal.tsx +++ b/src/components/Kyc/modals/KycRegionRestrictedModal.tsx @@ -29,7 +29,7 @@ export const KycRegionRestrictedModal = ({ visible, onClose }: KycRegionRestrict }} > -
+
{/* DrawerHeader carries the M/12; the cta keeps the L/16 of the outer stack */}
diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx index 4456b8c19a..72784abdd9 100644 --- a/src/components/Profile/components/PublicProfile.tsx +++ b/src/components/Profile/components/PublicProfile.tsx @@ -294,7 +294,7 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa }} > -
+
{t('noInviteTitle')} diff --git a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx index df14ac9337..c8371a196f 100644 --- a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx +++ b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx @@ -67,7 +67,7 @@ export const TransactionDetailsDrawer: React.FC = isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} avatarUrl={avatarUrl} - className="px-4 pb-4" + className="pb-4" /> diff --git a/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx b/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx index 905671eae4..012210b38e 100644 --- a/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx +++ b/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx @@ -137,7 +137,7 @@ export function CancelDepositActions({ }} > -
+
diff --git a/src/dev/surfaces/options/OnrampOptions.tsx b/src/dev/surfaces/options/OnrampOptions.tsx index 7233219266..305712aa0e 100644 --- a/src/dev/surfaces/options/OnrampOptions.tsx +++ b/src/dev/surfaces/options/OnrampOptions.tsx @@ -40,7 +40,7 @@ function Shell({ children }: { children: React.ReactNode }) { return ( -
+
{t('title')} diff --git a/src/features/home/components/HomeActionDrawers.tsx b/src/features/home/components/HomeActionDrawers.tsx index e660c3b25d..213500cb97 100644 --- a/src/features/home/components/HomeActionDrawers.tsx +++ b/src/features/home/components/HomeActionDrawers.tsx @@ -93,7 +93,7 @@ export function HomeActionDrawers() { return ( !isOpen && setDrawer(null)} hideBottomNav> - + {content && (
{content === 'add' && } diff --git a/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx index 8aaf5a3736..5525a22e2d 100644 --- a/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx +++ b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx @@ -59,7 +59,7 @@ export function ContributorsDrawer({ contributors }: ContributorsDrawerProps) { {t('contributors.title', { count: contributors.length })} -
+
{contributorCards.map((contributor, index) => ( Date: Mon, 7 Sep 2026 17:01:10 +0530 Subject: [PATCH 2/3] fix(drawer): page-background sheet + honest touch handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Kush findings, one commit because they share the drawer chrome: 1. Sheet background back to bg-background-page. e55b5d130 (#2984) deliberately went white; Kush's 2026-09-07 ruling reverts it — the sheet matches the app page background. Flagged in the PR body since it reverses a 3-day-old deliberate change. 2. Removed the Content-level onTouchMove stopPropagation. It silenced vaul's own document-level touchmove handlers (scroll containment + drag coordination), which is why dragging the sheet from its body felt broken and touches seemed to reach the page behind. The reason it existed — the document-level pull-to-refresh reacting to drawer touches — is now fixed at the root: usePullToRefresh ignores any touch starting on [data-vaul-drawer]/[data-vaul-overlay] (with a regression test). Whole-sheet drag-to-dismiss and no-P2R-during-drag verified with synthesized CDP touch swipes. 3. scrollbar-none on the drawer scroll container — Android flashes a scrollbar there while the sheet is dragged (tailwind-scrollbar utility, same overflow chain as the shadow fix). --- src/components/Global/Drawer/index.tsx | 17 ++++++++---- src/hooks/__tests__/usePullToRefresh.test.tsx | 26 +++++++++++++++++++ src/hooks/usePullToRefresh.ts | 5 ++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx index 4c9eb26714..00d458a305 100644 --- a/src/components/Global/Drawer/index.tsx +++ b/src/components/Global/Drawer/index.tsx @@ -103,15 +103,20 @@ const DrawerContent = React.forwardRef e.stopPropagation()} + // no onTouchMove stopPropagation here: it silenced vaul's own + // document-level touchmove handlers (scroll containment + drag + // coordination), which broke dragging the sheet from its body. + // pull-to-refresh ignores drawer touches itself (usePullToRefresh). > {accessibleTitle && {accessibleTitle}}
@@ -126,7 +131,9 @@ const DrawerContent = React.forwardRef diff --git a/src/hooks/__tests__/usePullToRefresh.test.tsx b/src/hooks/__tests__/usePullToRefresh.test.tsx index 618761f482..0fe393352b 100644 --- a/src/hooks/__tests__/usePullToRefresh.test.tsx +++ b/src/hooks/__tests__/usePullToRefresh.test.tsx @@ -160,4 +160,30 @@ describe('usePullToRefresh', () => { expect(invalidateQueries).not.toHaveBeenCalled() expect(impactHaptic).not.toHaveBeenCalled() }) + + it('ignores touches that start on an open vaul drawer', () => { + const invalidateQueries = jest.spyOn(queryClient, 'invalidateQueries') + renderHook(() => usePullToRefresh(), { wrapper }) + + const sheet = document.createElement('div') + sheet.setAttribute('data-vaul-drawer', '') + const inner = document.createElement('button') + sheet.appendChild(inner) + document.body.appendChild(sheet) + + const start = new Event('touchstart', { bubbles: true }) + Object.defineProperty(start, 'touches', { value: [{ clientX: 0, clientY: 0 }] }) + act(() => { + inner.dispatchEvent(start) + }) + + // a full downward drag on the sheet must not move the indicator + touch('touchmove', 200) + touch('touchend', 200) + + expect(indicator()?.style.opacity).toBe('0') + expect(invalidateQueries).not.toHaveBeenCalled() + expect(impactHaptic).not.toHaveBeenCalled() + sheet.remove() + }) }) diff --git a/src/hooks/usePullToRefresh.ts b/src/hooks/usePullToRefresh.ts index 0bf0d2d7df..45d70c94bc 100644 --- a/src/hooks/usePullToRefresh.ts +++ b/src/hooks/usePullToRefresh.ts @@ -168,6 +168,11 @@ export const usePullToRefresh = (options: UsePullToRefreshOptions = {}) => { const onTouchStart = (e: TouchEvent) => { if (refreshing || e.touches.length !== 1) return + // a touch on an open vaul sheet (or its overlay) is a drawer + // gesture, never a pull — without this, dragging the sheet moved + // the page behind it (the listeners here are on document) + const target = e.target as Element | null + if (target?.closest?.('[data-vaul-drawer],[data-vaul-overlay]')) return const allowed = shouldPullToRefreshRef.current ? shouldPullToRefreshRef.current() : window.scrollY === 0 if (!allowed) return // a new pull can start inside the retract window — put the arrow back From 4e809631320bc8894e168d436da469f251c4e29f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:02:04 +0530 Subject: [PATCH 3/3] =?UTF-8?q?revert(drawer):=20keep=20the=20white=20shee?= =?UTF-8?q?t=20=E2=80=94=20bg=20ruling=20flipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kush's 2026-09-07 ruling reversed: the sheet stays white (Slava's #2984 clean-surface choice), not bg-background-page. Only the background part of 5422facba reverts — the touch/drag fixes, pull-to-refresh guard, and scrollbar-none all stay. --- src/components/Global/Drawer/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx index 00d458a305..f74bac7bc3 100644 --- a/src/components/Global/Drawer/index.tsx +++ b/src/components/Global/Drawer/index.tsx @@ -103,12 +103,12 @@ const DrawerContent = React.forwardRef