@kahitsan/ksui 0.37.0 → 0.38.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.
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext, type Accessor } from "solid-js";
2
+
3
+ // A Portal-based popover (DatePicker, ComboBox, ...) must mount as a DOM
4
+ // DESCENDANT of an ancestor dialog-variant Modal's <dialog> element, not
5
+ // document.body. dialogEl.showModal() marks everything OUTSIDE the dialog's
6
+ // flat-tree subtree inert, and inertness is computed on the flat tree —
7
+ // independent of the Popover API's top-layer PAINT order (useTopLayer only
8
+ // fixes painting). A popover portaled straight to document.body from inside
9
+ // an open <dialog> paints above it but is excluded from hit-testing, so real
10
+ // clicks never reach it even though it's visibly on top.
11
+ //
12
+ // Default is `null`: no ancestor dialog (standalone usage), or the sheet
13
+ // variant (a plain <div>, no showModal(), so no inertness barrier exists) —
14
+ // both mean "mount to document.body", today's behavior.
15
+ const ModalLayerContext = createContext<Accessor<HTMLElement | null>>(() => null);
16
+
17
+ export const ModalLayerProvider = ModalLayerContext.Provider;
18
+
19
+ /**
20
+ * Mount target for a Portal-based popover: the nearest ancestor dialog's
21
+ * element when rendered inside a dialog-variant Modal, else document.body.
22
+ * Called once per popover-open (the Portal itself only reads `mount` at
23
+ * creation), so a plain `Node` accessor — not a reactive signal — is enough.
24
+ */
25
+ export function usePopoverMount(): () => Node {
26
+ const layer = useContext(ModalLayerContext);
27
+ return () => layer() ?? document.body;
28
+ }
@@ -0,0 +1,66 @@
1
+ import { onMount } from "solid-js";
2
+ import { injectCSS } from "./inject-css";
3
+
4
+ const STYLE_ID = "ksui-toplayer-style";
5
+ // UA is the lowest cascade priority, so any author rule (background, border,
6
+ // padding, width, sizing, ...) already beats [popover]'s UA defaults without
7
+ // help — resetting those was the bug (this stylesheet injects AFTER each
8
+ // component's own CSS, so a same-specificity reset would win by source order
9
+ // and strip the component's real chrome). Only inset and margin are genuine
10
+ // POSITIONING conflicts: UA's inset:0 leaves right/bottom pinned to 0 even
11
+ // after a component's inline `top`/`left` override the top/left portion, and
12
+ // UA's margin:auto re-centers inside that box — both fight the fixed
13
+ // coordinates each popup computes itself.
14
+ const STYLE_CSS = `
15
+ [data-ksui-toplayer]{inset:auto;margin:0;}
16
+ `;
17
+
18
+ /**
19
+ * Promotes `el` into the browser's top layer via the Popover API so it
20
+ * paints above a native <dialog> (Modal's default variant) regardless of
21
+ * z-index — an ordinary element can never out-paint a top-layer one.
22
+ * `"manual"` disables the API's own light-dismiss/Escape handling; every
23
+ * ksui popup keeps its own mousedown/Escape listeners unchanged.
24
+ *
25
+ * Call from the portaled panel's ref, mirroring useFocusTrap's shape:
26
+ * ref={(el) => { popupRef = el; onCleanup(useTopLayer(el)); }}
27
+ *
28
+ * showPopover() is deferred to onMount, not called inline here: this
29
+ * function runs synchronously inside <Portal>'s ref callback, which fires
30
+ * while the node is still detached (Portal appends its container to
31
+ * document.body *after* building the subtree) — el.isConnected is false
32
+ * and el.ownerDocument !== document at that point, so an inline call
33
+ * always throws InvalidStateError. onMount queues onto Solid's render-effect
34
+ * list, which only flushes once the whole synchronous render pass —
35
+ * including Portal's appendChild — has finished, so by the time it runs
36
+ * the node is guaranteed connected. (Same fix shape as Modal's DialogModal,
37
+ * which calls showModal() in onMount rather than in dialogEl's ref.)
38
+ */
39
+ export function useTopLayer(el: HTMLElement | undefined): () => void {
40
+ if (!el) return () => {};
41
+ injectCSS(STYLE_ID, STYLE_CSS);
42
+ el.setAttribute("popover", "manual");
43
+ el.setAttribute("data-ksui-toplayer", "");
44
+ onMount(() => {
45
+ // No Popover API (e.g. jsdom in unit tests) — nothing to promote, not an error.
46
+ if (typeof el.showPopover !== "function") return;
47
+ try {
48
+ el.showPopover();
49
+ } catch (err) {
50
+ // showPopover() throws if already open (rapid re-open); that's the only
51
+ // benign case left once we're guaranteed connected, and it's detectable
52
+ // because the element really is :popover-open despite the throw. Any
53
+ // other failure is real and must surface, not vanish into a no-op.
54
+ if (!el.matches(":popover-open")) {
55
+ console.error("[ksui] useTopLayer: showPopover() failed to promote element", err);
56
+ }
57
+ }
58
+ });
59
+ return () => {
60
+ try {
61
+ el.hidePopover();
62
+ } catch {
63
+ // already hidden, disconnected first, or API unavailable — harmless.
64
+ }
65
+ };
66
+ }