From 98aff7c577f8e64b5124aac9824147d1c76f640b Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:21:29 +0200 Subject: [PATCH] fix(transactions): keep booking dialog open while typing in the agent sheet (#1966) * fix(transactions): keep booking dialog open while typing in the agent sheet TransactionBookingDialog was a default modal Radix dialog; the agent sheet is a fixed z-[60] panel portaled outside its DOM, so a click in the assistant's text field counted as an outside interaction and dismissed the dialog, losing the half-filled booking. Apply the established non-modal convention (NewInvoiceDialog / NewJournalEntryDialog): modal={false} + DialogVeil, with escape and outside-dismiss prevented. Closing is explicit via the header X. Co-Authored-By: Claude Fable 5 * fix(transactions): restore page modality via inert while booking dialog is open Skeptic review caught that the non-modal convention was ported incompletely: NewInvoiceDialog pairs modal={false} with an inert effect on #dash-shell. Without it the background page stayed keyboard-, AT-, and (mobile nav) tap-reachable behind the veil, so a stray Shift+Tab plus Enter could navigate away and destroy a half-filled booking. Also corrects the comment that cited NewJournalEntryDialog as non-modal precedent (it is plain modal). Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- .../transactions/TransactionBookingDialog.tsx | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/components/transactions/TransactionBookingDialog.tsx b/components/transactions/TransactionBookingDialog.tsx index 185430c3..137d3967 100644 --- a/components/transactions/TransactionBookingDialog.tsx +++ b/components/transactions/TransactionBookingDialog.tsx @@ -1,8 +1,8 @@ 'use client' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogVeil } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { formatCurrency, formatDate } from '@/lib/utils' @@ -158,6 +158,21 @@ export default function TransactionBookingDialog({ return { bankAccount: account, bankAccountName: matched?.name ?? null } }, [transaction, cashAccounts, cashAccountsLoading]) + // The dialog is non-modal (see the Dialog below), so Radix does not trap + // focus or inert the page. Restore page modality by hand: `inert` on the + // dash shell blocks pointer, keyboard, and AT access to the page behind + // (including the mobile bottom nav, which sits above the veil), while the + // agent sheet (outside the shell) stays live. Same as NewInvoiceDialog. + useEffect(() => { + if (!open) return + const shell = document.getElementById('dash-shell') + if (!shell) return + shell.inert = true + return () => { + shell.inert = false + } + }, [open]) + if (!transaction) return null const isIncome = transaction.amount > 0 @@ -253,8 +268,21 @@ export default function TransactionBookingDialog({ setInboxPickerOpen(false) } onOpenChange(o) - }}> - + }} modal={false}> + + e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > {t('title')}