From 8d866e4f1a592d841eeb78b43160fa67e2eb3938 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 14 Aug 2026 14:03:44 +0200 Subject: [PATCH] fix: a table popover opens where it fits, in both chromes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three dialogs placed themselves, so the ribbon's viewport coordinates met CSS that hangs the box above its anchor: the formula popover landed under the band, clipped. Each caller now owns the placement — `anchored` in the ribbon, like every other menu there, and a wrapper in the editor that clamps a tall one into the scroll area instead of letting the overflow cut it. --- docs/architecture/ribbon.md | 4 +- src/lib/components/Editor.svelte | 87 ++++++++++-------- src/lib/components/TableFormulaDialog.svelte | 61 +++++-------- src/lib/components/TableSortDialog.svelte | 88 +++++++------------ src/lib/components/TableSplitDialog.svelte | 87 +++++++----------- .../components/ribbon/tabs/TableTabs.svelte | 88 +++++++++---------- 6 files changed, 184 insertions(+), 231 deletions(-) diff --git a/docs/architecture/ribbon.md b/docs/architecture/ribbon.md index a6ff357..238a0ef 100644 --- a/docs/architecture/ribbon.md +++ b/docs/architecture/ribbon.md @@ -70,7 +70,9 @@ at every height. A flex row cannot reserve width in part of its height, so it mo - **A menu must leave the flow.** `.ribbon-body` clips its own overflow, so the `anchored` action (`ribbon/menu.svelte.ts`) switches a panel to `position: fixed` and pins it under its wrapper, clamped inside the window. Panels drop below the **whole band**, not below their own button — - inside it they would cover the controls and group labels underneath. + inside it they would cover the controls and group labels underneath. A panel has to be out of + the flow (`position: absolute`) *before* it mounts: in it, it stretches the wrapper `anchored` + measures and lands its own height too low. - **A reused picker anchors its panel itself**, and no prop says when it opened. `pinPanels`, one action on the band, watches for a mounted absolutely-positioned node and pins that; the ColorPicker places its own and only needs the band-aware top. diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index e984496..7069807 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -497,6 +497,16 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; $effect(() => { if (!tableUi.visible && tableDialog) tableDialog = null; }); + // A dialog hangs above the table like the toolbar, so a table near the top of the + // scroller puts it outside the scroll area, where the overflow clips it. The + // container's padding is what the toolbar island overlays, so clear that too. + let tableDialogH = $state(0); + let tableDialogTop = $derived.by(() => { + const c = editorContainer; + if (!c || !tableDialogH) return tableUi.top; + const pad = parseFloat(getComputedStyle(c).paddingTop) || 0; + return Math.max(tableUi.top, c.scrollTop + pad + tableDialogH + 6); + }); // The DOM element of the table containing the current selection, or null. // nodeDOM(before(table)) returns the wrapper div the table node view renders. @@ -1109,42 +1119,39 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; onDialog={(which) => (tableDialog = which)} /> {/if} - {#if tableUi.visible} - { - editor?.chain().focus().splitCellInto(cols, rows).run(); - tableDialog = null; - }} - onClose={() => (tableDialog = null)} - /> - { - editor?.chain().focus().sortTable(options).run(); - tableDialog = null; - }} - onClose={() => (tableDialog = null)} - /> - { - editor?.chain().focus().setCellFormula(formula).run(); - tableDialog = null; - }} - onClose={() => (tableDialog = null)} - /> + {#if tableUi.visible && tableDialog} +
+ {#if tableDialog === 'split'} + { + editor?.chain().focus().splitCellInto(cols, rows).run(); + tableDialog = null; + }} + onClose={() => (tableDialog = null)} + /> + {:else if tableDialog === 'sort'} + { + editor?.chain().focus().sortTable(options).run(); + tableDialog = null; + }} + onClose={() => (tableDialog = null)} + /> + {:else} + { + editor?.chain().focus().setCellFormula(formula).run(); + tableDialog = null; + }} + onClose={() => (tableDialog = null)} + /> + {/if} +
{/if} {#if imageUi.visible} @@ -1184,6 +1191,14 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; transition: opacity 0.15s; } + /* The table popovers sit above the table's top-left corner like the toolbar. The + wrapper is what places them, so one measurement keeps a tall one in view. */ + .table-dialog { + position: absolute; + transform: translateY(calc(-100% - 6px)); + z-index: 300; + } + /* Hover hint for links (Word/LibreOffice style): URL + modifier-click tip, sitting just above the link. A child of .editor so it scrolls with content but isn't scaled by the zoom transform; pointer-events:none keeps the link itself clickable. */ diff --git a/src/lib/components/TableFormulaDialog.svelte b/src/lib/components/TableFormulaDialog.svelte index 384c21e..7d4d272 100644 --- a/src/lib/components/TableFormulaDialog.svelte +++ b/src/lib/components/TableFormulaDialog.svelte @@ -1,20 +1,15 @@ -{#if open} -