@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
|
@@ -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
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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
|
-
|
|
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
|
|
20
|
-
*
|
|
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
|