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..f74bac7bc3 100644 --- a/src/components/Global/Drawer/index.tsx +++ b/src/components/Global/Drawer/index.tsx @@ -106,20 +106,34 @@ 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}}
+ {/* 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 +152,8 @@ DrawerContent.displayName = 'DrawerContent' // own head instead — see CancelSendLinkDrawer and KycRegionRestrictedModal. const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
@@ -146,7 +161,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) => ( { 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