From 2f2d18327a0d2dd1fab41bc9fb945437232f6936 Mon Sep 17 00:00:00 2001 From: evanqua Date: Thu, 9 Jul 2026 10:58:20 -0700 Subject: [PATCH] fix: guard team-status dropdown against spurious upstream self-close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14 Root cause: HeroUI/react-aria's Popover (backing the team-status Dropdown) intermittently fired onOpenChange(false) 15-35ms after opening, with no onAction and no user interaction — closing the menu before Playwright (or a fast real user) could click a status option. This is a known, still-unresolved upstream focus/blur-tracking race in react-aria's overlay handling (adobe/react-spectrum#4533), not something in app code. Ruled out via instrumented local repro before landing on this: the existing auto-close effect (never fired), the custom framer-motion motionProps (removed entirely, bug persisted at the same rate), and shouldCloseOnBlur={false} (no effect, bug persisted). Fix: track when the dropdown was opened and whether the close came from an actual selection (onAction). Ignore onOpenChange(false) that fires within 150ms of opening unless it was a real selection — real users never dismiss a just-opened menu that fast, so this only ever suppresses the spurious closes, not genuine ones. Verified: 41/41 passing across 5 repeats on Firebase (previously ~50-60% failure rate on the affected scenarios), 10/10 on PocketBase. --- src/components/dispatch/calltracking.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/dispatch/calltracking.tsx b/src/components/dispatch/calltracking.tsx index de1ac80..3d7eacb 100644 --- a/src/components/dispatch/calltracking.tsx +++ b/src/components/dispatch/calltracking.tsx @@ -111,6 +111,14 @@ export const CallTrackingTable: React.FC = ({ const [closingCallId, setClosingCallId] = React.useState(null); const [openMenuToken, setOpenMenuToken] = React.useState(null); const previousOpenCallIdRef = React.useRef(null); + // HeroUI/react-aria's Popover can spuriously fire onOpenChange(false) a few + // milliseconds after opening, with no user interaction (a known upstream + // focus/blur-tracking race — see adobe/react-spectrum#4533). Real users + // never dismiss a menu that fast, so ignore closes inside this window + // unless they came from an actual selection (onAction). + const TEAM_STATUS_MENU_CLOSE_GUARD_MS = 150; + const teamStatusMenuOpenedAtRef = React.useRef(0); + const teamStatusMenuSelectedRef = React.useRef(false); const { notesTexts, setNotesTexts, @@ -437,9 +445,15 @@ export const CallTrackingTable: React.FC = ({ onOpenChange={(isOpen) => { if (isOpen) { if (isResolvedCall && !showResolvedCalls) return; + teamStatusMenuOpenedAtRef.current = Date.now(); setOpenMenuToken(`team-status:${call.id}:${team}`); return; } + const selected = teamStatusMenuSelectedRef.current; + teamStatusMenuSelectedRef.current = false; + if (!selected && Date.now() - teamStatusMenuOpenedAtRef.current < TEAM_STATUS_MENU_CLOSE_GUARD_MS) { + return; + } setOpenMenuToken((current) => current === `team-status:${call.id}:${team}` ? null : current ); @@ -456,7 +470,10 @@ export const CallTrackingTable: React.FC = ({ handleTeamStatusChange(call.id, team, key as string)} + onAction={(key) => { + teamStatusMenuSelectedRef.current = true; + handleTeamStatusChange(call.id, team, key as string); + }} > {statusOptions.map((status: string) => ( {status}