Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function DrawerPage() {
This is a vaul-based bottom sheet. Swipe down to dismiss.
</DrawerDescription>
</DrawerHeader>
<div className="px-4 pb-4">
<div className="pb-4">
<p className="text-body-s text-foreground-secondary">
The Drawer component wraps vaul and provides a consistent bottom-sheet experience.
It includes an overlay, drag handle, and max-height constraint (80vh).
Expand Down Expand Up @@ -83,7 +83,7 @@ export default function DrawerPage() {
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description</DrawerDescription>
</DrawerHeader>
<div className="px-4 pb-4">
<div className="pb-4">
{/* Content */}
</div>
<DrawerFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const OnrampConfirmationModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head carries M/12 beneath it; the slide keeps the L/16 of the outer stack */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
<IconBubble icon="alert" color="yellow" />
Expand Down
10 changes: 5 additions & 5 deletions src/components/Avatar/AvatarPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ export function AvatarPicker({ open, onOpenChange }: AvatarPickerProps) {

return (
<Drawer open={open} onOpenChange={onOpenChange}>
{/* 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 className="py-4" scrollAreaClassName="px-4">
{/* 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. */}
<DrawerContent className="py-4">
<DrawerHeader className="p-0 pb-4 text-left">
<DrawerTitle className="text-heading-s text-foreground-primary">{t('title')}</DrawerTitle>
<DrawerDescription>{t('description')}</DrawerDescription>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Badges/BadgeStatusDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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). */}
<DrawerContent className="pb-4">
<div className="px-4">
<div>
{/* 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
Expand Down
2 changes: 1 addition & 1 deletion src/components/Card/CardUnlockDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const CardUnlockDrawer: FC<Props> = ({ 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. */}
<div className="flex flex-col gap-4 px-4 pb-6">
<div className="flex flex-col gap-4 pb-6">
<div className="mx-auto w-full max-w-md">
<ScaledShareAsset
ref={captureRef}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Global/CancelSendLinkDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const CancelSendLinkDrawer = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* destructive-confirm anatomy: red icon bubble (design.md nested-drawer recipe) */}
<IconBubble icon="link-slash" color="red" className="mb-4" />

Expand Down
23 changes: 19 additions & 4 deletions src/components/Global/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,34 @@ const DrawerContent = React.forwardRef<React.ElementRef<typeof DrawerPrimitive.C
// chrome per the TX Details board (17490:115877): white background,
// no border, handle 32x5 sitting 8px from the top with 24px below.
// tx-details board 17835:84492: 16px top corners (was a hardcoded 10px)
// bg-white is deliberate (#2984, kush ruling 2026-09-07): the sheet
// is a clean white surface, not the page background
'fixed inset-x-0 bottom-0 z-50 mt-24 flex flex-col rounded-t-2xl bg-white',
className
)}
aria-describedby={undefined}
{...props}
onTouchMove={(e) => 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 && <DrawerTitle className="sr-only">{accessibleTitle}</DrawerTitle>}
<div className="mx-auto mt-2 mb-6 h-[5px] w-8 rounded-round bg-foreground-secondary" />
<div className="flex w-full justify-center">
{/* 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
Comment thread
kushagrasarathe marked this conversation as resolved.
* horizontal padding on the panel or on their content. */}
<div
ref={scrollAreaRef}
className={twMerge(
'max-h-[80vh] w-full overflow-auto pb-safe-bottom md:max-w-xl',
// scrollbar-none: android flashes a scrollbar on this
// container while the sheet itself is being dragged
'scrollbar-none max-h-[80vh] w-full overflow-auto px-4 pb-safe-bottom md:max-w-xl',
scrollAreaClassName
)}
>
Expand All @@ -138,15 +152,16 @@ DrawerContent.displayName = 'DrawerContent'
// own head instead — see CancelSendLinkDrawer and KycRegionRestrictedModal.
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={twMerge('grid gap-1 p-4 text-center sm:text-left', className)}
// py only: the scroll wrapper owns the horizontal L/16 padding
className={twMerge('grid gap-1 py-4 text-center sm:text-left', className)}
data-testid="drawer-header"
{...props}
/>
)
DrawerHeader.displayName = 'DrawerHeader'

const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={twMerge('mt-auto flex flex-col gap-2 p-4', className)} {...props} />
<div className={twMerge('mt-auto flex flex-col gap-2 py-4', className)} {...props} />
)
DrawerFooter.displayName = 'DrawerFooter'

