@tensor_1/react 2.26.1 → 2.26.3

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.
@@ -2,43 +2,174 @@
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
3
3
  import { computePosition } from "../overlay-position.js";
4
4
  const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
5
- const resolve = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
5
+ const resolveAnchor = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
6
+ let anchoringSupported;
7
+ function supportsAnchoring() {
8
+ if (anchoringSupported === void 0) {
9
+ try {
10
+ const css = typeof CSS !== "undefined" ? CSS : void 0;
11
+ anchoringSupported = !!css && typeof css.supports === "function" && css.supports("anchor-name", "--uix-a") && css.supports("position-anchor", "--uix-a") && css.supports("top", "round(anchor(bottom) + 1px, 1px)");
12
+ } catch {
13
+ anchoringSupported = false;
14
+ }
15
+ }
16
+ return anchoringSupported;
17
+ }
18
+ let anchorCount = 0;
19
+ const namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
20
+ function addAnchorName(el, name) {
21
+ const names = namesOf(getComputedStyle(el).getPropertyValue("anchor-name"));
22
+ if (!names.includes(name)) names.push(name);
23
+ el.style.setProperty("anchor-name", names.join(", "));
24
+ }
25
+ function removeAnchorName(el, name) {
26
+ const names = namesOf(el.style.getPropertyValue("anchor-name")).filter((n) => n !== name);
27
+ if (names.length) el.style.setProperty("anchor-name", names.join(", "));
28
+ else el.style.removeProperty("anchor-name");
29
+ }
30
+ function translationOf(el) {
31
+ const t = getComputedStyle(el).transform;
32
+ if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
33
+ try {
34
+ const m = new DOMMatrixReadOnly(t);
35
+ return { x: m.m41, y: m.m42 };
36
+ } catch {
37
+ return { x: 0, y: 0 };
38
+ }
39
+ }
40
+ const signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
41
+ const prepared = /* @__PURE__ */ new WeakSet();
6
42
  function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
7
43
  const opts = useRef({ placement, offset, padding });
8
44
  opts.current = { placement, offset, padding };
