kerfjs 5.0.0-beta.53 → 5.0.0-beta.55

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/dist/overlay.d.ts CHANGED
@@ -26,7 +26,11 @@ interface AnchorPositionOptions {
26
26
  * One-shot: position `el` relative to `anchor` — below by default, flipping above
27
27
  * if it would overflow the viewport, aligned to a horizontal edge and clamped into
28
28
  * view. Sets `el.style` `position: fixed`, `margin: 0`, `left`, and `top` (fixed so
29
- * `left`/`top` are viewport coordinates, matching `getBoundingClientRect`). This is
29
+ * `left`/`top` are viewport coordinates, matching `getBoundingClientRect`). When an
30
+ * ancestor makes itself the containing block for fixed descendants (a
31
+ * `transform`, `filter`, `perspective`, or `contain` on a wrapping `<dialog>`),
32
+ * `left`/`top` are translated (and unscaled) into that box's coordinates so `el`
33
+ * still lands next to the anchor. This is
30
34
  * `popover()`'s placement core, usable on any element (an inline hint, a tooltip) —
31
35
  * no overlay lifecycle. Pair with {@link autoReposition} to keep it glued while open.
32
36
  */
@@ -116,7 +120,12 @@ interface OverlayHandle {
116
120
  declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
117
121
  /** Options for {@link popover}. */
118
122
  interface PopoverOptions {
119
- /** Where to append the popover wrapper. Default `document.body`. */
123
+ /**
124
+ * Where to append the popover wrapper. Default: when `anchor` sits inside an
125
+ * open modal `<dialog>`, that dialog's `[data-kerf-overlay-host]` element (a
126
+ * dialog kerf opened gets one automatically), so the popover is part of the
127
+ * modal subtree and stays interactive; otherwise `document.body`.
128
+ */
120
129
  container?: Element;
121
130
  /** Class on the wrapper. Default `'kerf-popover'`. */
122
131
  className?: string;
@@ -160,7 +169,7 @@ declare function popover(anchor: Element, content: OverlayContent, options?: Pop
160
169
  type TooltipContent = string | SafeHtml | (() => MountResult);
161
170
  /** Options for {@link tooltip}. */
162
171
  interface TooltipOptions extends AnchorPositionOptions {
163
- /** Where to append the tooltip wrapper. Default `document.body`. */
172
+ /** Where to append the tooltip wrapper. Default: the anchor's modal-dialog host slot (see {@link PopoverOptions.container}), else `document.body`. */
164
173
  container?: Element;
165
174
  /** Class on the wrapper. Default `'kerf-tooltip'`. */
166
175
  className?: string;
package/dist/overlay.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { attach } from './chunk-FM6N2B4N.js';
2
- import { mount } from './chunk-HTXQR2KL.js';
2
+ import { mount } from './chunk-WMRFXSLG.js';
3
3
  import { delegate } from './chunk-X6OQ2HTR.js';
4
4
 
5
5
 
6
- import { jsx, escapeHtml } from './chunk-DU35YGKW.js';
7
-
6
+ import { jsx, escapeHtml } from './chunk-UGNPDLNI.js';
8
7
 
8
+ import { OVERLAY_HOST_SELECTOR } from './chunk-4TX6YJDM.js';
9
9
  import { devHooks } from './chunk-3C6F6NM4.js';
10
10
 
11
11
  // src/overlay-position.ts
@@ -26,8 +26,19 @@ function positionAnchored(el, anchor, options = {}) {
26
26
  below = true;
27
27
  let left = align === "end" ? anchorRect.right - elementRect.width : anchorRect.left;
28
28
  left = Math.max(0, Math.min(left, viewportWidth - elementRect.width));
29
- el.style.left = `${left}px`;
30
- el.style.top = `${below ? belowTop : aboveTop}px`;
29
+ const [originX, originY, scaleX, scaleY] = fixedOrigin(el);
30
+ el.style.left = `${(left - originX) / scaleX}px`;
31
+ el.style.top = `${((below ? belowTop : aboveTop) - originY) / scaleY}px`;
32
+ }
33
+ function fixedOrigin(el) {
34
+ const parent = el.parentNode;
35
+ if (!parent) return [0, 0, 1, 1];
36
+ const probe = el.ownerDocument.createElement("div");
37
+ probe.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;margin:0;padding:0;border:0";
38
+ parent.insertBefore(probe, el);
39
+ const rect = probe.getBoundingClientRect();
40
+ probe.remove();
41
+ return [rect.left, rect.top, rect.width || 1, rect.height || 1];
31
42
  }
32
43
  function autoReposition(el, anchor, options = {}) {
33
44
  const reposition = () => positionAnchored(el, anchor, options);
@@ -57,22 +68,58 @@ function supportsPopover() {
57
68
  }
58
69
  var OVERLAY_ARBITRATION = /* @__PURE__ */ Symbol("kerf.overlayArbitration");
59
70
  function overlayArbitration(doc) {
60
- return doc[OVERLAY_ARBITRATION] ??= {
71
+ const arbitrated = doc;
72
+ return arbitrated[OVERLAY_ARBITRATION] ??= {
61
73
  stack: [],
62
- handled: /* @__PURE__ */ new WeakSet()
74
+ handled: /* @__PURE__ */ new WeakSet(),
75
+ // KF-AHY6H4: an app-owned modal `<dialog>` opened outside kerf would leave
76
+ // the kerf surfaces already showing inert beneath it. A dialog fires
77
+ // `toggle` after it opens (it does not bubble, so listen in the capture
78
+ // phase); re-host above it exactly as kerf does for its own dialogs, which
79
+ // are skipped here because `overlay()` already re-hosted synchronously.
80
+ onToggle: (event) => {
81
+ const dialog = event.target;
82
+ const arbitration = arbitrated[OVERLAY_ARBITRATION];
83
+ if (dialog.tagName === "DIALOG" && !arbitration.stack.some(({ el }) => el === dialog) && isOpenModal(dialog, arbitration))
84
+ rehostAbove(arbitration, dialog);
85
+ }
63
86
  };
64
87
  }
65
- function modalDialogOpen(doc, arbitration) {
66
- if (arbitration.stack.some(
67
- ({ el, modal }) => modal && el.isConnected && el.open === true
68
- ))
88
+ function rehostAbove(arbitration, dialog) {
89
+ for (const { el, liftAbove } of arbitration.stack)
90
+ if (el !== dialog && el.isConnected && !dialog.contains(el)) liftAbove?.();
91
+ }
92
+ function isOpenModal(dialog, arbitration) {
93
+ if (!dialog.isConnected || dialog.open !== true)
94
+ return false;
95
+ if (arbitration.stack.some(({ el, modal }) => modal && el === dialog))
69
96
  return true;
70
97
  try {
71
- for (const dialog of doc.querySelectorAll("dialog[open]"))
72
- if (dialog.matches(":modal")) return true;
98
+ return dialog.matches(":modal");
73
99
  } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function modalDialogOpen(doc, arbitration) {
104
+ return arbitration.stack.some(({ el }) => isOpenModal(el, arbitration)) || Array.from(doc.querySelectorAll("dialog[open]")).some(
105
+ (dialog) => isOpenModal(dialog, arbitration)
106
+ );
107
+ }
108
+ function overlayHost(anchor) {
109
+ const arbitration = overlayArbitration(document);
110
+ const dialog = anchor.closest("dialog");
111
+ if (dialog === null || !isOpenModal(dialog, arbitration)) return void 0;
112
+ let host = Array.from(dialog.querySelectorAll(OVERLAY_HOST_SELECTOR)).find(
113
+ (el) => el.closest("dialog") === dialog
114
+ );
115
+ if (host === void 0 && arbitration.stack.some(({ el }) => el === dialog)) {
116
+ dialog.insertAdjacentHTML(
117
+ "beforeend",
118
+ '<div data-kerf-overlay-host data-morph-skip data-morph-preserve style="display:contents"></div>'
119
+ );
120
+ host = dialog.lastElementChild;
74
121
  }
75
- return false;
122
+ return host;
76
123
  }
77
124
  function ownsInput(stack, self, kind) {
78
125
  for (let i = stack.indexOf(self) + 1; i < stack.length; i++) {
@@ -108,7 +155,8 @@ function overlay(content, options = {}) {
108
155
  assertInitialFocusSelector(initialFocus);
109
156
  const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
110
157
  const arbitration = overlayArbitration(document);
111
- const liftOverModal = !native && modalDialogOpen(document, arbitration);
158
+ const containerDialog = container.closest("dialog");
159
+ const liftOverModal = !native && !(containerDialog !== null && isOpenModal(containerDialog, arbitration)) && modalDialogOpen(document, arbitration);
112
160
  const topLayer = native || liftOverModal;
113
161
  const useDialog = topLayer && trap && supportsDialog();
114
162
  const usePopover = topLayer && !trap && supportsPopover();
@@ -157,6 +205,8 @@ function overlay(content, options = {}) {
157
205
  const { stack } = arbitration;
158
206
  const stackIndex = stack.indexOf(surface);
159
207
  if (stackIndex !== -1) stack.splice(stackIndex, 1);
208
+ if (stack.length === 0)
209
+ document.removeEventListener("toggle", arbitration.onToggle, true);
160
210
  for (const other of stack)
161
211
  if (other.restoreTo !== null && wrapper.contains(other.restoreTo))
162
212
  other.restoreTo = surface.restoreTo;
@@ -191,18 +241,22 @@ function overlay(content, options = {}) {
191
241
  wrapper,
192
242
  typeof content === "function" ? content : () => content
193
243
  );
194
- arbitration.stack.push(surface);
244
+ if (arbitration.stack.push(surface) === 1)
245
+ document.addEventListener("toggle", arbitration.onToggle, true);
195
246
  if (useDialog) {
196
247
  wrapper.showModal();
197
248
  nativeOpened = true;
198
- for (const other of arbitration.stack)
199
- if (other !== surface && other.el.isConnected) other.liftAbove?.();
249
+ rehostAbove(arbitration, wrapper);
200
250
  } else if (usePopover) {
201
251
  wrapper.showPopover();
202
252
  nativeOpened = true;
203
253
  wrapper.style.inset = "auto";
204
254
  }
205
255
  installDismissal();
256
+ if (container.closest(OVERLAY_HOST_SELECTOR) !== null) {
257
+ makeTabReachable();
258
+ removers.push(attach(wrapper, () => close));
259
+ }
206
260
  applyInitialFocus();
207
261
  } catch (error) {
208
262
  close();
@@ -283,6 +337,14 @@ function overlay(content, options = {}) {
283
337
  );
284
338
  }
285
339
  }
340
+ function makeTabReachable() {
341
+ focusable(wrapper);
342
+ const onTab = (event) => {
343
+ if (event.key === "Tab") focusable(wrapper);
344
+ };
345
+ document.addEventListener("keydown", onTab, true);
346
+ removers.push(() => document.removeEventListener("keydown", onTab, true));
347
+ }
286
348
  function applyInitialFocus() {
287
349
  if (initialFocus !== false) {
288
350
  if (typeof initialFocus === "string") {
@@ -339,7 +401,7 @@ function popover(anchor, content, options = {}) {
339
401
  } = options;
340
402
  const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
341
403
  const handle = overlay(content, {
342
- container,
404
+ container: container ?? overlayHost(anchor),
343
405
  className,
344
406
  dismiss,
345
407
  trap: false,
@@ -382,7 +444,7 @@ function tooltip(anchor, content, options = {}) {
382
444
  function show() {
383
445
  if (!anchor.isConnected) return;
384
446
  const handle = overlay(body, {
385
- container,
447
+ container: container ?? overlayHost(anchor),
386
448
  className,
387
449
  dismiss: false,
388
450
  trap: false,
@@ -403,6 +465,9 @@ function tooltip(anchor, content, options = {}) {
403
465
  hide();
404
466
  });
405
467
  current = { handle, stop, unwatch };
468
+ void handle.result.then(() => {
469
+ if (current?.handle === handle) hide();
470
+ });
406
471
  }
407
472
  function hide() {
408
473
  const shown = current;