@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 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 === "Escape") setOpen(false);
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)(BottomSheet, {
9833
- open,
9834
- onClose: () => setOpen(false),
9835
- children
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 (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9839
- ref,
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
- }), document.body);
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 === "Escape") setOpen(false);
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 (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13261
- ref,
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
- }), document.body);
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" && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14493
- ref: listboxRef,
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
- }), document.body),
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 === "Escape") setOpen(false);
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(BottomSheet, {
9809
- open,
9810
- onClose: () => setOpen(false),
9811
- children
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 createPortal(/* @__PURE__ */ jsx("div", {
9815
- ref,
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
- }), document.body);
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 === "Escape") setOpen(false);
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 createPortal(/* @__PURE__ */ jsx("div", {
13237
- ref,
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
- }), document.body);
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" && createPortal(/* @__PURE__ */ jsxs("div", {
14469
- ref: listboxRef,
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
- }), document.body),
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.358",
3
+ "version": "1.2.359",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",