#607: make inline edit toolbar usable on mobile (wrap + viewport clamp)

Problem: On phone-width viewports (~375-430px), the floating
FormattingToolbar in EditableCell rendered as a single row with
17+ controls (B/I/U + 7 color swatches + 3 align buttons + font +
size + Save/Cancel). The row was wider than the screen, so users
only saw the edges (X, ✓, two "default" dropdowns) and could not
reach Bold/Italic/Underline, color swatches, alignment, or font
controls. The horizontal clamp also relied on the table's
scroll-container bounds, which on mobile extend beyond the viewport
because the table is wider than the screen — so the clamp could
park the toolbar partially off-screen.

Fix in artifacts/tx-os/src/components/editable-cell.tsx:
- Toolbar container: `flex` → `flex flex-wrap` with `gap-x-1 gap-y-1`
  and `max-w-[calc(100vw-16px)]` so it wraps onto multiple rows
  whenever a single row would exceed the viewport. Bumped `py` to
  `py-1` for breathing room between wrapped rows.
- Horizontal clamp: in addition to the scroll-container bounds,
  also clamp against the viewport (`[8, window.innerWidth - tbWidth
  - 8]`) so the toolbar always lands fully on-screen even when the
  scroll container is wider than the screen.

Scope: visual / positioning only. No changes to TipTap config,
toolbar buttons, or save/cancel logic. Above-vs-below placement
(#581 iOS keyboard handling) is preserved.
This commit is contained in:
Riyadh
2026-05-19 08:12:56 +00:00
parent 9d7184ec3d
commit 7ab1d1bfe7
@@ -606,12 +606,22 @@ function FormattingToolbar({
// Clamp horizontally to the table's scroll container if we can
// find one; fall back to the viewport otherwise. This keeps the
// toolbar visually anchored inside the table.
// #607: also clamp against the viewport in case the table's
// scroll container is wider than the screen (mobile/iPad), so
// the toolbar never lands off-screen and stays usable when it
// wraps to multiple rows.
const container = findScrollContainer(anchor);
const containerRect = container?.getBoundingClientRect();
const minLeft = containerRect ? containerRect.left + 4 : 8;
const maxLeft = containerRect
? containerRect.right - tbWidth - 4
: window.innerWidth - tbWidth - 8;
const vpMinLeft = 8;
const vpMaxLeft = window.innerWidth - tbWidth - 8;
const minLeft = Math.max(
vpMinLeft,
containerRect ? containerRect.left + 4 : vpMinLeft,
);
const maxLeft = Math.min(
vpMaxLeft,
containerRect ? containerRect.right - tbWidth - 4 : vpMaxLeft,
);
let left = rect.left;
if (left > maxLeft) left = maxLeft;
if (left < minLeft) left = minLeft;
@@ -645,7 +655,7 @@ function FormattingToolbar({
zIndex: 1000,
visibility: pos ? "visible" : "hidden",
}}
className="flex items-center gap-1 bg-white border border-gray-200 rounded shadow-md px-1 py-0.5"
className="flex flex-wrap items-center gap-x-1 gap-y-1 bg-white border border-gray-200 rounded shadow-md px-1 py-1 max-w-[calc(100vw-16px)]"
onMouseDown={(e) => e.preventDefault()}
data-testid="em-edit-toolbar"
>