@cplieger/web-terminal-ui 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,17 +38,15 @@
38
38
 
39
39
  import type { TerminalFeature } from "../kernel/types.js";
40
40
  import type { ClipboardApi } from "./clipboard.js";
41
+ import { placeMenuAt } from "./menu-position.js";
41
42
 
42
43
  // Slightly longer than the browser's ~500ms native long-press so a native word
43
44
  // selection (which we defer to) has registered — and fired selectionchange —
44
45
  // before this fires.
45
46
  const LONG_PRESS_MS = 550;
46
47
  const LONG_PRESS_MOVE_PX = 10;
47
- const EDGE_MARGIN = 8;
48
- // When the menu can't fit below the anchor (a long-press near the bottom edge or
49
- // above the keyboard), it opens above the point. This gap lifts it clear of the
50
- // fingertip so the touch does not cover the menu items.
51
- const MENU_GAP = 16;
48
+ // Viewport clamping + the flip-above-the-fingertip gap live in the shared
49
+ // point-anchored positioner (menu-position.ts), shared with the tab menu.
52
50
  // After a touch long-press opens the menu, swallow the trailing click/tap that
53
51
  // the same gesture emits on finger-release for this window, so the menu does not
54
52
  // immediately dismiss itself (the classic contextmenu-then-touchend race).
@@ -209,35 +207,10 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
209
207
  return; // nothing to offer
210
208
  }
211
209
  // Make visible (so it has measurable size), then place it within the
212
- // VISIBLE viewport. position:fixed => x/y are viewport coordinates; the
213
- // visual viewport (when present) is the area above the on-screen
214
- // keyboard, so clamping to it keeps the menu off the keyboard too.
210
+ // visible viewport via the shared point-anchored positioner (clamp to
211
+ // the visual viewport; flip above the finger near the bottom edge).
215
212
  menu.classList.add("visible");
216
- const vv = window.visualViewport;
217
- const viewLeft = vv ? vv.offsetLeft : 0;
218
- const viewTop = vv ? vv.offsetTop : 0;
219
- const viewWidth = vv ? vv.width : window.innerWidth;
220
- const viewHeight = vv ? vv.height : window.innerHeight;
221
- const menuW = menu.offsetWidth;
222
- const menuH = menu.offsetHeight;
223
- const left = Math.max(
224
- viewLeft + EDGE_MARGIN,
225
- Math.min(x, viewLeft + viewWidth - menuW - EDGE_MARGIN),
226
- );
227
- // Open just below the point; but if that would overflow the visible
228
- // bottom (a long-press near the bottom edge, where the user naturally
229
- // taps), flip above the finger so the menu is neither clipped off-screen
230
- // nor hidden under the fingertip.
231
- let top = y;
232
- if (y + menuH + EDGE_MARGIN > viewTop + viewHeight) {
233
- top = y - menuH - MENU_GAP;
234
- }
235
- top = Math.max(
236
- viewTop + EDGE_MARGIN,
237
- Math.min(top, viewTop + viewHeight - menuH - EDGE_MARGIN),
238
- );
239
- menu.style.left = `${String(left)}px`;
240
- menu.style.top = `${String(top)}px`;
213
+ placeMenuAt(menu, x, y);
241
214
  // A touch long-press is immediately followed by a synthetic click on
242
215
  // release; swallow it so the menu stays open (see SWALLOW_MS).
243
216
  if (touch) {
@@ -0,0 +1,50 @@
1
+ // menu-position.ts — shared point-anchored placement for the fixed-position
2
+ // menus (the terminal context menu and the tab-strip menu), which previously
3
+ // carried two hand-synced copies of this exact clamp/flip math.
4
+ //
5
+ // position:fixed means x/y are viewport coordinates; the VISUAL viewport
6
+ // (when present) is the area above the on-screen keyboard, so clamping to it
7
+ // keeps a menu off the keyboard too.
8
+
9
+ /** Clamp margin from the visible viewport edges (px). */
10
+ const EDGE_MARGIN = 8;
11
+
12
+ /** Gap between the pointer and a flipped-above menu (px), so the menu is not
13
+ * hidden under the fingertip that long-pressed near the bottom edge. */
14
+ const FLIP_GAP = 16;
15
+
16
+ /** Place an already-visible `menu` at viewport point (x, y): open just below
17
+ * the point, flip above it when that would overflow the visible bottom, and
18
+ * clamp into the visual viewport on both axes. The menu is absolute-positioned
19
+ * against the terminal root (.wt-root), so the viewport-space result is
20
+ * rebased onto its offsetParent's box — placement is identical whether the
21
+ * root fills the viewport (wt-viewport) or an embedder's panel
22
+ * (wt-container). The menu must be measurable (rendered, not display:none)
23
+ * when called. */
24
+ export function placeMenuAt(menu: HTMLElement, x: number, y: number): void {
25
+ const vv = window.visualViewport;
26
+ const viewLeft = vv ? vv.offsetLeft : 0;
27
+ const viewTop = vv ? vv.offsetTop : 0;
28
+ const viewWidth = vv ? vv.width : window.innerWidth;
29
+ const viewHeight = vv ? vv.height : window.innerHeight;
30
+ const menuW = menu.offsetWidth;
31
+ const menuH = menu.offsetHeight;
32
+ const left = Math.max(
33
+ viewLeft + EDGE_MARGIN,
34
+ Math.min(x, viewLeft + viewWidth - menuW - EDGE_MARGIN),
35
+ );
36
+ let top = y;
37
+ if (y + menuH + EDGE_MARGIN > viewTop + viewHeight) {
38
+ top = y - menuH - FLIP_GAP;
39
+ }
40
+ top = Math.max(viewTop + EDGE_MARGIN, Math.min(top, viewTop + viewHeight - menuH - EDGE_MARGIN));
41
+ // Rebase from viewport coordinates onto the positioned ancestor's box: left/
42
+ // top on an absolute element are offsets from the offsetParent's padding box,
43
+ // which for all terminal chrome is .wt-root (getBoundingClientRect is its
44
+ // viewport-space position; the root has no border, so border/padding offsets
45
+ // do not enter). A missing offsetParent (display:none ancestor — the menu is
46
+ // visible when called, so not in practice) falls back to (0,0) viewport base.
47
+ const base = menu.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
48
+ menu.style.left = `${String(Math.round(left - base.left))}px`;
49
+ menu.style.top = `${String(Math.round(top - base.top))}px`;
50
+ }