@dreamtree-org/twreact-ui 1.0.68 → 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.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import React__default, { forwardRef, createElement, useId, useRef, useState, useEffect, useImperativeHandle, useMemo, useCallback, createContext, useContext, cloneElement, PureComponent } from 'react';
2
+ import React__default, { forwardRef, createElement, useId, useRef, useState, useEffect, useImperativeHandle, useMemo, useLayoutEffect, useCallback, createContext, useContext, cloneElement, PureComponent } from 'react';
3
3
  import ReactDOM, { createPortal } from 'react-dom';
4
4
 
5
5
  function _extends() {
@@ -4881,8 +4881,6 @@ var Table = function Table(_ref) {
4881
4881
  }
4882
4882
  }, "Next")))));
4883
4883
  };
4884
-
4885
- /* ---------------- ActionMenuPortal (updated for mobile) ---------------- */
4886
4884
  function ActionMenuPortal(_ref4) {
4887
4885
  var anchorElem = _ref4.anchorElem,
4888
4886
  anchorRow = _ref4.anchorRow,
@@ -4891,10 +4889,7 @@ function ActionMenuPortal(_ref4) {
4891
4889
  _ref4.onClose;
4892
4890
  var onAction = _ref4.onAction,
4893
4891
  menuRef = _ref4.menuRef;
4894
- var _useState29 = useState(0),
4895
- _useState30 = _slicedToArray(_useState29, 2),
4896
- setTick = _useState30[1];
4897
- var _useState31 = useState({
4892
+ var _useState29 = useState({
4898
4893
  left: 0,
4899
4894
  top: 0,
4900
4895
  transformOrigin: "top right",
@@ -4902,11 +4897,15 @@ function ActionMenuPortal(_ref4) {
4902
4897
  width: 180,
4903
4898
  opacity: 0
4904
4899
  }),
4905
- _useState32 = _slicedToArray(_useState31, 2),
4906
- style = _useState32[0],
4907
- setStyle = _useState32[1];
4900
+ _useState30 = _slicedToArray(_useState29, 2),
4901
+ style = _useState30[0],
4902
+ setStyle = _useState30[1];
4908
4903
  var menuWidth = 180;
4909
4904
  var maxMenuHeight = 320;
4905
+ var margin = 8;
4906
+ var minMenuHeight = 80;
4907
+
4908
+ // compute position using actual menu height (if available) and layout measurements
4910
4909
  var computePosition = function computePosition() {
4911
4910
  if (!anchorElem) return;
4912
4911
  var rect = anchorElem.getBoundingClientRect();
@@ -4914,52 +4913,95 @@ function ActionMenuPortal(_ref4) {
4914
4913
  var scrollX = window.scrollX || window.pageXOffset;
4915
4914
  var spaceBelow = window.innerHeight - rect.bottom;
4916
4915
  var spaceAbove = rect.top;
4917
- var top;
4916
+
4917
+ // allowed max height based on available space
4918
+ var allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, Math.max(spaceBelow - margin, spaceAbove - margin)));
4919
+
4920
+ // choose placement by comparing actual available spaces
4918
4921
  var placement = "bottom";
4919
- var maxHeight = Math.min(maxMenuHeight, Math.max(80, spaceBelow - 16));
4920
4922
  if (spaceBelow < 160 && spaceAbove > spaceBelow) {
4921
4923
  placement = "top";
4922
- maxHeight = Math.min(maxMenuHeight, Math.max(80, spaceAbove - 16));
4923
- top = rect.top + scrollY - maxHeight - 8;
4924
+ // when placing on top we should cap allowedMaxHeight by spaceAbove
4925
+ allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, spaceAbove - margin));
4924
4926
  } else {
4925
- top = rect.bottom + scrollY + 8;
4927
+ // placing bottom: cap by spaceBelow
4928
+ allowedMaxHeight = Math.min(maxMenuHeight, Math.max(minMenuHeight, spaceBelow - margin));
4926
4929
  }
4930
+
4931
+ // measure the menu's content height if we can, and compute the actual menu height we'll use.
4932
+ var measuredMenuHeight = allowedMaxHeight;
4933
+ var menuEl = menuRef === null || menuRef === void 0 ? void 0 : menuRef.current;
4934
+ if (menuEl) {
4935
+ // scrollHeight is the content height; offsetHeight may reflect current rendered height.
4936
+ var contentHeight = menuEl.scrollHeight || menuEl.offsetHeight || allowedMaxHeight;
4937
+ // actual height will be min(contentHeight, allowedMaxHeight)
4938
+ measuredMenuHeight = Math.min(contentHeight, allowedMaxHeight);
4939
+ }
4940
+
4941
+ // compute top according to placement and measuredMenuHeight
4942
+ var top;
4943
+ if (placement === "top") {
4944
+ // place menu so its bottom sits just above the anchor (rect.top - margin)
4945
+ top = rect.top + scrollY - measuredMenuHeight - margin;
4946
+ } else {
4947
+ // place menu just below the anchor (rect.bottom + margin)
4948
+ top = rect.bottom + scrollY + margin;
4949
+ }
4950
+
4951
+ // clamp top to viewport (so it never goes off-screen)
4952
+ var minTop = margin + scrollY;
4953
+ var maxTop = window.innerHeight + scrollY - measuredMenuHeight - margin;
4954
+ if (top < minTop) top = minTop;
4955
+ if (top > maxTop) top = maxTop;
4956
+
4957
+ // compute left and clamp horizontally
4927
4958
  var left = rect.right + scrollX - menuWidth;
4928
- if (left < 8) left = 8;
4929
- if (left + menuWidth > window.innerWidth - 8) left = window.innerWidth - menuWidth - 8;
4959
+ if (left < margin) left = margin;
4960
+ if (left + menuWidth > window.innerWidth - margin) {
4961
+ left = Math.max(margin, window.innerWidth - menuWidth - margin);
4962
+ }
4930
4963
  var transformOrigin = placement === "bottom" ? "top right" : "bottom right";
4964
+
4965
+ // set style (opacity 1 to fade-in)
4931
4966
  setStyle({
4932
4967
  left: left,
4933
4968
  top: top,
4934
4969
  transformOrigin: transformOrigin,
4935
- maxHeight: maxHeight,
4970
+ maxHeight: allowedMaxHeight,
4936
4971
  width: menuWidth,
4937
4972
  opacity: 1
4938
4973
  });
4939
4974
  };
4940
- useEffect(function () {
4975
+
4976
+ // useLayoutEffect so the position is measured & applied before paint
4977
+ useLayoutEffect(function () {
4978
+ // compute once after mount
4941
4979
  computePosition();
4942
4980
  var onScrollOrResize = function onScrollOrResize() {
4943
- computePosition();
4944
- setTick(function (t) {
4945
- return t + 1;
4981
+ // using requestAnimationFrame to avoid layout thrash on fast scroll/resize
4982
+ window.requestAnimationFrame(function () {
4983
+ return computePosition();
4946
4984
  });
4947
4985
  };
4948
4986
  window.addEventListener("resize", onScrollOrResize);
4987
+ // capture scrolls (true) so position updates even when scrolling ancestor elements
4949
4988
  window.addEventListener("scroll", onScrollOrResize, true);
4950
4989
  return function () {
4951
4990
  window.removeEventListener("resize", onScrollOrResize);
4952
4991
  window.removeEventListener("scroll", onScrollOrResize, true);
4953
4992
  };
4954
- }, [anchorElem]);
4993
+ // Recompute when anchor element changes or actions change (content height may change).
4994
+ }, [anchorElem, actions, menuRef]);
4955
4995
  return /*#__PURE__*/createPortal(/*#__PURE__*/React__default.createElement("div", {
4956
4996
  ref: menuRef,
4957
4997
  style: {
4998
+ position: "absolute",
4958
4999
  top: style.top,
4959
5000
  left: style.left,
4960
5001
  width: style.width,
4961
5002
  maxHeight: style.maxHeight,
4962
- transformOrigin: style.transformOrigin
5003
+ transformOrigin: style.transformOrigin,
5004
+ opacity: style.opacity
4963
5005
  },
4964
5006
  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",
4965
5007
  onClick: function onClick(e) {
@@ -10350,7 +10392,12 @@ var DatePicker = /*#__PURE__*/React__default.forwardRef(function (_ref, forwarde
10350
10392
  currentMonth = _useState4[0],
10351
10393
  setCurrentMonth = _useState4[1];
10352
10394
  var datePickerRef = useRef(null);
10395
+ var popupRef = useRef(null);
10353
10396
  var gridButtonRefs = useRef([]);
10397
+ var _useState5 = useState({}),
10398
+ _useState6 = _slicedToArray(_useState5, 2),
10399
+ popupStyle = _useState6[0],
10400
+ setPopupStyle = _useState6[1];
10354
10401
  useImperativeHandle(forwardedRef, function () {
10355
10402
  return datePickerRef.current;
10356
10403
  }, []);
@@ -10363,7 +10410,7 @@ var DatePicker = /*#__PURE__*/React__default.forwardRef(function (_ref, forwarde
10363
10410
  // Close on outside click
10364
10411
  useEffect(function () {
10365
10412
  var handleClickOutside = function handleClickOutside(e) {
10366
- if (datePickerRef.current && !datePickerRef.current.contains(e.target)) {
10413
+ if (datePickerRef.current && !datePickerRef.current.contains(e.target) && popupRef.current && !popupRef.current.contains(e.target)) {
10367
10414
  setIsOpen(false);
10368
10415
  }
10369
10416
  };
@@ -10479,6 +10526,75 @@ var DatePicker = /*#__PURE__*/React__default.forwardRef(function (_ref, forwarde
10479
10526
  if (idx >= 0) focusDayAtIndex(idx);
10480
10527
  }, 0);
10481
10528
  };
10529
+
10530
+ // compute popup position so it's aware of viewport edges (top/bottom/left/right)
10531
+ var computePopupPosition = function computePopupPosition() {
10532
+ var margin = 8;
10533
+ var trigger = datePickerRef.current;
10534
+ var popup = popupRef.current;
10535
+ if (!trigger || !popup) return;
10536
+ var triggerRect = trigger.getBoundingClientRect();
10537
+ // measure popup by temporarily making it visible if hidden
10538
+ var popupRect = popup.getBoundingClientRect();
10539
+
10540
+ // If the popup is not yet fully measured (width/height 0), try to read offsetWidth/Height
10541
+ if (popupRect.width === 0 || popupRect.height === 0) {
10542
+ popupRect = {
10543
+ width: popup.offsetWidth || 240,
10544
+ height: popup.offsetHeight || 300
10545
+ };
10546
+ }
10547
+
10548
+ // Decide vertical placement
10549
+ var spaceBelow = window.innerHeight - triggerRect.bottom;
10550
+ var spaceAbove = triggerRect.top;
10551
+ var placeBelow = spaceBelow >= popupRect.height + margin;
10552
+ if (!placeBelow && spaceAbove >= popupRect.height + margin) placeBelow = false;
10553
+ // If neither side has full space, pick the side with more space
10554
+ if (!placeBelow && spaceBelow < popupRect.height + margin && spaceAbove < popupRect.height + margin) {
10555
+ placeBelow = spaceBelow >= spaceAbove;
10556
+ }
10557
+
10558
+ // compute top coordinate (fixed positioning)
10559
+ var top;
10560
+ if (placeBelow) {
10561
+ top = Math.min(window.innerHeight - margin - popupRect.height, triggerRect.bottom + margin);
10562
+ } else {
10563
+ top = Math.max(margin, triggerRect.top - popupRect.height - margin);
10564
+ }
10565
+
10566
+ // Decide horizontal placement (try align left to trigger)
10567
+ var left = triggerRect.left;
10568
+ // If overflowing right, shift left
10569
+ if (left + popupRect.width > window.innerWidth - margin) {
10570
+ left = Math.max(margin, window.innerWidth - margin - popupRect.width);
10571
+ }
10572
+ // If overflowing left, clamp
10573
+ if (left < margin) left = margin;
10574
+ var transformOrigin = placeBelow ? "top left" : "bottom left";
10575
+ setPopupStyle({
10576
+ position: "fixed",
10577
+ top: "".concat(Math.round(top), "px"),
10578
+ left: "".concat(Math.round(left), "px"),
10579
+ zIndex: 9999,
10580
+ transformOrigin: transformOrigin
10581
+ });
10582
+ };
10583
+
10584
+ // recompute on open, on resize/scroll, and when popup content changes (currentMonth)
10585
+ useLayoutEffect(function () {
10586
+ if (!isOpen) return;
10587
+ computePopupPosition();
10588
+ var handleResize = function handleResize() {
10589
+ return computePopupPosition();
10590
+ };
10591
+ window.addEventListener("resize", handleResize);
10592
+ window.addEventListener("scroll", handleResize, true); // capture scrolling in ancestors
10593
+ return function () {
10594
+ window.removeEventListener("resize", handleResize);
10595
+ window.removeEventListener("scroll", handleResize, true);
10596
+ };
10597
+ }, [isOpen, currentMonth]);
10482
10598
  var header = /*#__PURE__*/React__default.createElement("div", {
10483
10599
  className: "mb-4 flex items-center justify-between"
10484
10600
  }, /*#__PURE__*/React__default.createElement("button", {
@@ -10503,9 +10619,11 @@ var DatePicker = /*#__PURE__*/React__default.forwardRef(function (_ref, forwarde
10503
10619
  className: "h-4 w-4"
10504
10620
  })));
10505
10621
  var calendarPopup = /*#__PURE__*/React__default.createElement("div", {
10506
- className: "absolute z-50 mt-1 w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
10622
+ ref: popupRef,
10623
+ className: "w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
10507
10624
  role: "dialog",
10508
- "aria-modal": "false"
10625
+ "aria-modal": "false",
10626
+ style: popupStyle
10509
10627
  }, header, /*#__PURE__*/React__default.createElement("div", {
10510
10628
  className: "mb-2 grid grid-cols-7 gap-1"
10511
10629
  }, Array.from({
@@ -11854,11 +11972,10 @@ function Slider(_ref6) {
11854
11972
  var _excluded$g = ["value", "onChange", "placeholder", "label", "error", "disabled", "required", "minDate", "maxDate", "className", "weekStartsOn", "portal", "displayFormat", "locale", "showClear", "closeOnSelect"];
11855
11973
 
11856
11974
  /*
11857
- Single-calendar DateRangePicker with hover-preview
11858
- - Enforces mandatory range selection: the component will NOT commit or show a selected range
11859
- until both start and end dates are chosen.
11860
- - Popup cannot be dismissed (via outside click, trigger toggle, or Escape) while only
11861
- a start date is selected. Use Clear to cancel the in-progress selection.
11975
+ Single-calendar DateRangePicker with hover-preview and space-aware popup positioning
11976
+ - Enforces mandatory range selection until both start and end are chosen.
11977
+ - Popup flips above/below and clamps horizontally to avoid clipping.
11978
+ - Popup won't close while only a start date is selected (Clear cancels in-progress selection).
11862
11979
  */
11863
11980
 
11864
11981
  var normalizeDate = function normalizeDate(d) {
@@ -11932,11 +12049,16 @@ var DateRangePicker = function DateRangePicker(_ref) {
11932
12049
  if (start) setCurrentMonth(start);
11933
12050
  }, [start, end]);
11934
12051
  var datePickerRef = useRef(null);
12052
+ var popupRef = useRef(null);
12053
+ var _useState1 = useState({}),
12054
+ _useState10 = _slicedToArray(_useState1, 2),
12055
+ popupStyle = _useState10[0],
12056
+ setPopupStyle = _useState10[1];
11935
12057
 
11936
12058
  // Outside click: only close if there is NOT an in-progress single-date selection
11937
12059
  useEffect(function () {
11938
12060
  var handleClickOutside = function handleClickOutside(e) {
11939
- if (datePickerRef.current && !datePickerRef.current.contains(e.target)) {
12061
+ if (datePickerRef.current && !datePickerRef.current.contains(e.target) && (!popupRef.current || !popupRef.current.contains(e.target))) {
11940
12062
  // If user has selected a start but no end, prevent closing to force range selection
11941
12063
  if (localStart && !localEnd) {
11942
12064
  return;
@@ -12092,10 +12214,82 @@ var DateRangePicker = function DateRangePicker(_ref) {
12092
12214
  }, /*#__PURE__*/React__default.createElement(ChevronRight, {
12093
12215
  className: "h-4 w-4"
12094
12216
  })));
12217
+
12218
+ // compute popup position so it's aware of viewport edges (top/bottom/left/right)
12219
+ var computePopupPosition = function computePopupPosition() {
12220
+ var margin = 8;
12221
+ var trigger = datePickerRef.current;
12222
+ var popup = popupRef.current;
12223
+ if (!trigger || !popup) return;
12224
+ var triggerRect = trigger.getBoundingClientRect();
12225
+ // measure popup by temporarily making it visible if hidden
12226
+ var popupRect = popup.getBoundingClientRect();
12227
+
12228
+ // If the popup is not yet fully measured (width/height 0), try to read offsetWidth/Height
12229
+ if (popupRect.width === 0 || popupRect.height === 0) {
12230
+ popupRect = {
12231
+ width: popup.offsetWidth || 320,
12232
+ height: popup.offsetHeight || 300
12233
+ };
12234
+ }
12235
+
12236
+ // Decide vertical placement
12237
+ var spaceBelow = window.innerHeight - triggerRect.bottom;
12238
+ var spaceAbove = triggerRect.top;
12239
+ var placeBelow = spaceBelow >= popupRect.height + margin;
12240
+ if (!placeBelow && spaceAbove >= popupRect.height + margin) placeBelow = false;
12241
+ // If neither side has full space, pick the side with more space
12242
+ if (!placeBelow && spaceBelow < popupRect.height + margin && spaceAbove < popupRect.height + margin) {
12243
+ placeBelow = spaceBelow >= spaceAbove;
12244
+ }
12245
+
12246
+ // compute top coordinate (fixed positioning)
12247
+ var top;
12248
+ if (placeBelow) {
12249
+ top = Math.min(window.innerHeight - margin - popupRect.height, triggerRect.bottom + margin);
12250
+ } else {
12251
+ top = Math.max(margin, triggerRect.top - popupRect.height - margin);
12252
+ }
12253
+
12254
+ // Decide horizontal placement (try align left to trigger)
12255
+ var left = triggerRect.left;
12256
+ // If overflowing right, shift left
12257
+ if (left + popupRect.width > window.innerWidth - margin) {
12258
+ left = Math.max(margin, window.innerWidth - margin - popupRect.width);
12259
+ }
12260
+ // If overflowing left, clamp
12261
+ if (left < margin) left = margin;
12262
+ var transformOrigin = placeBelow ? "top left" : "bottom left";
12263
+ setPopupStyle({
12264
+ position: "fixed",
12265
+ top: "".concat(Math.round(top), "px"),
12266
+ left: "".concat(Math.round(left), "px"),
12267
+ zIndex: 9999,
12268
+ transformOrigin: transformOrigin
12269
+ });
12270
+ };
12271
+
12272
+ // recompute on open, on resize/scroll, and when popup content changes (currentMonth)
12273
+ useLayoutEffect(function () {
12274
+ if (!isOpen) return;
12275
+ // compute on next frame to ensure popup ref exists in DOM (especially with portal)
12276
+ computePopupPosition();
12277
+ var handleResize = function handleResize() {
12278
+ return computePopupPosition();
12279
+ };
12280
+ window.addEventListener("resize", handleResize);
12281
+ window.addEventListener("scroll", handleResize, true); // capture scrolling in ancestors
12282
+ return function () {
12283
+ window.removeEventListener("resize", handleResize);
12284
+ window.removeEventListener("scroll", handleResize, true);
12285
+ };
12286
+ }, [isOpen, currentMonth]);
12095
12287
  var calendarPopup = /*#__PURE__*/React__default.createElement("div", {
12096
- className: "absolute z-50 mt-1 w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
12288
+ ref: popupRef,
12289
+ className: "w-80 rounded-md border border-gray-300 bg-white p-4 shadow-lg",
12097
12290
  role: "dialog",
12098
- "aria-modal": "false"
12291
+ "aria-modal": "false",
12292
+ style: popupStyle
12099
12293
  }, header, /*#__PURE__*/React__default.createElement("div", {
12100
12294
  className: "mb-2 grid grid-cols-7 gap-1"
12101
12295
  }, Array.from({
@@ -20439,59 +20633,64 @@ var Helpers = /*#__PURE__*/function () {
20439
20633
  }
20440
20634
  }]);
20441
20635
  }();
20636
+
20637
+ // A serializable-safe TrackingPromise that *does not* put the promise or any methods in the redux action payload.
20638
+ // This class is only intended to be used locally (not as part of redux/store payloads).
20639
+ // DO NOT EVER put an instance of TrackingPromise directly in redux actions.
20442
20640
  var TrackingPromise = /*#__PURE__*/function () {
20443
20641
  function TrackingPromise(callback) {
20444
20642
  var _this = this;
20445
20643
  _classCallCheck$1(this, TrackingPromise);
20446
- _defineProperty$4(this, "state", "pending");
20447
- _defineProperty$4(this, "instance", null);
20448
- _defineProperty$4(this, "callback", null);
20449
- _defineProperty$4(this, "promiseParams", null);
20450
- this.callback = callback;
20451
- this.instance = new Promise(function (resolve, reject) {
20452
- _this.promiseParams = {
20453
- resolve: resolve,
20454
- reject: reject
20644
+ this._state = "pending";
20645
+ this._callback = callback;
20646
+ this._promise = new Promise(function (resolve, reject) {
20647
+ _this._resolve = function (value) {
20648
+ _this._state = "fulfilled";
20649
+ resolve(value);
20650
+ };
20651
+ _this._reject = function (reason) {
20652
+ _this._state = "rejected";
20653
+ reject(reason);
20455
20654
  };
20456
- _this.callback(_this.resolve.bind(_this), _this.reject.bind(_this));
20655
+ _this._callback(_this._resolve, _this._reject);
20457
20656
  });
20458
20657
  }
20459
20658
  return _createClass$1(TrackingPromise, [{
20460
20659
  key: "resolve",
20461
20660
  value: function resolve(value) {
20462
- this.state = "fulfilled";
20463
- this.promiseParams.resolve(value);
20661
+ if (this._state !== "pending") return;
20662
+ this._resolve(value);
20464
20663
  }
20465
20664
  }, {
20466
20665
  key: "reject",
20467
20666
  value: function reject(reason) {
20468
- this.state = "rejected";
20469
- this.promiseParams.reject(reason);
20667
+ if (this._state !== "pending") return;
20668
+ this._reject(reason);
20470
20669
  }
20471
20670
  }, {
20472
20671
  key: "isFulfilled",
20473
20672
  value: function isFulfilled() {
20474
- return this.state === "fulfilled";
20673
+ return this._state === "fulfilled";
20475
20674
  }
20476
20675
  }, {
20477
20676
  key: "isRejected",
20478
20677
  value: function isRejected() {
20479
- return this.state === "rejected";
20678
+ return this._state === "rejected";
20480
20679
  }
20481
20680
  }, {
20482
20681
  key: "isPending",
20483
20682
  value: function isPending() {
20484
- return this.state === "pending";
20683
+ return this._state === "pending";
20485
20684
  }
20486
20685
  }, {
20487
20686
  key: "then",
20488
20687
  value: function then(onFulfilled, onRejected) {
20489
- return this.instance.then(onFulfilled, onRejected);
20688
+ return this._promise.then(onFulfilled, onRejected);
20490
20689
  }
20491
20690
  }, {
20492
20691
  key: "catch",
20493
20692
  value: function _catch(onRejected) {
20494
- return this.instance["catch"](onRejected);
20693
+ return this._promise["catch"](onRejected);
20495
20694
  }
20496
20695
  }]);
20497
20696
  }();