nuvox 0.1.3 → 0.1.4

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
@@ -3,6 +3,48 @@
3
3
  All notable changes to this project are documented here.
4
4
  Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## [0.1.4]
7
+
8
+ ### Fixed
9
+ - The `[data-nuvox-portal]` exemption added in 0.1.3 fixed a nested
10
+ overlay closing its own container, but was too broad: it exempted
11
+ a click inside ANY portal, including one belonging to a completely
12
+ unrelated SIBLING overlay that merely shares a common enclosing
13
+ overlay. Two `DropdownMenu`s both living inside the same `Popover`
14
+ are siblings of each other, not nested in each other — opening one
15
+ while the other was open left both on screen at once, only closing
16
+ on a re-click or a selection. `useClickOutside` now walks the real
17
+ relationship instead of doing a blanket portal check: it follows
18
+ "which portal contains this click" → "whose trigger opened that
19
+ portal" → "is that trigger contained in my own refs, or itself
20
+ inside another portal" (repeating for arbitrarily deep nesting) —
21
+ which correctly tells "genuinely nested inside me" apart from
22
+ "elsewhere inside a shared ancestor," however many levels deep.
23
+ `ThemedPortal` now tracks each portal's own anchor element for this
24
+ to walk. See the "Sibling overlays" and first two "Nested overlay"
25
+ cases in `tests/nested-overlays.test.tsx`.
26
+ - A `DropdownMenu` (or `Select`/`Combobox`/`ColorPicker`) nested
27
+ inside an open `Popover` rendered BEHIND it: each component's CSS
28
+ sets a fixed z-index tier (`Popover` 1060, `DropdownMenu`/`Select`/
29
+ `Combobox` 1050, `Tooltip` 1100, `Toast` 1200, ...), correct for
30
+ independent, non-nested overlays but backwards for this one
31
+ relationship — the nested overlay needs to paint above its own
32
+ container regardless of which tier either belongs to. `ThemedPortal`
33
+ now detects when an overlay's own trigger sits inside another,
34
+ already-open overlay's portaled content, and in that case only,
35
+ assigns it an inline z-index above the shared 1150 base (below
36
+ `Toast`'s 1200, which stays on top of everything as a global
37
+ notification regardless of nesting) via `position: relative` on the
38
+ portal wrapper itself — not the component's own CSS class, so
39
+ ordinary non-nested overlays are completely untouched. Verified safe
40
+ for `Popover`/`DropdownMenu` specifically: both position their
41
+ content with inline `position: fixed` (`useFloatingPosition`), which
42
+ isn't affected by an ancestor's `position: relative` — only
43
+ `transform`/`filter`/etc. would change a fixed descendant's
44
+ containing block. Not verified against every possible pairing (e.g.
45
+ a `position: absolute` `Tooltip` ending up as the nested one) — this
46
+ covers the reported and tested composition, not every combination.
47
+
6
48
  ## [0.1.3]
7
49
 
8
50
  ### Fixed
@@ -1448,6 +1448,12 @@ function withAccentOverride(color, children) {
1448
1448
  var import_react = require("react");
1449
1449
  var import_react_dom = require("react-dom");
1450
1450
  var TRACKED_ATTRIBUTES = ["data-theme", "data-accent"];
1451
+ var portalAnchors = /* @__PURE__ */ new WeakMap();
1452
+ function getPortalAnchor(portalNode) {
1453
+ return portalAnchors.get(portalNode) ?? null;
1454
+ }
1455
+ var nextNestedStackOffset = 0;
1456
+ var NESTED_OVERRIDE_BASE_Z_INDEX = 1150;
1451
1457
  function ThemedPortal({ anchorRef, children, container }) {
1452
1458
  const portalElRef = (0, import_react.useRef)(null);
1453
1459
  if (!portalElRef.current && typeof document !== "undefined") {
@@ -1459,6 +1465,14 @@ function ThemedPortal({ anchorRef, children, container }) {
1459
1465
  const portalEl = portalElRef.current;
1460
1466
  if (!portalEl) return;
1461
1467
  target.appendChild(portalEl);
1468
+ if (anchorRef.current) {
1469
+ portalAnchors.set(portalEl, anchorRef.current);
1470
+ const enclosingPortal = anchorRef.current.closest("[data-nuvox-portal]");
1471
+ if (enclosingPortal) {
1472
+ portalEl.style.position = "relative";
1473
+ portalEl.style.zIndex = String(NESTED_OVERRIDE_BASE_Z_INDEX + nextNestedStackOffset++);
1474
+ }
1475
+ }
1462
1476
  function syncAttribute(attr) {
1463
1477
  const anchor2 = anchorRef.current;
1464
1478
  if (!anchor2 || !portalEl) return;
@@ -16529,6 +16543,19 @@ function getOpenIntent(key) {
16529
16543
 
16530
16544
  // src/theme-engine/useClickOutside.ts
16531
16545
  var import_react25 = require("react");
16546
+ function isNestedWithinRefs(target, refs) {
16547
+ if (!(target instanceof Element)) return false;
16548
+ let current = target;
16549
+ for (let depth = 0; depth < 20; depth++) {
16550
+ const portalNode = current.closest("[data-nuvox-portal]");
16551
+ if (!portalNode) return false;
16552
+ const anchor = getPortalAnchor(portalNode);
16553
+ if (!anchor) return false;
16554
+ if (refs.some((ref) => ref.current?.contains(anchor))) return true;
16555
+ current = anchor;
16556
+ }
16557
+ return false;
16558
+ }
16532
16559
  function useClickOutside(isOpen, refs, onOutsideClick) {
16533
16560
  (0, import_react25.useEffect)(() => {
16534
16561
  if (!isOpen) return;
@@ -16536,7 +16563,7 @@ function useClickOutside(isOpen, refs, onOutsideClick) {
16536
16563
  const target = e.target;
16537
16564
  const isInside = refs.some((ref) => ref.current?.contains(target));
16538
16565
  if (isInside) return;
16539
- if (target instanceof Element && target.closest("[data-nuvox-portal]")) return;
16566
+ if (isNestedWithinRefs(target, refs)) return;
16540
16567
  onOutsideClick();
16541
16568
  }
16542
16569
  document.addEventListener("pointerdown", onPointerDown, true);