@camstack/ui-library 1.2.358 → 1.2.359
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/CHANGELOG.md +6 -0
- package/dist/index.cjs +60 -15
- package/dist/index.js +60 -15
- package/dist/primitives/top-layer-portal.d.ts +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.359] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Pickers, dropdown menus and popovers now open on top of a modal dialog instead of behind it
|
|
15
|
+
|
|
10
16
|
## [1.2.358] - 2026-10-30
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/index.cjs
CHANGED
|
@@ -9693,6 +9693,38 @@ function BottomSheet({ open, onClose, title, children, className }) {
|
|
|
9693
9693
|
})] });
|
|
9694
9694
|
}
|
|
9695
9695
|
//#endregion
|
|
9696
|
+
//#region src/primitives/top-layer-portal.tsx
|
|
9697
|
+
/**
|
|
9698
|
+
* Where a floating surface (popover, dropdown, listbox) is mounted so it shows
|
|
9699
|
+
* above everything. A modal `<dialog>` sits in the browser's top layer, which
|
|
9700
|
+
* paints over anything in `document.body` and makes it inert — so a surface
|
|
9701
|
+
* portalled to the body opens "under" the dialog and cannot be clicked. The
|
|
9702
|
+
* surface is therefore mounted INSIDE the trigger's dialog (still interactive)
|
|
9703
|
+
* and itself promoted to the top layer with the native popover API, which also
|
|
9704
|
+
* frees it from the dialog's transform and overflow clipping: its `fixed`
|
|
9705
|
+
* coordinates stay viewport coordinates. Outside a dialog it goes to the body
|
|
9706
|
+
* as before. Where the popover API is missing it degrades to a plain `fixed`
|
|
9707
|
+
* element.
|
|
9708
|
+
*/
|
|
9709
|
+
function TopLayerPortal({ anchor, surfaceRef, className, children, ...props }) {
|
|
9710
|
+
const host = anchor.current?.closest("dialog") ?? document.body;
|
|
9711
|
+
(0, react$1.useLayoutEffect)(() => {
|
|
9712
|
+
const el = surfaceRef?.current;
|
|
9713
|
+
if (!el) return;
|
|
9714
|
+
if (typeof el.showPopover !== "function") return;
|
|
9715
|
+
el.setAttribute("popover", "manual");
|
|
9716
|
+
try {
|
|
9717
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
9718
|
+
} catch {}
|
|
9719
|
+
}, [surfaceRef]);
|
|
9720
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9721
|
+
ref: surfaceRef,
|
|
9722
|
+
className: cn("inset-auto m-0 text-foreground", className),
|
|
9723
|
+
...props,
|
|
9724
|
+
children
|
|
9725
|
+
}), host);
|
|
9726
|
+
}
|
|
9727
|
+
//#endregion
|
|
9696
9728
|
//#region src/primitives/popover.tsx
|
|
9697
9729
|
var PopoverContext = (0, react$1.createContext)(null);
|
|
9698
9730
|
function usePopoverContext() {
|
|
@@ -9758,6 +9790,7 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9758
9790
|
const { open, setOpen, contentId, triggerId, triggerRef } = usePopoverContext();
|
|
9759
9791
|
const isMobile = useIsMobile();
|
|
9760
9792
|
const ref = (0, react$1.useRef)(null);
|
|
9793
|
+
const sheetRef = (0, react$1.useRef)(null);
|
|
9761
9794
|
const [coords, setCoords] = (0, react$1.useState)(null);
|
|
9762
9795
|
const [placement, setPlacement] = (0, react$1.useState)(null);
|
|
9763
9796
|
(0, react$1.useLayoutEffect)(() => {
|
|
@@ -9814,7 +9847,9 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9814
9847
|
if (el && !el.contains(e.target) && !trigger?.contains(e.target)) setOpen(false);
|
|
9815
9848
|
};
|
|
9816
9849
|
const escHandler = (e) => {
|
|
9817
|
-
if (e.key
|
|
9850
|
+
if (e.key !== "Escape") return;
|
|
9851
|
+
e.preventDefault();
|
|
9852
|
+
setOpen(false);
|
|
9818
9853
|
};
|
|
9819
9854
|
document.addEventListener("mousedown", handler);
|
|
9820
9855
|
document.addEventListener("keydown", escHandler);
|
|
@@ -9829,14 +9864,20 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9829
9864
|
isMobile
|
|
9830
9865
|
]);
|
|
9831
9866
|
if (!open) return null;
|
|
9832
|
-
if (isMobile) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9867
|
+
if (isMobile) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopLayerPortal, {
|
|
9868
|
+
anchor: triggerRef,
|
|
9869
|
+
surfaceRef: sheetRef,
|
|
9870
|
+
className: "pointer-events-none fixed inset-0 h-dvh w-dvw max-w-none overflow-visible border-0 bg-transparent p-0 [&>*]:pointer-events-auto",
|
|
9871
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BottomSheet, {
|
|
9872
|
+
open,
|
|
9873
|
+
onClose: () => setOpen(false),
|
|
9874
|
+
children
|
|
9875
|
+
})
|
|
9836
9876
|
});
|
|
9837
9877
|
if (!coords || typeof document === "undefined") return null;
|
|
9838
|
-
return
|
|
9839
|
-
|
|
9878
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopLayerPortal, {
|
|
9879
|
+
anchor: triggerRef,
|
|
9880
|
+
surfaceRef: ref,
|
|
9840
9881
|
id: contentId,
|
|
9841
9882
|
role: "dialog",
|
|
9842
9883
|
"aria-labelledby": triggerId,
|
|
@@ -9850,7 +9891,7 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9850
9891
|
className: cn("z-50 overflow-y-auto bg-background-elevated border border-border rounded-lg shadow-lg p-3", className),
|
|
9851
9892
|
...props,
|
|
9852
9893
|
children
|
|
9853
|
-
})
|
|
9894
|
+
});
|
|
9854
9895
|
}
|
|
9855
9896
|
//#endregion
|
|
9856
9897
|
//#region src/composites/icon-picker.tsx
|
|
@@ -13243,7 +13284,9 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13243
13284
|
if (el && !el.contains(e.target) && !trigger?.contains(e.target)) setOpen(false);
|
|
13244
13285
|
};
|
|
13245
13286
|
const escHandler = (e) => {
|
|
13246
|
-
if (e.key
|
|
13287
|
+
if (e.key !== "Escape") return;
|
|
13288
|
+
e.preventDefault();
|
|
13289
|
+
setOpen(false);
|
|
13247
13290
|
};
|
|
13248
13291
|
document.addEventListener("mousedown", handler);
|
|
13249
13292
|
document.addEventListener("keydown", escHandler);
|
|
@@ -13257,8 +13300,9 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13257
13300
|
triggerRef
|
|
13258
13301
|
]);
|
|
13259
13302
|
if (!open || !coords || typeof document === "undefined") return null;
|
|
13260
|
-
return
|
|
13261
|
-
|
|
13303
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopLayerPortal, {
|
|
13304
|
+
anchor: triggerRef,
|
|
13305
|
+
surfaceRef: ref,
|
|
13262
13306
|
id: contentId,
|
|
13263
13307
|
role: "menu",
|
|
13264
13308
|
"aria-labelledby": triggerId,
|
|
@@ -13271,7 +13315,7 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13271
13315
|
className: cn("z-50 bg-background-elevated border border-border rounded-md shadow-lg py-1 min-w-[160px]", className),
|
|
13272
13316
|
...props,
|
|
13273
13317
|
children
|
|
13274
|
-
})
|
|
13318
|
+
});
|
|
13275
13319
|
}
|
|
13276
13320
|
function DropdownItem({ className, icon: Icon, variant = "default", children, onClick, ...props }) {
|
|
13277
13321
|
const { setOpen } = useDropdownContext();
|
|
@@ -14489,8 +14533,9 @@ function OptionSelect(props) {
|
|
|
14489
14533
|
children: selectedOptions[0]?.label ?? triggerPlaceholder
|
|
14490
14534
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" })]
|
|
14491
14535
|
}),
|
|
14492
|
-
open && !isDisabled && coords !== null && typeof document !== "undefined" &&
|
|
14493
|
-
|
|
14536
|
+
open && !isDisabled && coords !== null && typeof document !== "undefined" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TopLayerPortal, {
|
|
14537
|
+
anchor: triggerRef,
|
|
14538
|
+
surfaceRef: listboxRef,
|
|
14494
14539
|
id: listboxId,
|
|
14495
14540
|
role: "listbox",
|
|
14496
14541
|
style: {
|
|
@@ -14526,7 +14571,7 @@ function OptionSelect(props) {
|
|
|
14526
14571
|
},
|
|
14527
14572
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5 shrink-0" }), createLabel]
|
|
14528
14573
|
})]
|
|
14529
|
-
}),
|
|
14574
|
+
}),
|
|
14530
14575
|
description !== void 0 && description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
14531
14576
|
className: DESC_CLASS$1,
|
|
14532
14577
|
children: description
|
package/dist/index.js
CHANGED
|
@@ -9669,6 +9669,38 @@ function BottomSheet({ open, onClose, title, children, className }) {
|
|
|
9669
9669
|
})] });
|
|
9670
9670
|
}
|
|
9671
9671
|
//#endregion
|
|
9672
|
+
//#region src/primitives/top-layer-portal.tsx
|
|
9673
|
+
/**
|
|
9674
|
+
* Where a floating surface (popover, dropdown, listbox) is mounted so it shows
|
|
9675
|
+
* above everything. A modal `<dialog>` sits in the browser's top layer, which
|
|
9676
|
+
* paints over anything in `document.body` and makes it inert — so a surface
|
|
9677
|
+
* portalled to the body opens "under" the dialog and cannot be clicked. The
|
|
9678
|
+
* surface is therefore mounted INSIDE the trigger's dialog (still interactive)
|
|
9679
|
+
* and itself promoted to the top layer with the native popover API, which also
|
|
9680
|
+
* frees it from the dialog's transform and overflow clipping: its `fixed`
|
|
9681
|
+
* coordinates stay viewport coordinates. Outside a dialog it goes to the body
|
|
9682
|
+
* as before. Where the popover API is missing it degrades to a plain `fixed`
|
|
9683
|
+
* element.
|
|
9684
|
+
*/
|
|
9685
|
+
function TopLayerPortal({ anchor, surfaceRef, className, children, ...props }) {
|
|
9686
|
+
const host = anchor.current?.closest("dialog") ?? document.body;
|
|
9687
|
+
useLayoutEffect(() => {
|
|
9688
|
+
const el = surfaceRef?.current;
|
|
9689
|
+
if (!el) return;
|
|
9690
|
+
if (typeof el.showPopover !== "function") return;
|
|
9691
|
+
el.setAttribute("popover", "manual");
|
|
9692
|
+
try {
|
|
9693
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
9694
|
+
} catch {}
|
|
9695
|
+
}, [surfaceRef]);
|
|
9696
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
9697
|
+
ref: surfaceRef,
|
|
9698
|
+
className: cn("inset-auto m-0 text-foreground", className),
|
|
9699
|
+
...props,
|
|
9700
|
+
children
|
|
9701
|
+
}), host);
|
|
9702
|
+
}
|
|
9703
|
+
//#endregion
|
|
9672
9704
|
//#region src/primitives/popover.tsx
|
|
9673
9705
|
var PopoverContext = createContext(null);
|
|
9674
9706
|
function usePopoverContext() {
|
|
@@ -9734,6 +9766,7 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9734
9766
|
const { open, setOpen, contentId, triggerId, triggerRef } = usePopoverContext();
|
|
9735
9767
|
const isMobile = useIsMobile();
|
|
9736
9768
|
const ref = useRef(null);
|
|
9769
|
+
const sheetRef = useRef(null);
|
|
9737
9770
|
const [coords, setCoords] = useState(null);
|
|
9738
9771
|
const [placement, setPlacement] = useState(null);
|
|
9739
9772
|
useLayoutEffect(() => {
|
|
@@ -9790,7 +9823,9 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9790
9823
|
if (el && !el.contains(e.target) && !trigger?.contains(e.target)) setOpen(false);
|
|
9791
9824
|
};
|
|
9792
9825
|
const escHandler = (e) => {
|
|
9793
|
-
if (e.key
|
|
9826
|
+
if (e.key !== "Escape") return;
|
|
9827
|
+
e.preventDefault();
|
|
9828
|
+
setOpen(false);
|
|
9794
9829
|
};
|
|
9795
9830
|
document.addEventListener("mousedown", handler);
|
|
9796
9831
|
document.addEventListener("keydown", escHandler);
|
|
@@ -9805,14 +9840,20 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9805
9840
|
isMobile
|
|
9806
9841
|
]);
|
|
9807
9842
|
if (!open) return null;
|
|
9808
|
-
if (isMobile) return /* @__PURE__ */ jsx(
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9843
|
+
if (isMobile) return /* @__PURE__ */ jsx(TopLayerPortal, {
|
|
9844
|
+
anchor: triggerRef,
|
|
9845
|
+
surfaceRef: sheetRef,
|
|
9846
|
+
className: "pointer-events-none fixed inset-0 h-dvh w-dvw max-w-none overflow-visible border-0 bg-transparent p-0 [&>*]:pointer-events-auto",
|
|
9847
|
+
children: /* @__PURE__ */ jsx(BottomSheet, {
|
|
9848
|
+
open,
|
|
9849
|
+
onClose: () => setOpen(false),
|
|
9850
|
+
children
|
|
9851
|
+
})
|
|
9812
9852
|
});
|
|
9813
9853
|
if (!coords || typeof document === "undefined") return null;
|
|
9814
|
-
return
|
|
9815
|
-
|
|
9854
|
+
return /* @__PURE__ */ jsx(TopLayerPortal, {
|
|
9855
|
+
anchor: triggerRef,
|
|
9856
|
+
surfaceRef: ref,
|
|
9816
9857
|
id: contentId,
|
|
9817
9858
|
role: "dialog",
|
|
9818
9859
|
"aria-labelledby": triggerId,
|
|
@@ -9826,7 +9867,7 @@ function PopoverContent({ className, children, align = "start", offset = 4, ...p
|
|
|
9826
9867
|
className: cn("z-50 overflow-y-auto bg-background-elevated border border-border rounded-lg shadow-lg p-3", className),
|
|
9827
9868
|
...props,
|
|
9828
9869
|
children
|
|
9829
|
-
})
|
|
9870
|
+
});
|
|
9830
9871
|
}
|
|
9831
9872
|
//#endregion
|
|
9832
9873
|
//#region src/composites/icon-picker.tsx
|
|
@@ -13219,7 +13260,9 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13219
13260
|
if (el && !el.contains(e.target) && !trigger?.contains(e.target)) setOpen(false);
|
|
13220
13261
|
};
|
|
13221
13262
|
const escHandler = (e) => {
|
|
13222
|
-
if (e.key
|
|
13263
|
+
if (e.key !== "Escape") return;
|
|
13264
|
+
e.preventDefault();
|
|
13265
|
+
setOpen(false);
|
|
13223
13266
|
};
|
|
13224
13267
|
document.addEventListener("mousedown", handler);
|
|
13225
13268
|
document.addEventListener("keydown", escHandler);
|
|
@@ -13233,8 +13276,9 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13233
13276
|
triggerRef
|
|
13234
13277
|
]);
|
|
13235
13278
|
if (!open || !coords || typeof document === "undefined") return null;
|
|
13236
|
-
return
|
|
13237
|
-
|
|
13279
|
+
return /* @__PURE__ */ jsx(TopLayerPortal, {
|
|
13280
|
+
anchor: triggerRef,
|
|
13281
|
+
surfaceRef: ref,
|
|
13238
13282
|
id: contentId,
|
|
13239
13283
|
role: "menu",
|
|
13240
13284
|
"aria-labelledby": triggerId,
|
|
@@ -13247,7 +13291,7 @@ function DropdownContent({ className, children, align = "start", offset = 4, ...
|
|
|
13247
13291
|
className: cn("z-50 bg-background-elevated border border-border rounded-md shadow-lg py-1 min-w-[160px]", className),
|
|
13248
13292
|
...props,
|
|
13249
13293
|
children
|
|
13250
|
-
})
|
|
13294
|
+
});
|
|
13251
13295
|
}
|
|
13252
13296
|
function DropdownItem({ className, icon: Icon, variant = "default", children, onClick, ...props }) {
|
|
13253
13297
|
const { setOpen } = useDropdownContext();
|
|
@@ -14465,8 +14509,9 @@ function OptionSelect(props) {
|
|
|
14465
14509
|
children: selectedOptions[0]?.label ?? triggerPlaceholder
|
|
14466
14510
|
}), /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" })]
|
|
14467
14511
|
}),
|
|
14468
|
-
open && !isDisabled && coords !== null && typeof document !== "undefined" &&
|
|
14469
|
-
|
|
14512
|
+
open && !isDisabled && coords !== null && typeof document !== "undefined" && /* @__PURE__ */ jsxs(TopLayerPortal, {
|
|
14513
|
+
anchor: triggerRef,
|
|
14514
|
+
surfaceRef: listboxRef,
|
|
14470
14515
|
id: listboxId,
|
|
14471
14516
|
role: "listbox",
|
|
14472
14517
|
style: {
|
|
@@ -14502,7 +14547,7 @@ function OptionSelect(props) {
|
|
|
14502
14547
|
},
|
|
14503
14548
|
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 shrink-0" }), createLabel]
|
|
14504
14549
|
})]
|
|
14505
|
-
}),
|
|
14550
|
+
}),
|
|
14506
14551
|
description !== void 0 && description.length > 0 && /* @__PURE__ */ jsx("p", {
|
|
14507
14552
|
className: DESC_CLASS$1,
|
|
14508
14553
|
children: description
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type HTMLAttributes, type JSX, type ReactNode, type RefObject } from "react";
|
|
2
|
+
export interface TopLayerPortalProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** The trigger the surface belongs to; its enclosing `<dialog>`, if any, hosts the surface. */
|
|
4
|
+
readonly anchor: RefObject<Element | null>;
|
|
5
|
+
/** The surface element, for the caller's outside-click and collision logic. */
|
|
6
|
+
readonly surfaceRef?: RefObject<HTMLDivElement | null>;
|
|
7
|
+
readonly children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Where a floating surface (popover, dropdown, listbox) is mounted so it shows
|
|
11
|
+
* above everything. A modal `<dialog>` sits in the browser's top layer, which
|
|
12
|
+
* paints over anything in `document.body` and makes it inert — so a surface
|
|
13
|
+
* portalled to the body opens "under" the dialog and cannot be clicked. The
|
|
14
|
+
* surface is therefore mounted INSIDE the trigger's dialog (still interactive)
|
|
15
|
+
* and itself promoted to the top layer with the native popover API, which also
|
|
16
|
+
* frees it from the dialog's transform and overflow clipping: its `fixed`
|
|
17
|
+
* coordinates stay viewport coordinates. Outside a dialog it goes to the body
|
|
18
|
+
* as before. Where the popover API is missing it degrades to a plain `fixed`
|
|
19
|
+
* element.
|
|
20
|
+
*/
|
|
21
|
+
export declare function TopLayerPortal({ anchor, surfaceRef, className, children, ...props }: TopLayerPortalProps): JSX.Element | null;
|