45
+ const session = useRef(false);
46
+ const side = useRef(null);
47
+ const written = useRef("");
48
+ const anchoring = useRef(null);
49
+ const anchoringFailed = useRef(false);
50
+ const anchorName = useRef("");
51
+ const release = useCallback((keepPlace) => {
52
+ const current = anchoring.current;
53
+ if (!current) return;
54
+ anchoring.current = null;
55
+ written.current = "";
56
+ const floating = floatingRef.current;
57
+ if (floating) {
58
+ const s = floating.style;
59
+ if (keepPlace && current.anchor.isConnected) {
60
+ const a = current.anchor.getBoundingClientRect();
61
+ s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
62
+ s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
63
+ }
64
+ s.removeProperty("position-anchor");
65
+ }
66
+ removeAnchorName(current.anchor, current.name);
67
+ }, [floatingRef]);
9
68
  const reposition = useCallback(() => {
10
- const anchorEl = resolve(anchor);
69
+ const anchorEl = resolveAnchor(anchor);
11
70
  const floating = floatingRef.current;
12
71
  if (!anchorEl || !floating) return;
13
72
  const s = floating.style;
14
- s.position = "fixed";
15
- s.inset = "auto";
16
- s.margin = "0";
73
+ if (!prepared.has(floating)) {
74
+ s.position = "fixed";
75
+ s.inset = "auto";
76
+ s.margin = "0";
77
+ prepared.add(floating);
78
+ written.current = "";
79
+ }
17
80
  const a = anchorEl.getBoundingClientRect();
81
+ const size = { width: floating.offsetWidth, height: floating.offsetHeight };
18
82
  const { placement: p, offset: o, padding: pad } = opts.current;
19
83
  const pos = computePosition(
20
84
  { x: a.x, y: a.y, width: a.width, height: a.height },
21
- { width: floating.offsetWidth, height: floating.offsetHeight },
85
+ size,
22
86
  { width: window.innerWidth, height: window.innerHeight },
23
- { placement: p, offset: o, padding: pad }
87
+ { placement: p, offset: o, padding: pad, stickySide: side.current ?? void 0 }
24
88
  );
25
- s.left = `${Math.round(pos.x)}px`;
26
- s.top = `${Math.round(pos.y)}px`;
27
- floating.dataset.placement = pos.placement;
28
- }, [anchor, floatingRef]);
89
+ const measured = size.width > 0 || size.height > 0;
90
+ if (measured && session.current) side.current = pos.side;
91
+ if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
92
+ const writeFixed = (at) => {
93
+ const left2 = `${Math.round(at.x)}px`;
94
+ const top2 = `${Math.round(at.y)}px`;
95
+ const key2 = `fixed ${left2} ${top2}`;
96
+ if (key2 === written.current) return;
97
+ s.left = left2;
98
+ s.top = top2;
99
+ written.current = key2;
100
+ };
101
+ if (anchoringFailed.current || !supportsAnchoring()) {
102
+ writeFixed(pos);
103
+ return;
104
+ }
105
+ if (anchoring.current && anchoring.current.anchor !== anchorEl) release(false);
106
+ if (!anchoring.current) {
107
+ anchorName.current || (anchorName.current = `--uix-anchor-${++anchorCount}`);
108
+ addAnchorName(anchorEl, anchorName.current);
109
+ s.setProperty("position-anchor", anchorName.current);
110
+ anchoring.current = { anchor: anchorEl, name: anchorName.current, yEdge: "bottom", xEdge: "left", dy: 0, dx: 0, verified: false };
111
+ written.current = "";
112
+ }
113
+ const current = anchoring.current;
114
+ current.yEdge = pos.side === "bottom" ? "bottom" : "top";
115
+ current.xEdge = pos.side === "right" ? "right" : "left";
116
+ current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
117
+ current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
118
+ const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
119
+ const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
120
+ const key = `anchored ${top} ${left}`;
121
+ if (key !== written.current) {
122
+ s.top = top;
123
+ s.left = left;
124
+ written.current = key;
125
+ }
126
+ if (!current.verified && measured) {
127
+ current.verified = true;
128
+ const box = floating.getBoundingClientRect();
129
+ const t = translationOf(floating);
130
+ const dxc = box.left + box.width / 2 - t.x - (pos.x + size.width / 2);
131
+ const dyc = box.top + box.height / 2 - t.y - (pos.y + size.height / 2);
132
+ if (Math.abs(dxc) > 2 || Math.abs(dyc) > 2) {
133
+ anchoringFailed.current = true;
134
+ release(false);
135
+ writeFixed(pos);
136
+ }
137
+ }
138
+ }, [anchor, floatingRef, release]);
29
139
  useIsomorphicLayoutEffect(() => {
30
140
  if (!open) return;
141
+ session.current = true;
31
142
  reposition();
32
- const onScroll = () => reposition();
143
+ const anchorEl = resolveAnchor(anchor);
144
+ const floating = floatingRef.current;
145
+ const onScroll = (event) => {
146
+ const target = event.target;
147
+ if (target && typeof target.contains === "function") {
148
+ if (floating?.contains(target)) return;
149
+ if (anchorEl && !target.contains(anchorEl)) return;
150
+ }
151
+ reposition();
152
+ };
153
+ const onResize = () => reposition();
33
154
  window.addEventListener("scroll", onScroll, { passive: true, capture: true });
34
- window.addEventListener("resize", onScroll);
155
+ window.addEventListener("resize", onResize);
156
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(() => reposition()) : null;
157
+ if (floating) observer?.observe(floating);
158
+ if (anchorEl) observer?.observe(anchorEl);
35
159
  return () => {
36
160
  window.removeEventListener("scroll", onScroll, true);
37
- window.removeEventListener("resize", onScroll);
161
+ window.removeEventListener("resize", onResize);
162
+ observer?.disconnect();
163
+ session.current = false;
164
+ side.current = null;
165
+ anchoringFailed.current = false;
166
+ release(true);
38
167
  };
39
- }, [open, reposition]);
168
+ }, [open, reposition, anchor, floatingRef, release]);
169
+ useEffect(() => () => release(false), [release]);
40
170
  return reposition;
41
171
  }
42
172
  export {
173
+ resolveAnchor,
43
174
  useAnchoredPosition
44
175
  };
