Pressing Tab in a page's context menu throws "Maximum call stack size exceeded"
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 72/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- clojure, react
- Área
- accessibility, frontend
Línea de trabajo
Empieza por deps/shui/src/logseq/shui/popup/core.cljs y lee el controlador onOpenChange de x-popup, especialmente cómo gestiona "focus-out". Reproduce la secuencia de Tab del menú de página del issue. El trabajo está terminado cuando el foco puede salir del menú, el menú se cierra y no se produce ningún error de maximum-call-stack.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- Sin datos de lenguaje
- Estrellas
- 28
- Forks
- 2
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de logseq/db-test
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Todos los issues de logseq/db-test
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
appandflow/stim#2081 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
SAP/fundamental-ngx#14574 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
mantinedev/mantine#9233 ·
Los mantenedores suelen responder en 8 días
-
.Frontend .Team/UXWest Difficulty:Easy Misc/Accessibility Priority:P3 Type:Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
metabase/metabase#83338 · 1 comentario ·
Los mantenedores suelen responder en 1 día