Expand Down
10 changes: 5 additions & 5 deletions src/components/Global/QRBottomDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,18 @@ 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. */}
<DrawerContent
className={`mt-0 h-screen touch-none p-4 supports-[height:100dvh]:h-dvh ${className || ''}`}
className={`mt-0 h-screen touch-none py-4 supports-[height:100dvh]:h-dvh ${className || ''}`}
style={{ '--qr-drawer-expanded': `${QR_DRAWER_EXPANDED_PX}px` } as CSSProperties}
scrollAreaRef={scrollAreaRef}
scrollAreaClassName={`overscroll-contain max-h-[calc(var(--qr-drawer-expanded)-3.3125rem)] ${activeSnapPoint === snapPoints[0] || !scrollable ? 'touch-none' : ''}`}
>
<DrawerTitle className="space-y-2 mb-3">
<h2 className="text-heading-card">{title}</h2>
</DrawerTitle>
{/* 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 */}
<div className="pr-1 pb-1">
{/* the scroll wrapper's default px-4 covers the button
shadow's right edge now; pb-1 still covers the 4px
bottom offset */}
<div className="pb-1">
<QRCodeWrapper url={url} />
<div className="mx-auto mt-4 w-full p-2 text-center text-body-m">{text}</div>
<Divider text={tCommon('or')} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/Global/TokenSelector/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ const TokenSelector: React.FC<NewTokenSelectorProps> = ({ classNameButton, viewT
</Button>

<Drawer open={isDrawerOpen} onOpenChange={closeDrawer}>
<DrawerContent accessibleTitle={t('tokenSelector.drawerTitle')} className="p-4">
<DrawerContent accessibleTitle={t('tokenSelector.drawerTitle')} className="py-4">
<div ref={contentRef} className="mx-auto md:max-w-2xl">
{showNetworkList ? (
<NetworkListView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const UnlockMethodModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
Expand Down
4 changes: 2 additions & 2 deletions src/components/Kyc/InitiateKycModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export const InitiateKycModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center gap-4 px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center gap-4 pt-1 pb-6 text-center">
<IconBubble icon="alert" color="yellow" />
<DrawerHeader className="w-full gap-2 p-0 text-center sm:text-center">
<DrawerTitle>{t('degraded.title')}</DrawerTitle>
Expand Down Expand Up @@ -333,7 +333,7 @@ export const InitiateKycModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head owns the M/12 beneath it; everything after keeps
the drawer's L/16 rhythm */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/KycStatusDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, onKeepMounted }: KycStatusDra
return (
<>
<Drawer open={isOpen} onOpenChange={onClose}>
<DrawerContent accessibleTitle={t('statusDrawerTitle')} className="p-4 pb-12">
<DrawerContent accessibleTitle={t('statusDrawerTitle')} className="pt-4" scrollAreaClassName="pb-12">
{renderContent()}
{sumsubFlow.error && (
<p className="mt-3 text-center text-body-s text-foreground-error">{sumsubFlow.error}</p>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/modals/KycActionRequiredModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const KycActionRequiredModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/modals/KycFailedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const KycFailedModal = ({
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/modals/KycProcessingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const KycProcessingModal = ({ visible, onClose }: KycProcessingModalProps
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
<div className="mb-3 flex w-full flex-col items-center gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/modals/KycRegionRestrictedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const KycRegionRestrictedModal = ({ visible, onClose }: KycRegionRestrict
}}
>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
{/* DrawerHeader carries the M/12; the cta keeps the L/16 of the outer stack */}
<div className="flex w-full flex-col items-center gap-4">
<IconBubble icon="globe-lock" className="bg-action-primary" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/Profile/components/PublicProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ username, isLoggedIn = fa
}}
>
<DrawerContent>
<div className="flex flex-col items-center gap-4 px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center gap-4 pt-1 pb-6 text-center">
<IconBubble icon="user" className="bg-action-primary" />
<DrawerHeader className="w-full gap-2 p-0 text-center sm:text-center">
<DrawerTitle>{t('noInviteTitle')}</DrawerTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const TransactionDetailsDrawer: React.FC<TransactionDetailsDrawerProps> =
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
avatarUrl={avatarUrl}
className="px-4 pb-4"
className="pb-4"
/>
</DrawerContent>
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export function CancelDepositActions({
}}
>
<DrawerContent>
<div className="flex flex-col items-center gap-4 px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center gap-4 pt-1 pb-6 text-center">
<IconBubble icon="ban" color="red" />
<DrawerHeader className="w-full gap-2 p-0 text-center sm:text-center">
<DrawerTitle>
Expand Down
2 changes: 1 addition & 1 deletion src/dev/surfaces/options/OnrampOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function Shell({ children }: { children: React.ReactNode }) {
return (
<Drawer open>
<DrawerContent>
<div className="flex flex-col items-center px-4 pt-1 pb-6 text-center">
<div className="flex flex-col items-center pt-1 pb-6 text-center">
<div className="mb-3 flex w-full flex-col items-center gap-4">
<IconBubble icon="alert" color="yellow" />
<DrawerTitle>{t('title')}</DrawerTitle>
Expand Down
2 changes: 1 addition & 1 deletion src/features/home/components/HomeActionDrawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function HomeActionDrawers() {

return (
<Drawer open={drawer !== null} onOpenChange={(isOpen) => !isOpen && setDrawer(null)} hideBottomNav>
<DrawerContent className="px-4 pb-2">
<DrawerContent className="pb-2">
{content && (
<div className="flex flex-col gap-4">
{content === 'add' && <ScreenMark icon="plus" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function ContributorsDrawer({ contributors }: ContributorsDrawerProps) {
{t('contributors.title', { count: contributors.length })}
</DrawerTitle>
</DrawerHeader>
<div className="space-y-0 max-h-[60vh] overflow-auto px-4">
<div className="space-y-0 max-h-[60vh] overflow-auto">
{contributorCards.map((contributor, index) => (
<ContributorCard
key={contributor.uuid}
Expand Down
26 changes: 26 additions & 0 deletions src/hooks/__tests__/usePullToRefresh.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
5 changes: 5 additions & 0 deletions src/hooks/usePullToRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading