@dreamtree-org/twreact-ui 1.0.69 → 1.0.70

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/dist/index.js CHANGED
@@ -4901,8 +4901,6 @@ var Table = function Table(_ref) {
4901
4901
  }
4902
4902
  }, "Next")))));
4903
4903
  };
4904
-
4905
- /* ---------------- ActionMenuPortal (updated for mobile) ---------------- */
4906
4904
  function ActionMenuPortal(_ref4) {
4907
4905
  var anchorElem = _ref4.anchorElem,
4908
4906
  anchorRow = _ref4.anchorRow,
@@ -4911,10 +4909,7 @@ function ActionMenuPortal(_ref4) {
4911
4909
  _ref4.onClose;
4912
4910
  var onAction = _ref4.onAction,
4913
4911
  menuRef = _ref4.menuRef;
4914
- var _useState29 = React.useState(0),
4915
- _useState30 = _slicedToArray(_useState29, 2),
4916
- setTick = _useState30[1];
4917
- var _useState31 = React.useState({
4912
+ var _useState29 = React.useState({
4918
4913
  left: 0,
4919
4914
  top: 0,
4920
4915
  transformOrigin: "top right",
@@ -4922,11 +4917,15 @@ function ActionMenuPortal(_ref4) {
4922
4917
  width: 180,
4923
4918
  opacity: 0
4924
4919
  }),
4925
- _useState32 = _slicedToArray(_useState31, 2),
4926
- style = _useState32[0],
4927
- setStyle = _useState32[1];
4920
+ _useState30 = _slicedToArray(_useState29, 2),
4921
+ style = _useState30[0],
4922
+ setStyle = _useState30[1];
4928
4923
  var menuWidth = 180;
4929
4924
  var maxMenuHeight = 320;
4925
+ var margin = 8;
4926
+ var minMenuHeight = 80;
4927
+
4928
+ // compute position using actual menu height (if available) and layout measurements
4930
4929
  var computePosition = function computePosition() {
4931
4930
  if (!anchorElem) return;
4932
4931
  var rect = anchorElem.getBoundingClientRect();
@@ -4934,52 +4933,95 @@ function ActionMenuPortal(_ref4) {
4934
4933
  var scrollX = window.scrollX || window.pageXOffset;
4935
4934
  var spaceBelow = window.innerHeight - rect.bottom;
4936
4935
  var spaceAbove = rect.top;
4937
- var top;
4936
+
4937
+ // allowed max height based on available space
4938
+ var allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, Math.max(spaceBelow - margin, spaceAbove - margin)));
4939
+
4940
+ // choose placement by comparing actual available spaces
4938
4941
  var placement = "bottom";
4939
- var maxHeight = Math.min(maxMenuHeight, Math.max(80, spaceBelow - 16));
4940
4942
  if (spaceBelow < 160 && spaceAbove > spaceBelow) {
4941
4943
  placement = "top";
4942
- maxHeight = Math.min(maxMenuHeight, Math.max(80, spaceAbove - 16));
4943
- top = rect.top + scrollY - maxHeight - 8;
4944
+ // when placing on top we should cap allowedMaxHeight by spaceAbove
4945
+ allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, spaceAbove - margin));
4944
4946
  } else {
4945
- top = rect.bottom + scrollY + 8;
4947
+ // placing bottom: cap by spaceBelow
4948
+ allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, spaceBelow - margin));
4946
4949
  }
4950
+
4951
+ // measure the menu's content height if we can, and compute the actual menu height we'll use.
4952
+ var measuredMenuHeight = allowedMaxHeight;
4953
+ var menuEl = menuRef === null || menuRef === void 0 ? void 0 : menuRef.current;
4954
+ if (menuEl) {
4955
+ // scrollHeight is the content height; offsetHeight may reflect current rendered height.
4956
+ var contentHeight = menuEl.scrollHeight || menuEl.offsetHeight || allowedMaxHeight;
4957
+ // actual height will be min(contentHeight, allowedMaxHeight)
4958
+ measuredMenuHeight = Math.min(contentHeight, allowedMaxHeight);
4959
+ }
4960
+
4961
+ // compute top according to placement and measuredMenuHeight
4962
+ var top;
4963
+ if (placement === "top") {
4964
+ // place menu so its bottom sits just above the anchor (rect.top - margin)
4965
+ top = rect.top + scrollY - measuredMenuHeight - margin;
4966
+ } else {
4967
+ // place menu just below the anchor (rect.bottom + margin)
4968
+ top = rect.bottom + scrollY + margin;
4969
+ }
4970
+
4971
+ // clamp top to viewport (so it never goes off-screen)
4972
+ var minTop = margin + scrollY;
4973
+ var maxTop = window.innerHeight + scrollY - measuredMenuHeight - margin;
4974
+ if (top < minTop) top = minTop;
4975
+ if (top > maxTop) top = maxTop;
4976
+
4977
+ // compute left and clamp horizontally
4947
4978
  var left = rect.right + scrollX - menuWidth;
4948
- if (left < 8) left = 8;
4949
- if (left + menuWidth > window.innerWidth - 8) left = window.innerWidth - menuWidth - 8;
4979
+ if (left < margin) left = margin;
4980
+ if (left + menuWidth > window.innerWidth - margin) {
4981
+ left = Math.max(margin, window.innerWidth - menuWidth - margin);
4982
+ }
4950
4983
  var transformOrigin = placement === "bottom" ? "top right" : "bottom right";
4984
+
4985
+ // set style (opacity 1 to fade-in)
4951
4986
  setStyle({
4952
4987
  left: left,
4953
4988
  top: top,
4954
4989
  transformOrigin: transformOrigin,
4955
- maxHeight: maxHeight,
4990
+ maxHeight: allowedMaxHeight,
4956
4991
  width: menuWidth,
4957
4992
  opacity: 1
4958
4993
  });
4959
4994
  };
