@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.
- package/package.json +1 -1
- package/src/components/base/AddAttachmentTile.tsx +8 -2
- package/src/components/base/BadgeSelect.tsx +8 -2
- package/src/components/base/DatePicker.test.tsx +58 -1
- package/src/components/base/DatePicker.tsx +36 -3
- package/src/components/base/Modal.tsx +9 -1
- package/src/components/composite/ComboBox.tsx +15 -5
- package/src/components/composite/MentionTextarea.tsx +8 -2
- package/src/components/composite/PaymentAccountPicker.tsx +8 -2
- package/src/components/composite/SearchableSelect.tsx +8 -2
- package/src/components/composite/VoucherPicker.test.tsx +341 -21
- package/src/components/composite/VoucherPicker.tsx +396 -135
- package/src/utils/modal-layer.ts +28 -0
- package/src/utils/top-layer.ts +66 -0
|
@@ -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
|
+
}
|