Pressing Tab in a page's context menu throws "Maximum call stack size exceeded"
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 72/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- clojure, react
- Ambito
- accessibility, frontend
Direzione di ricerca
Inizia da deps/shui/src/logseq/shui/popup/core.cljs e leggi l’handler onOpenChange di x-popup, soprattutto come gestisce "focus-out". Riproduci la sequenza di Tab del menu della pagina descritta nell’issue. Il lavoro è terminato quando il focus può uscire dal menu, il menu si chiude e non si verifica alcun errore di maximum-call-stack.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Steps
On a DB graph, on the web app:
- Open a page. Here a page with a few blocks; a tag page gives the same result.
- Right-click the page title. The page menu opens ("Add to Favorites", "Delete page", ...).
- Press Tab.
Expected
Focus leaves the menu and the menu closes.
Actual
RangeError: Maximum call stack size exceeded. in Base UI's focus guard handlers, reported to the window as uncaught. The menu stays open.
The steps reproduced it 5 of 5 times (Playwright, trusted input, a new graph each time). Shift+Tab then Tab in the same menu reproduced it 5 of 5 times on an ordinary page and 5 of 5 on a tag page; Tab, Tab 3 of 3. The same keys in a block's context menu (right-click a bullet) did not reproduce it (0 of 5 for Shift+Tab, Tab and 0 of 5 for Tab, Tab).
Cause (read in code, checked on a page without Logseq)
x-popup in deps/shui/src/logseq/shui/popup/core.cljs renders a shui popup as a Base UI Menu (the lockfile has @base-ui/react 1.6.0) with a hidden trigger button (:tab-index -1) and a portal, and its onOpenChange handler cancels every close whose reason is "focus-out":
focus-transition? (= reason "focus-out")
;; ...
(if (or (not last-popup?)
target-toggle?
opening-outside-press?
menu-transition?
focus-transition?)
(some-> e (.cancel))
...)
The trigger and this cancel both came with 50abc4b93 ("fix: stabilize imperative popup toggles"). Before it, x-popup rendered no trigger and canceled a focus-out close only when a close target was inside the popup content (popup-focus-retained?).
While the menu is open, Base UI keeps tabbable focus guards next to the trigger (useTriggerFocusGuards), around the portal (FloatingPortal, "outside"), and inside the portal around the popup (FloatingFocusManager, "inside"). Tab from the menu starts a cycle of 4 guard handlers, each calling .focus() inside the previous one's focus handler:
- The guard after the trigger (
handleFocusTargetFocus) asks the menu to close with reason"focus-out".x-popupcancels the close, so the menu and its guards stay, and the handler focuses the next tabbable element after itself, the portal's outside guard. - The outside guard, entered from outside the portal, focuses the inside guard before the popup.
- That guard, entered from outside the portal, focuses the tabbable element after the focused one (itself), which is the inside guard after the popup.
- The inside guard after the popup, entered from inside the portal, focuses its
nextFocusableElement, the guard after the trigger. The cycle returns to 1.
A focus log of the steps shows this order repeating, and the error's stack recorded 120 frames deep shows the 4 handlers in turn (handleFocusTargetFocus, the FloatingPortal guard's onFocus, the 2 FloatingFocusManager guards' onFocus). The close in step 1 is the only way out of the cycle: a close that goes through unmounts the guards.
The same loop happens on a page with only React 19.2.6 and @base-ui/react, a Menu opened on mount with the trigger x-popup renders and an onOpenChange that cancels "focus-out" closes as x-popup does:
<Menu.Root
open={open}
onOpenChange={(next, details) => {
if (!next && details.reason === "focus-out") {
details.cancel();
return;
}
setOpen(next);
}}
>
<Menu.Trigger
render={
<button
tabIndex={-1}
aria-hidden
style={{ position: "fixed", top: -10000, left: -10000, width: 1, height: 1, opacity: 0, pointerEvents: "none" }}
/>
}
/>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item>Add to Favorites</Menu.Item>
<Menu.Item>Delete page</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
With @base-ui/react 1.6.0, 1 Tab overflowed the stack 3 of 3 times, and so did Tab, Tab and Shift+Tab, Tab (3 of 3 each). With the close let through (the details.cancel() branch removed), each sequence gave 0 of 3 and the menu closed.
Upstream: the same error from the same Base UI handler is mui/base-ui#5715 (a non-modal Popover whose trigger is the only tabbable element on the page), fixed by mui/base-ui#5733, merged on 2026-09-21 and in no release yet (the latest release, 1.8.0, is from 2026-09-04). That fix does not end this loop. With the preview build of #5733 from pkg.pr.new (its useTriggerFocusGuards matches the merged code), the page above overflowed the stack 3 of 3 times for each of the 3 key sequences with the cancel and 0 of 3 without it. A Base UI upgrade alone leaves the error in Logseq; the cancel of "focus-out" closes in x-popup has to change.
Found by a monkey test (gremlins.js with trusted Playwright input) of the web build of master 16c4ed1a0: 9 of 28 runs overflowed the stack in these guard handlers. The top frame is handleFocusTargetFocus in 1 run and a guard's onFocus in 8, both pairs of frames from the cycle above. In seed 109 it came from a trusted Tab at action 143. ddmin reduced that run to 6 actions: "/node" typed with no editor open, a synthetic click on a table cell, a right-click left of the page title (the page menu opened), Shift+Tab, Tab, and "/image" typed. Replayed with the recorded 20 ms between actions, the 6 actions reproduced it 0 of 5 times; with a screenshot saved after each action, 2 of 2. The steps above are read from those screenshots and leave out the first 2 actions and the last.
- Lingua principale
- Nessun dato sulla lingua
- Stelle
- 28
- Fork
- 2
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
Non abbiamo ancora controllato i file di configurazione di questo progetto. Parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di logseq/db-test
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
Tutte le issue di logseq/db-test
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
-
accessibility ready-for-agent
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
accessibility good first issue hacktoberfest help wanted ui
Difficoltà 1/5 1-3 ore Idoneità per principianti 92/100
I maintainer di solito rispondono entro 1 giorno
-
bug help wanted
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 92/100
I maintainer di solito rispondono entro 1 giorno