4960
- React.useEffect(function () {
4995
+
4996
+ // useLayoutEffect so the position is measured & applied before paint
4997
+ React.useLayoutEffect(function () {
4998
+ // compute once after mount
4961
4999
  computePosition();
4962
5000
  var onScrollOrResize = function onScrollOrResize() {
4963
- computePosition();
4964
- setTick(function (t) {
4965
- return t + 1;
5001
+ // using requestAnimationFrame to avoid layout thrash on fast scroll/resize
5002
+ window.requestAnimationFrame(function () {
5003
+ return computePosition();
4966
5004
  });
4967
5005
  };
4968
5006
  window.addEventListener("resize", onScrollOrResize);
5007
+ // capture scrolls (true) so position updates even when scrolling ancestor elements
4969
5008
  window.addEventListener("scroll", onScrollOrResize, true);
4970
5009
  return function () {
4971
5010
  window.removeEventListener("resize", onScrollOrResize);
4972
5011
  window.removeEventListener("scroll", onScrollOrResize, true);
4973
5012
  };
4974
- }, [anchorElem]);
5013
+ // Recompute when anchor element changes or actions change (content height may change).
5014
+ }, [anchorElem, actions, menuRef]);
4975
5015
  return /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/React.createElement("div", {
4976
5016
  ref: menuRef,
4977
5017
  style: {
5018
+ position: "absolute",
4978
5019
  top: style.top,
4979
5020
  left: style.left,
4980
5021
  width: style.width,
4981
5022
  maxHeight: style.maxHeight,
4982
- transformOrigin: style.transformOrigin
5023
+ transformOrigin: style.transformOrigin,
5024
+ opacity: style.opacity
4983
5025
  },
4984
5026
  className: "absolute z-50 bg-white rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 overflow-y-auto transition-opacity duration-150 ease-out",
4985
5027
  onClick: function onClick(e) {
@@ -10370,7 +10412,12 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
10370
10412
  currentMonth = _useState4[0],
10371
10413
  setCurrentMonth = _useState4[1];
10372
10414
  var datePickerRef = React.useRef(null);
10415
+ var popupRef = React.useRef(null);
10373
10416
  var gridButtonRefs = React.useRef([]);
10417
+ var _useState5 = React.useState({}),
10418
+ _useState6 = _slicedToArray(_useState5, 2),
10419
+ popupStyle = _useState6[0],
10420
+ setPopupStyle = _useState6[1];
10374
10421
  React.useImperativeHandle(forwardedRef, function () {
10375
10422
  return datePickerRef.current;
10376
10423
  }, []);
@@ -10383,7 +10430,7 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
10383
10430
  // Close on outside click
10384
10431
  React.useEffect(function () {
10385
10432
  var handleClickOutside = function handleClickOutside(e) {
10386
- if (datePickerRef.current && !datePickerRef.current.contains(e.target)) {
10433
+ if (datePickerRef.current && !datePickerRef.current.contains(e.target) && popupRef.current && !popupRef.current.contains(e.target)) {
10387
10434
  setIsOpen(false);
10388
10435
  }
10389
10436
  };
@@ -10499,6 +10546,75 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
10499
10546
  if (idx >= 0) focusDayAtIndex(idx);
10500
10547
  }, 0);
10501
10548
  };
10549
+
10550
+ // compute popup position so it's aware of viewport edges (top/bottom/left/right)
10551
+ var computePopupPosition = function computePopupPosition() {
10552
+ var margin = 8;
10553
+ var trigger = datePickerRef.current;
10554
+ var popup = popupRef.current;
10555
+ if (!trigger || !popup) return;
10556
+ var triggerRect = trigger.getBoundingClientRect();
10557
+ // measure popup by temporarily making it visible if hidden
10558
+ var popupRect = popup.getBoundingClientRect();
10559
+
10560
+ // If the popup is not yet fully measured (width/height 0), try to read offsetWidth/Height
10561
+ if (popupRect.width === 0 || popupRect.height === 0) {
10562
+ popupRect = {
10563
+ width: popup.offsetWidth || 240,
10564
+ height: popup.offsetHeight || 300
10565
+ };
10566
+ }
10567
+
10568
+ // Decide vertical placement
10569
+ var spaceBelow = window.innerHeight - triggerRect.bottom;
10570
+ var spaceAbove = triggerRect.top;
10571
+ var placeBelow = spaceBelow >= popupRect.height + margin;
10572
+ if (!placeBelow && spaceAbove >= popupRect.height + margin) placeBelow = false;
10573
+ // If neither side has full space, pick the side with more space
10574
+ if (!placeBelow && spaceBelow < popupRect.height + margin && spaceAbove < popupRect.height + margin) {
10575
+ placeBelow = spaceBelow >= spaceAbove;
10576
+ }
10577
+
10578
+ // compute top coordinate (fixed positioning)
10579
+ var top;
10580
+ if (placeBelow) {
10581
+ top = Math.min(window.innerHeight - margin - popupRect.height, triggerRect.bottom + margin);
10582
+ } else {
10583
+ top = Math.max(margin, triggerRect.top - popupRect.height - margin);
10584
+ }
10585
+
10586
+ // Decide horizontal placement (try align left to trigger)
10587
+ var left = triggerRect.left;
10588
+ // If overflowing right, shift left
10589
+ if (left + popupRect.width > window.innerWidth - margin) {
10590
+ left = Math.max(margin, window.innerWidth - margin - popupRect.width);
10591
+ }
10592
+ // If overflowing left, clamp
10593
+ if (left < margin) left = margin;
10594
+ var transformOrigin = placeBelow ? "top left" : "bottom left";
10595
+ setPopupStyle({
10596
+ position: "fixed",
10597
+ top: "".concat(Math.round(top), "px"),
10598
+ left: "".concat(Math.round(left), "px"),
10599
+ zIndex: 9999,
10600
+ transformOrigin: transformOrigin
10601
+ });
10602
+ };
10603
+
10604
+ // recompute on open, on resize/scroll, and when popup content changes (currentMonth)
10605
+ React.useLayoutEffect(function () {
10606
+ if (!isOpen) return;
10607
+ computePopupPosition();
10608
+ var handleResize = function handleResize() {
10609
+ return computePopupPosition();
10610
+ };
10611
+ window.addEventListener("resize", handleResize);
10612
+ window.addEventListener("scroll", handleResize, true); // capture scrolling in ancestors
10613
+ return function () {
10614
+ window.removeEventListener("resize", handleResize);
10615
+ window.removeEventListener("scroll", handleResize, true);
10616
+ };
10617
+ }, [isOpen, currentMonth]);
10502
10618
  var header = /*#__PURE__*/React.createElement("div", {
10503
10619
  className: "mb-4 flex items-center justify-between"
10504
10620
  }, /*#__PURE__*/React.createElement("button", {
@@ -10523,9 +10639,11 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
10523
10639
  className: "h-4 w-4"
10524
10640
  })));
10525
10641
  var calendarPopup = /*#__PURE__*/React.createElement("div", {
10526
- className: "absolute z-50 mt-1 w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
10642
+ ref: popupRef,
10643
+ className: "w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
10527
10644
  role: "dialog",
10528
- "aria-modal": "false"
10645
+ "aria-modal": "false",
10646
+ style: popupStyle
10529
10647
  }, header, /*#__PURE__*/React.createElement("div", {
10530
10648
  className: "mb-2 grid grid-cols-7 gap-1"
10531
10649
  }, Array.from({
@@ -11874,11 +11992,10 @@ function Slider(_ref6) {
11874
11992
  var _excluded$g = ["value", "onChange", "placeholder", "label", "error", "disabled", "required", "minDate", "maxDate", "className", "weekStartsOn", "portal", "displayFormat", "locale", "showClear", "closeOnSelect"];
11875
11993
 
11876
11994
  /*
11877
- Single-calendar DateRangePicker with hover-preview
11878
- - Enforces mandatory range selection: the component will NOT commit or show a selected range
11879
- until both start and end dates are chosen.
11880
- - Popup cannot be dismissed (via outside click, trigger toggle, or Escape) while only
11881
- a start date is selected. Use Clear to cancel the in-progress selection.
11995
+ Single-calendar DateRangePicker with hover-preview and space-aware popup positioning
11996
+ - Enforces mandatory range selection until both start and end are chosen.
11997
+ - Popup flips above/below and clamps horizontally to avoid clipping.
11998
+ - Popup won't close while only a start date is selected (Clear cancels in-progress selection).
11882
11999
  */
11883
12000
 
11884
12001
  var normalizeDate = function normalizeDate(d) {
@@ -11952,11 +12069,16 @@ var DateRangePicker = function DateRangePicker(_ref) {
11952
12069
  if (start) setCurrentMonth(start);
11953
12070
  }, [start, end]);
11954
12071
  var datePickerRef = React.useRef(null);
12072
+ var popupRef = React.useRef(null);
12073
+ var _useState1 = React.useState({}),
12074
+ _useState10 = _slicedToArray(_useState1, 2),
12075
+ popupStyle = _useState10[0],
12076
+ setPopupStyle = _useState10[1];
11955
12077
 
11956
12078
  // Outside click: only close if there is NOT an in-progress single-date selection
11957
12079
  React.useEffect(function () {
11958
12080
  var handleClickOutside = function handleClickOutside(e) {
11959
- if (datePickerRef.current && !datePickerRef.current.contains(e.target)) {
12081
+ if (datePickerRef.current && !datePickerRef.current.contains(e.target) && (!popupRef.current || !popupRef.current.contains(e.target))) {
11960
12082
  // If user has selected a start but no end, prevent closing to force range selection
11961
12083
  if (localStart && !localEnd) {
11962
12084
  return;
@@ -12112,10 +12234,82 @@ var DateRangePicker = function DateRangePicker(_ref) {
12112
12234
  }, /*#__PURE__*/React.createElement(ChevronRight, {
12113
12235
  className: "h-4 w-4"
12114
12236
  })));
12237
+
12238
+ // compute popup position so it's aware of viewport edges (top/bottom/left/right)
12239
+ var computePopupPosition = function computePopupPosition() {
12240
+ var margin = 8;
12241
+ var trigger = datePickerRef.current;
12242
+ var popup = popupRef.current;
12243
+ if (!trigger || !popup) return;
12244
+ var triggerRect = trigger.getBoundingClientRect();
12245
+ // measure popup by temporarily making it visible if hidden
12246
+ var popupRect = popup.getBoundingClientRect();
12247
+
12248
+ // If the popup is not yet fully measured (width/height 0), try to read offsetWidth/Height
12249
+ if (popupRect.width === 0 || popupRect.height === 0) {
12250
+ popupRect = {
12251
+ width: popup.offsetWidth || 320,
12252
+ height: popup.offsetHeight || 300
12253
+ };
12254
+ }
12255
+
12256
+ // Decide vertical placement
12257
+ var spaceBelow = window.innerHeight - triggerRect.bottom;
12258
+ var spaceAbove = triggerRect.top;
12259
+ var placeBelow = spaceBelow >= popupRect.height + margin;
12260
+ if (!placeBelow && spaceAbove >= popupRect.height + margin) placeBelow = false;
12261
+ // If neither side has full space, pick the side with more space
12262
+ if (!placeBelow && spaceBelow < popupRect.height + margin && spaceAbove < popupRect.height + margin) {
12263
+ placeBelow = spaceBelow >= spaceAbove;
12264
+ }
12265
+
12266
+ // compute top coordinate (fixed positioning)
12267
+ var top;
12268
+ if (placeBelow) {
12269
+ top = Math.min(window.innerHeight - margin - popupRect.height, triggerRect.bottom + margin);
12270
+ } else {
12271
+ top = Math.max(margin, triggerRect.top - popupRect.height - margin);
12272
+ }
12273
+
12274
+ // Decide horizontal placement (try align left to trigger)
12275
+ var left = triggerRect.left;
12276
+ // If overflowing right, shift left
12277
+ if (left + popupRect.width > window.innerWidth - margin) {
12278
+ left = Math.max(margin, window.innerWidth - margin - popupRect.width);
12279
+ }
12280
+ // If overflowing left, clamp
12281
+ if (left < margin) left = margin;
12282
+ var transformOrigin = placeBelow ? "top left" : "bottom left";
12283
+ setPopupStyle({
12284
+ position: "fixed",
12285
+ top: "".concat(Math.round(top), "px"),
12286
+ left: "".concat(Math.round(left), "px"),
12287
+ zIndex: 9999,
12288
+ transformOrigin: transformOrigin
12289
+ });
12290
+ };
12291
+
12292
+ // recompute on open, on resize/scroll, and when popup content changes (currentMonth)
12293
+ React.useLayoutEffect(function () {
12294
+ if (!isOpen) return;
12295
+ // compute on next frame to ensure popup ref exists in DOM (especially with portal)
12296
+ computePopupPosition();
12297
+ var handleResize = function handleResize() {
12298
+ return computePopupPosition();
12299
+ };
12300
+ window.addEventListener("resize", handleResize);
12301
+ window.addEventListener("scroll", handleResize, true); // capture scrolling in ancestors
12302
+ return function () {
12303
+ window.removeEventListener("resize", handleResize);
12304
+ window.removeEventListener("scroll", handleResize, true);
12305
+ };
12306
+ }, [isOpen, currentMonth]);
12115
12307
  var calendarPopup = /*#__PURE__*/React.createElement("div", {
12116
- className: "absolute z-50 mt-1 w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
12308
+ ref: popupRef,
12309
+ className: "w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
12117
12310
  role: "dialog",
12118
- "aria-modal": "false"
12311
+ "aria-modal": "false",
12312
+ style: popupStyle
12119
12313
  }, header, /*#__PURE__*/React.createElement("div", {
12120
12314
  className: "mb-2 grid grid-cols-7 gap-1"
12121
12315
  }, Array.from({