@celestia-island/hikari 0.55.172 → 0.55.175

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.172",
3
+ "version": "0.55.175",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -66,3 +66,35 @@ describe("HHoverRevealAction touch reveal modes", () => {
66
66
  expect(el.className).not.toContain("is-revealed");
67
67
  });
68
68
  });
69
+
70
+ describe("HHoverRevealAction bare-click robustness", () => {
71
+ it("reveals on mousedown even without a preceding hover (synthetic clicks)", async () => {
72
+ // Test drivers dispatch a click straight at the extension's coordinates
73
+ // with no mouseenter; without the mousedown reveal the extension stays
74
+ // pointer-events:none and the click passes through to the page below.
75
+ const { el } = await mountHost();
76
+ expect(el.className).not.toContain("is-revealed");
77
+ el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
78
+ await Promise.resolve();
79
+ expect(el.className).toContain("is-revealed");
80
+ });
81
+
82
+ it("keeps the touch suppress window authoritative over mousedown", async () => {
83
+ // Touch interactions fire an emulated mousedown after the tap; during
84
+ // the touch linger the touch path owns the state and a stray emulated
85
+ // mousedown must not re-reveal (which would clear the hide timer).
86
+ vi.useFakeTimers();
87
+ const { el } = await mountHost();
88
+ el.dispatchEvent(new Event("touchstart", { bubbles: true }));
89
+ el.dispatchEvent(new Event("touchend", { bubbles: true }));
90
+ await Promise.resolve();
91
+ expect(el.className).toContain("is-revealed");
92
+ // Emulated mousedown while the touch linger owns the reveal.
93
+ el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
94
+ // After the linger expires the extension hides — the mousedown must
95
+ // not have reset the hide timer (i.e. must have been suppressed).
96
+ vi.advanceTimersByTime(3100);
97
+ await Promise.resolve();
98
+ expect(el.className).not.toContain("is-revealed");
99
+ });
100
+ });
@@ -107,6 +107,17 @@ export default defineComponent({
107
107
  reveal();
108
108
  }
109
109
 
110
+ function onMouseDown() {
111
+ // Bare synthetic clicks (test drivers dispatching a click at the
112
+ // extension's coordinates without a preceding hover move) land while
113
+ // the extension is still pointer-events:none — the click passes
114
+ // through to whatever sits underneath and the action never fires.
115
+ // A mousedown anywhere on the host reveals the extension first; the
116
+ // click that follows in the same interaction then finds it live.
117
+ if (Date.now() < suppressMouseUntil) return;
118
+ reveal();
119
+ }
120
+
110
121
  function onMouseLeave() {
111
122
  if (Date.now() < suppressMouseUntil) return;
112
123
  scheduleHide(props.hideDelay);
@@ -133,6 +144,7 @@ export default defineComponent({
133
144
  "data-placement": props.placement,
134
145
  onMouseenter: onMouseEnter,
135
146
  onMouseleave: onMouseLeave,
147
+ onMousedown: onMouseDown,
136
148
  onTouchstart: onTouchStart,
137
149
  onTouchend: onTouchEnd,
138
150
  onTouchcancel: onTouchEnd,
@@ -15,6 +15,10 @@ import { usePopupManager } from "./usePopupManager";
15
15
  * - escape hatches: empty titles, [data-hk-tooltip-native], elements
16
16
  * inside an HkTooltip wrapper — never engage
17
17
  * - pointerdown hides but stays engaged; scroll/resize and Esc disengage
18
+ * - the popup mounts inside #hk-overlay-root (same home as the component
19
+ * tooltip's teleported popup) — at body level its in-root ordinal z
20
+ * would land under the root's document-space elevation and every
21
+ * overlay-hosted surface (the filter panel popover) clipped it
18
22
  * - idempotent install per document; uninstall tears everything down
19
23
  */
20
24
 
@@ -199,6 +203,13 @@ describe("installHkTooltipBridge", () => {
199
203
  expect(el.getAttribute("title")).toBe("Scrolled");
200
204
  });
201
205
 
206
+ it("mounts the popup inside the overlay root", () => {
207
+ install();
208
+ const root = document.querySelector("#hk-overlay-root");
209
+ expect(root).not.toBeNull();
210
+ expect(bridgePopup()?.parentElement).toBe(root);
211
+ });
212
+
202
213
  it("is idempotent per document and uninstallable", () => {
203
214
  vi.useFakeTimers();
204
215
  install();
@@ -25,9 +25,15 @@
25
25
  // - empty / whitespace-only titles never engage
26
26
  //
27
27
  // The bridge holds one popup-manager handle (kind "tooltip"), so its
28
- // popups share the tooltip z band with component tooltips. Installing
29
- // twice on the same document is idempotent: the second install returns
30
- // the first install's uninstall.
28
+ // popups share the tooltip z band with component tooltips — inside the
29
+ // overlay root, which is where the popup MOUNTS (same home as the
30
+ // HkTooltip component's teleported popup: the in-root ordinal z is
31
+ // meaningless at body level once #hk-overlay-root carries its own
32
+ // document-space elevation). Installing twice on the same document is
33
+ // idempotent: the second install returns the first install's uninstall.
34
+ // Side effect: ensures #hk-overlay-root exists before the install appends
35
+ // the popup (the component tooltip's Teleport target is the same node).
36
+ import { ensureOverlayRoot } from "../overlay/overlayRoot";
31
37
  import { applyTooltipPosition, type TooltipPlacement } from "./tooltipPosition";
32
38
  import { usePopupManager, type PopupHandle } from "./usePopupManager";
33
39
  import { reportHkRuntime, type HkRuntimeHandle } from "./registry";
@@ -206,7 +212,19 @@ export function installHkTooltipBridge(options: HkTooltipBridgeOptions = {}): ()
206
212
  const content = document.createElement("div");
207
213
  content.className = "hk-tooltip-content";
208
214
  popup.appendChild(content);
209
- document.body.appendChild(popup);
215
+ // Mount into the overlay root, NOT <body> (2026-10-16 defect report):
216
+ // the manager's z here is the IN-ROOT ordinal band (tooltip 300, above
217
+ // anchored popups 200) — it only competes inside the root's
218
+ // `isolation: isolate` fence. At body level the same number is a
219
+ // near-bottom document z, far under `#hk-overlay-root`'s own elevation
220
+ // (--hk-z-overlay-root, 90000), so every tooltip the bridge showed over
221
+ // an overlay-hosted surface — the filter panel's operator `title=`s
222
+ // over its popover — painted UNDER the panel and was clipped by it.
223
+ // The component tooltips teleport into the root for exactly this
224
+ // reason; the bridge shares their popup classes AND must share their
225
+ // home. `pointer-events` stays none on both the root and the popup, so
226
+ // the hosting subtree changes nothing about hit-testing.
227
+ ensureOverlayRoot().appendChild(popup);
210
228
 
211
229
  const state: BridgeState = {
212
230
  root: document,
@@ -16,12 +16,19 @@ export const TOOLTIP_GAP_PX = 8;
16
16
  * Build the inline style that pins a `position: fixed` popup to one side
17
17
  * of a trigger rect.
18
18
  *
19
- * The popup teleports to <body> — inside any root CSS zoom subtree —
20
- * while `rect` is already in the root visual space (standardized zoom
19
+ * The popup floats OUTSIDE the page's zoomed subtrees — the HkTooltip
20
+ * component teleports it into #hk-overlay-root and the bridge appends
21
+ * it there too, both mounted under unzoomed <body>, outside every
22
+ * zoomed subtree — while `rect`
23
+ * is already in the root visual space (standardized zoom
21
24
  * reports ancestor zoom applied). The browser scales the tooltip's local
22
25
  * px back up at paint, so the visual rect values must be written divided
23
26
  * by the cumulative zoom or the tooltip drifts zoom× off its trigger
24
- * (chest's root-level manual DPI scale).
27
+ * (chest's root-level manual DPI scale). This contract quietly assumes
28
+ * the overlay root itself never carries a CSS zoom: the anchor math here
29
+ * divides by ancestorZoom(document.body) only, while the clamp pass
30
+ * below divides by ancestorZoom(popup) — the two agree exactly because
31
+ * the root sits unzoomed between them.
25
32
  *
26
33
  * `width: max-content` is the position-independence contract (same rule
27
34
  * as .hk-popover-panel): a fixed element otherwise shrink-to-fits against