@@ -44,6 +44,7 @@ function useDialog(open, onClose) {
44
44
  lockBodyScroll();
45
45
  let locked = true;
46
46
  const handleClose = () => {
47
+ if (el.open) return;
47
48
  if (locked) {
48
49
  locked = false;
49
50
  unlockBodyScroll();
package/dist/index.cjs CHANGED
@@ -294,6 +294,7 @@ function useDialog(open, onClose) {
294
294
  lockBodyScroll();
295
295
  let locked = true;
296
296
  const handleClose = () => {
297
+ if (el.open) return;
297
298
  if (locked) {
298
299
  locked = false;
299
300
  unlockBodyScroll();
@@ -703,10 +704,15 @@ function fitsOn(side, a, f, vp, offset, padding) {
703
704
  return spaceOn(side, a, vp, offset) >= need + padding;
704
705
  }
705
706
  function computePosition(anchor, floating, viewport, options = {}) {
706
- const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true } = options;
707
+ const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true, stickySide } = options;
707
708
  const { side: preferred, align } = parsePlacement(placement);
708
709
  let side = preferred;
709
- if (flip && !fitsOn(side, anchor, floating, viewport, offset, padding)) {
710
+ const sticky = stickySide === preferred || stickySide === OPPOSITE[preferred] ? stickySide : void 0;
711
+ if (flip && sticky) {
712
+ side = sticky;
713
+ const opp = OPPOSITE[side];
714
+ if (!fitsOn(side, anchor, floating, viewport, offset, padding) && fitsOn(opp, anchor, floating, viewport, offset, padding)) side = opp;
715
+ } else if (flip && !fitsOn(side, anchor, floating, viewport, offset, padding)) {
710
716
  const opp = OPPOSITE[side];
711
717
  if (fitsOn(opp, anchor, floating, viewport, offset, padding) || spaceOn(opp, anchor, viewport, offset) > spaceOn(side, anchor, viewport, offset)) {
712
718
  side = opp;
@@ -742,41 +748,171 @@ function computePosition(anchor, floating, viewport, options = {}) {
742
748
 
743
749
  // src/hooks/useAnchoredPosition.ts
744
750
  var useIsomorphicLayoutEffect = typeof document !== "undefined" ? import_react4.useLayoutEffect : import_react4.useEffect;
745
- var resolve = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
751
+ var resolveAnchor = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
752
+ var anchoringSupported;
753
+ function supportsAnchoring() {
754
+ if (anchoringSupported === void 0) {
755
+ try {
756
+ const css = typeof CSS !== "undefined" ? CSS : void 0;
757
+ anchoringSupported = !!css && typeof css.supports === "function" && css.supports("anchor-name", "--uix-a") && css.supports("position-anchor", "--uix-a") && css.supports("top", "round(anchor(bottom) + 1px, 1px)");
758
+ } catch {
759
+ anchoringSupported = false;
760
+ }
761
+ }
762
+ return anchoringSupported;
763
+ }
764
+ var anchorCount = 0;
765
+ var namesOf = (value) => value.split(",").map((n) => n.trim()).filter((n) => n.startsWith("--"));
766
+ function addAnchorName(el, name) {
767
+ const names = namesOf(getComputedStyle(el).getPropertyValue("anchor-name"));
768
+ if (!names.includes(name)) names.push(name);
769
+ el.style.setProperty("anchor-name", names.join(", "));
770
+ }
771
+ function removeAnchorName(el, name) {
772
+ const names = namesOf(el.style.getPropertyValue("anchor-name")).filter((n) => n !== name);
773
+ if (names.length) el.style.setProperty("anchor-name", names.join(", "));
774
+ else el.style.removeProperty("anchor-name");
775
+ }
776
+ function translationOf(el) {
777
+ const t = getComputedStyle(el).transform;
778
+ if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
779
+ try {
780
+ const m = new DOMMatrixReadOnly(t);
781
+ return { x: m.m41, y: m.m42 };
782
+ } catch {
783
+ return { x: 0, y: 0 };
784
+ }
785
+ }
786
+ var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
787
+ var prepared = /* @__PURE__ */ new WeakSet();
746
788
  function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
747
789
  const opts = (0, import_react4.useRef)({ placement, offset, padding });
748
790
  opts.current = { placement, offset, padding };
791
+ const session = (0, import_react4.useRef)(false);
792
+ const side = (0, import_react4.useRef)(null);
793
+ const written = (0, import_react4.useRef)("");
794
+ const anchoring = (0, import_react4.useRef)(null);
795
+ const anchoringFailed = (0, import_react4.useRef)(false);
796
+ const anchorName = (0, import_react4.useRef)("");
797
+ const release = (0, import_react4.useCallback)((keepPlace) => {
798
+ const current = anchoring.current;
799
+ if (!current) return;
800
+ anchoring.current = null;
801
+ written.current = "";
802
+ const floating = floatingRef.current;
803
+ if (floating) {
804
+ const s = floating.style;
805
+ if (keepPlace && current.anchor.isConnected) {
806
+ const a = current.anchor.getBoundingClientRect();
807
+ s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
808
+ s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
809
+ }
810
+ s.removeProperty("position-anchor");
811
+ }
812
+ removeAnchorName(current.anchor, current.name);
813
+ }, [floatingRef]);
749
814
  const reposition = (0, import_react4.useCallback)(() => {
750
- const anchorEl = resolve(anchor);
815
+ const anchorEl = resolveAnchor(anchor);
751
816
  const floating = floatingRef.current;
752
817
  if (!anchorEl || !floating) return;
753
818
  const s = floating.style;
754
- s.position = "fixed";
755
- s.inset = "auto";
756
- s.margin = "0";
819
+ if (!prepared.has(floating)) {
820
+ s.position = "fixed";
821
+ s.inset = "auto";
822
+ s.margin = "0";
823
+ prepared.add(floating);
824
+ written.current = "";
825
+ }
757
826
  const a = anchorEl.getBoundingClientRect();
827
+ const size = { width: floating.offsetWidth, height: floating.offsetHeight };
758
828
  const { placement: p, offset: o, padding: pad } = opts.current;
759
829
  const pos = computePosition(
760
830
  { x: a.x, y: a.y, width: a.width, height: a.height },
761
- { width: floating.offsetWidth, height: floating.offsetHeight },
831
+ size,
762
832
  { width: window.innerWidth, height: window.innerHeight },
763
- { placement: p, offset: o, padding: pad }
833
+ { placement: p, offset: o, padding: pad, stickySide: side.current ?? void 0 }
764
834
  );
765
- s.left = `${Math.round(pos.x)}px`;
766
- s.top = `${Math.round(pos.y)}px`;
767
- floating.dataset.placement = pos.placement;
768
- }, [anchor, floatingRef]);
835
+ const measured = size.width > 0 || size.height > 0;
836
+ if (measured && session.current) side.current = pos.side;
837
+ if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
838
+ const writeFixed = (at) => {
839
+ const left2 = `${Math.round(at.x)}px`;
840
+ const top2 = `${Math.round(at.y)}px`;
841
+ const key2 = `fixed ${left2} ${top2}`;
842
+ if (key2 === written.current) return;
843
+ s.left = left2;
844
+ s.top = top2;
845
+ written.current = key2;
846
+ };
847
+ if (anchoringFailed.current || !supportsAnchoring()) {
848
+ writeFixed(pos);
849
+ return;
850
+ }
851
+ if (anchoring.current && anchoring.current.anchor !== anchorEl) release(false);
852
+ if (!anchoring.current) {
853
+ anchorName.current || (anchorName.current = `--uix-anchor-${++anchorCount}`);
854
+ addAnchorName(anchorEl, anchorName.current);
855
+ s.setProperty("position-anchor", anchorName.current);
856
+ anchoring.current = { anchor: anchorEl, name: anchorName.current, yEdge: "bottom", xEdge: "left", dy: 0, dx: 0, verified: false };
857
+ written.current = "";
858
+ }
859
+ const current = anchoring.current;
860
+ current.yEdge = pos.side === "bottom" ? "bottom" : "top";
861
+ current.xEdge = pos.side === "right" ? "right" : "left";
862
+ current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
863
+ current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
864
+ const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
865
+ const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
866
+ const key = `anchored ${top} ${left}`;
867
+ if (key !== written.current) {
868
+ s.top = top;
869
+ s.left = left;
870
+ written.current = key;
871
+ }
872
+ if (!current.verified && measured) {
873
+ current.verified = true;
874
+ const box = floating.getBoundingClientRect();
875
+ const t = translationOf(floating);
876
+ const dxc = box.left + box.width / 2 - t.x - (pos.x + size.width / 2);
877
+ const dyc = box.top + box.height / 2 - t.y - (pos.y + size.height / 2);
878
+ if (Math.abs(dxc) > 2 || Math.abs(dyc) > 2) {
879
+ anchoringFailed.current = true;
880
+ release(false);
881
+ writeFixed(pos);
882
+ }
883
+ }
884
+ }, [anchor, floatingRef, release]);
769
885
  useIsomorphicLayoutEffect(() => {
770
886
  if (!open) return;
887
+ session.current = true;
771
888
  reposition();
772
- const onScroll = () => reposition();
889
+ const anchorEl = resolveAnchor(anchor);
890
+ const floating = floatingRef.current;
891
+ const onScroll = (event) => {
892
+ const target = event.target;
893
+ if (target && typeof target.contains === "function") {
894
+ if (floating?.contains(target)) return;
895
+ if (anchorEl && !target.contains(anchorEl)) return;
896
+ }
897
+ reposition();
898
+ };
899
+ const onResize = () => reposition();
773
900
  window.addEventListener("scroll", onScroll, { passive: true, capture: true });
774
- window.addEventListener("resize", onScroll);
901
+ window.addEventListener("resize", onResize);
902
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(() => reposition()) : null;
903
+ if (floating) observer?.observe(floating);
904
+ if (anchorEl) observer?.observe(anchorEl);
775
905
  return () => {
776
906
  window.removeEventListener("scroll", onScroll, true);
777
- window.removeEventListener("resize", onScroll);
907
+ window.removeEventListener("resize", onResize);
908
+ observer?.disconnect();
909
+ session.current = false;
910
+ side.current = null;
911
+ anchoringFailed.current = false;
912
+ release(true);
778
913
  };
779
- }, [open, reposition]);
914
+ }, [open, reposition, anchor, floatingRef, release]);
915
+ (0, import_react4.useEffect)(() => () => release(false), [release]);
780
916
  return reposition;
781
917
  }
782
918
 
@@ -1489,17 +1625,36 @@ var helpLabelFor = (thing) => typeof thing === "string" || typeof thing === "num
1489
1625
  // src/components/Popover.tsx
1490
1626
  var import_react14 = require("react");
1491
1627
  var import_jsx_runtime11 = require("react/jsx-runtime");
1628
+ var useIsomorphicLayoutEffect3 = typeof document !== "undefined" ? import_react14.useLayoutEffect : import_react14.useEffect;
1629
+ var PLACING = "data-uix-placing";
1630
+ var isPopoverOpen = (el) => {
1631
+ try {
1632
+ return el.matches(":popover-open");
1633
+ } catch {
1634
+ return false;
1635
+ }
1636
+ };
1637
+ var settle = (el) => {
1638
+ if (!el.hasAttribute(PLACING)) return;
1639
+ getComputedStyle(el).getPropertyValue("transform");
1640
+ el.removeAttribute(PLACING);
1641
+ };
1492
1642
  function Popover({
1493
1643
  popover = "auto",
1494
1644
  anchor,
1495
1645
  placement = "bottom-start",
1496
1646
  offset = 6,
1647
+ closeWhenAnchorHidden = false,
1648
+ onAnchorHidden,
1497
1649
  className,
1498
1650
  children,
1499
1651
  ...props
1500
1652
  }) {
1501
1653
  const ref = (0, import_react14.useRef)(null);
1502
1654
  const [open, setOpen] = (0, import_react14.useState)(false);
1655
+ const openRef = (0, import_react14.useRef)(false);
1656
+ const onAnchorHiddenRef = (0, import_react14.useRef)(onAnchorHidden);
1657
+ onAnchorHiddenRef.current = onAnchorHidden;
1503
1658
  const popoverAttr = { popover };
1504
1659
  const reposition = useAnchoredPosition(anchor ?? null, ref, {
1505
1660
  open: open && !!anchor,
@@ -1509,14 +1664,55 @@ function Popover({
1509
1664
  (0, import_react14.useEffect)(() => {
1510
1665
  const el = ref.current;
1511
1666
  if (!el || !anchor) return;
1667
+ const hold = (event) => {
1668
+ if (event.newState !== "open") return;
1669
+ el.setAttribute(PLACING, "");
1670
+ window.setTimeout(() => settle(el), 150);
1671
+ };
1512
1672
  const sync = () => {
1513
- const isOpen = el.matches(":popover-open");
1514
- if (isOpen) reposition();
1515
- setOpen(isOpen);
1673
+ if (!isPopoverOpen(el)) {
1674
+ el.removeAttribute(PLACING);
1675
+ setOpen(false);
1676
+ return;
1677
+ }
1678
+ reposition();
1679
+ window.setTimeout(() => {
1680
+ if (!isPopoverOpen(el)) return;
1681
+ if (!openRef.current) {
1682
+ setOpen(true);
1683
+ return;
1684
+ }
1685
+ reposition();
1686
+ settle(el);
1687
+ }, 0);
1516
1688
  };
1689
+ el.addEventListener("beforetoggle", hold);
1517
1690
  el.addEventListener("toggle", sync);
1518
- return () => el.removeEventListener("toggle", sync);
1691
+ return () => {
1692
+ el.removeEventListener("beforetoggle", hold);
1693
+ el.removeEventListener("toggle", sync);
1694
+ el.removeAttribute(PLACING);
1695
+ };
1519
1696
  }, [anchor, reposition]);
1697
+ useIsomorphicLayoutEffect3(() => {
1698
+ openRef.current = open;
1699
+ if (open && ref.current) settle(ref.current);
1700
+ }, [open]);
1701
+ (0, import_react14.useEffect)(() => {
1702
+ const el = ref.current;
1703
+ const anchorEl = resolveAnchor(anchor);
1704
+ if (!closeWhenAnchorHidden || !open || !el || !anchorEl || typeof IntersectionObserver !== "function") return;
1705
+ const observer = new IntersectionObserver((entries) => {
1706
+ const latest = entries[entries.length - 1];
1707
+ if (!latest || latest.isIntersecting || !isPopoverOpen(el)) return;
1708
+ onAnchorHiddenRef.current?.();
1709
+ const active = el.ownerDocument.activeElement;
1710
+ if (active && el.contains(active)) active.blur();
1711
+ el.hidePopover();
1712
+ });
1713
+ observer.observe(anchorEl);
1714
+ return () => observer.disconnect();
1715
+ }, [closeWhenAnchorHidden, open, anchor]);
1520
1716
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
1521
1717
  }
1522
1718
 
@@ -3614,7 +3810,7 @@ var VIEW_MENU_COLUMN_LABELS = {
3614
3810
  };
3615
3811
  var fill = (template, values) => template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
3616
3812
  var columnText = (column) => column.textLabel ?? (typeof column.label === "string" ? column.label : column.id);
3617
- var isPopoverOpen = (el) => {
3813
+ var isPopoverOpen2 = (el) => {
3618
3814
  try {
3619
3815
  return !!el?.matches(":popover-open");
3620
3816
  } catch {
@@ -3630,14 +3826,14 @@ function ColumnRowMenu({ label, actions, triggerRef }) {
3630
3826
  const enabled = () => items.current.filter((el) => !!el && !el.disabled);
3631
3827
  const close = (returnFocus) => {
3632
3828
  const el = document.getElementById(menuId);
3633
- if (isPopoverOpen(el)) el.hidePopover();
3829
+ if (isPopoverOpen2(el)) el.hidePopover();
3634
3830
  setOpen(false);
3635
3831
  if (returnFocus) button.current?.focus();
3636
3832
  };
3637
3833
  (0, import_react36.useEffect)(() => {
3638
3834
  if (!open) return;
3639
3835
  const el = document.getElementById(menuId);
3640
- if (el && typeof el.showPopover === "function" && !isPopoverOpen(el)) {
3836
+ if (el && typeof el.showPopover === "function" && !isPopoverOpen2(el)) {
3641
3837
  try {
3642
3838
  el.showPopover();
3643
3839
  } catch {
@@ -3688,7 +3884,7 @@ function ColumnRowMenu({ label, actions, triggerRef }) {
3688
3884
  "aria-expanded": open,
3689
3885
  "aria-controls": open ? menuId : void 0,
3690
3886
  onPointerDown: () => {
3691
- wasOpenAtPress.current = isPopoverOpen(document.getElementById(menuId));
3887
+ wasOpenAtPress.current = isPopoverOpen2(document.getElementById(menuId));
3692
3888
  },
3693
3889
  onClick: () => {
3694
3890
  const wasOpen = wasOpenAtPress.current;
@@ -7660,7 +7856,7 @@ function DiffViewer({
7660
7856
  const [internalResolutions, setInternalResolutions] = (0, import_react51.useState)({});
7661
7857
  const resolutions = controlledResolutions ?? internalResolutions;
7662
7858
  const summary = summarizeDiff(entries, resolutions);
7663
- const resolve2 = (path, resolution) => {
7859
+ const resolve = (path, resolution) => {
7664
7860
  if (controlledResolutions === void 0) setInternalResolutions((currentState) => ({ ...currentState, [path]: resolution }));
7665
7861
  onResolutionChange?.(path, resolution);
7666
7862
  };
@@ -7707,9 +7903,9 @@ function DiffViewer({
7707
7903
  /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("pre", { children: renderValue(entry[version], labels.notPresent) })
7708
7904
  ] }, version)) }),
7709
7905
  /* @__PURE__ */ (0, import_jsx_runtime75.jsxs)("div", { className: "uix-diff-viewer__actions", role: "group", "aria-label": fillLabel(labels.resolve, { path: entry.path }), children: [
7710
- /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--primary", buttonSize), "aria-label": fillLabel(labels.acceptIncomingFor, { path: entry.path }), "aria-pressed": resolution === "accept", onClick: () => resolve2(entry.path, "accept"), children: labels.acceptIncoming }),
7711
- /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--secondary", buttonSize), "aria-label": fillLabel(labels.keepCurrentFor, { path: entry.path }), "aria-pressed": resolution === "skip", onClick: () => resolve2(entry.path, "skip"), children: labels.keepCurrent }),
7712
- /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--ghost", buttonSize), "aria-label": fillLabel(labels.markPendingFor, { path: entry.path }), "aria-pressed": resolution === "pending", onClick: () => resolve2(entry.path, "pending"), children: labels.markPending })
7906
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--primary", buttonSize), "aria-label": fillLabel(labels.acceptIncomingFor, { path: entry.path }), "aria-pressed": resolution === "accept", onClick: () => resolve(entry.path, "accept"), children: labels.acceptIncoming }),
7907
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--secondary", buttonSize), "aria-label": fillLabel(labels.keepCurrentFor, { path: entry.path }), "aria-pressed": resolution === "skip", onClick: () => resolve(entry.path, "skip"), children: labels.keepCurrent }),
7908
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("button", { type: "button", className: cx("uix-btn uix-btn--ghost", buttonSize), "aria-label": fillLabel(labels.markPendingFor, { path: entry.path }), "aria-pressed": resolution === "pending", onClick: () => resolve(entry.path, "pending"), children: labels.markPending })
7713
7909
  ] })
7714
7910
  ] }, entry.path);
7715
7911
  })
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject, ButtonHTMLAttributes, ReactNode, InputHTMLAttributes, AriaAttributes, HTMLAttributes, Ref, TextareaHTMLAttributes, SelectHTMLAttributes, AnchorHTMLAttributes, MouseEvent, DetailsHTMLAttributes, TdHTMLAttributes, TableHTMLAttributes, ThHTMLAttributes } from 'react';
3
- import { P as Placement } from './overlay-position-CEGSz6Z2.cjs';
4
- export { A as Align, a as PositionOptions, b as PositionResult, R as Rect, S as Side, c as Size, d as computePosition } from './overlay-position-CEGSz6Z2.cjs';
3
+ import { P as Placement } from './overlay-position-Cjv5E7mn.cjs';
4
+ export { A as Align, a as PositionOptions, b as PositionResult, R as Rect, S as Side, c as Size, d as computePosition } from './overlay-position-Cjv5E7mn.cjs';
5
5
 
6
6
  declare function cx(...args: (string | false | null | undefined | 0)[]): string;
7
7
 
@@ -182,10 +182,15 @@ interface UseAnchoredPositionOptions {
182
182
  * Position a floating element (a native-popover overlay) against an anchor with
183
183
  * cross-browser flip/shift — the DOM half of the overlay-position engine (UIX-FIX-02).
184
184
  *
185
- * Measures both elements with getBoundingClientRect, applies `position: fixed` + left/top
186
- * (overriding the popover's default centered `inset:0; margin:auto`), and keeps the overlay
187
- * glued to its anchor on scroll/resize while `open`. Returns a `reposition` fn for callers
188
- * that want to nudge it manually (e.g. right when a native popover's `toggle` fires).
185
+ * The engine decides the side, alignment and viewport clamp from getBoundingClientRect.
186
+ * Where the browser supports CSS anchor positioning, that result is written as an offset
187
+ * from the anchor (`position-anchor` + `anchor()`), so the browser keeps the overlay glued
188
+ * to its anchor while the page scrolls, with no frame of lag; elsewhere it is written as
189
+ * `position: fixed` + left/top and follows scroll/resize from JS. While `open`, the side
190
+ * chosen at open is kept until it no longer fits and the other side does, scrolls inside
191
+ * the overlay itself are ignored, and a size change of either element re-places it.
192
+ * Returns a `reposition` fn for callers that want to nudge it manually (e.g. right when a
193
+ * native popover's `toggle` fires).
189
194
  */
190
195
  declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding }: UseAnchoredPositionOptions): () => void;
191
196
 
@@ -1643,16 +1648,26 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
1643
1648
  popover?: 'auto' | 'manual';
1644
1649
  /**
1645
1650
  * Element (or ref) to anchor against. When set, the popover is placed with
1646
- * cross-browser JS positioning — flip when it won't fit, shift to stay on-screen —
1647
- * instead of relying on CSS `anchor()`, which is Chromium-only and detaches the
1648
- * popover from its trigger everywhere else. The native Popover API still provides
1649
- * the top layer (so it escapes `overflow` clipping) and light-dismiss.
1651
+ * cross-browser JS positioning — flip when it won't fit, shift to stay on-screen.
1652
+ * Where the browser supports CSS anchor positioning, the placement is written as an
1653
+ * offset from the anchor, so it stays attached while the page scrolls; elsewhere it
1654
+ * follows scroll from JS. The native Popover API still provides the top layer (so it
1655
+ * escapes `overflow` clipping) and light-dismiss.
1650
1656
  */
1651
1657
  anchor?: RefObject<HTMLElement | null> | HTMLElement | null;
1652
1658
  /** Preferred placement when `anchor` is set. Default `'bottom-start'`. */
1653
1659
  placement?: Placement;
1654
1660
  /** Gap between the trigger and the popover, in px. Default 6. */
1655
1661
  offset?: number;
1662
+ /**
1663
+ * Close the popover once its `anchor` has scrolled completely out of view (outside the
1664
+ * viewport or a clipping ancestor), e.g. a picker opened from an editor that the user
1665
+ * scrolls away from. Focus inside the popover is let go rather than sent back to the
1666
+ * hidden anchor. Needs `anchor`. Default `false`.
1667
+ */
1668
+ closeWhenAnchorHidden?: boolean;
1669
+ /** Called when `closeWhenAnchorHidden` is about to close the popover. */
1670
+ onAnchorHidden?: () => void;
1656
1671
  children?: ReactNode;
1657
1672
  }
1658
1673
  /**
@@ -1660,7 +1675,7 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
1660
1675
  * things like a filter popover, menu, or rich select by putting controls inside.
1661
1676
  * Pass `anchor` to get collision-aware, cross-browser placement (UIX-FIX-02).
1662
1677
  */
1663
- declare function Popover({ popover, anchor, placement, offset, className, children, ...props }: PopoverProps): react.JSX.Element;
1678
+ declare function Popover({ popover, anchor, placement, offset, closeWhenAnchorHidden, onAnchorHidden, className, children, ...props }: PopoverProps): react.JSX.Element;
1664
1679
 
1665
1680
  interface CommandPaletteProps extends HTMLAttributes<HTMLDivElement> {
1666
1681
  /** TENSOR RX-125 (UIX-04): translatable; English default. */
@@ -39,6 +39,13 @@ interface PositionOptions {
39
39
  flip?: boolean;
40
40
  /** Slide along the cross axis to stay on-screen. Default true. */
41
41
  shift?: boolean;
42
+ /**
43
+ * The side the element is already on, while it stays open. It is kept unless it no longer
44
+ * fits AND the opposite side does, so a size change or a small scroll never flips an open
45
+ * overlay back and forth. Ignored when it is not on `placement`'s axis. Unset = decide from
46
+ * `placement` alone (an overlay that is opening).
47
+ */
48
+ stickySide?: Side;
42
49
  }
43
50
  interface PositionResult {
44
51
  /** Viewport-space left, for `position: fixed`. */