@trackunit/react-components 1.10.5 → 1.10.7

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/index.cjs.js CHANGED
@@ -12,11 +12,11 @@ var IconSpriteSolid = require('@trackunit/ui-icons/icons-sprite-solid.svg');
12
12
  var stringTs = require('string-ts');
13
13
  var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
14
14
  var reactSlot = require('@radix-ui/react-slot');
15
+ var esToolkit = require('es-toolkit');
15
16
  var reactVirtual = require('@tanstack/react-virtual');
16
17
  var usehooksTs = require('usehooks-ts');
17
18
  var reactRouter = require('@tanstack/react-router');
18
19
  var react$1 = require('@floating-ui/react');
19
- var esToolkit = require('es-toolkit');
20
20
  var tailwindMerge = require('tailwind-merge');
21
21
  var reactHelmetAsync = require('react-helmet-async');
22
22
  var reactTabs = require('@radix-ui/react-tabs');
@@ -279,6 +279,8 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
279
279
  * Hook to detect if text content is wrapping to multiple lines
280
280
  *
281
281
  * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
282
+ * @param {object} [options] - Configuration options
283
+ * @param {boolean} [options.skip] - Whether to skip observing for wrapping (default: false)
282
284
  * @returns {{
283
285
  * ref: RefObject<TElement | null>;
284
286
  * isTooltipVisible: boolean;
@@ -286,7 +288,8 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
286
288
  * - `ref`: a ref to attach to the element you want to observe for truncation.
287
289
  * - `isTextWrapping`: a boolean indicating if the text is wrapping.
288
290
  */
289
- const useIsTextWrapping = () => {
291
+ const useIsTextWrapping = (options = {}) => {
292
+ const { skip = false } = options;
290
293
  const ref = react.useRef(null);
291
294
  const [isTextWrapping, setIsTextWrapping] = react.useState(false);
292
295
  const setTextWrappingState = react.useCallback(() => {
@@ -297,7 +300,7 @@ const useIsTextWrapping = () => {
297
300
  setIsTextWrapping(clientHeight > scrollHeight / 2);
298
301
  }, []);
299
302
  react.useEffect(() => {
300
- if (!ref.current) {
303
+ if (skip || !ref.current) {
301
304
  return;
302
305
  }
303
306
  // Perform an immediate measurement on mount.
@@ -311,8 +314,8 @@ const useIsTextWrapping = () => {
311
314
  observer.observe(ref.current);
312
315
  // Clean up on unmount
313
316
  return () => observer.disconnect();
314
- }, [setTextWrappingState]);
315
- return { ref, isTextWrapping };
317
+ }, [setTextWrappingState, skip]);
318
+ return react.useMemo(() => ({ ref, isTextWrapping }), [isTextWrapping]);
316
319
  };
317
320
 
318
321
  const cvaText = cssClassVarianceUtilities.cvaMerge(["text-black", "m-0", "relative", "text-sm", "font-normal"], {
@@ -1074,6 +1077,8 @@ const createBreakpointState = ({ width }) => {
1074
1077
  * an object with boolean values indicating which breakpoints are currently active.
1075
1078
  *
1076
1079
  * @param {RefObject<HTMLElement>} ref - Reference to the container element to observe
1080
+ * @param {object} [options] - Configuration options
1081
+ * @param {boolean} [options.skip] - Whether to skip observing for breakpoint changes (default: false)
1077
1082
  * @returns {BreakpointState} An object containing boolean values for each container size breakpoint.
1078
1083
  * @example
1079
1084
  * const MyComponent = () => {
@@ -1093,7 +1098,8 @@ const createBreakpointState = ({ width }) => {
1093
1098
  * );
1094
1099
  * }
1095
1100
  */
1096
- const useContainerBreakpoints = (ref) => {
1101
+ const useContainerBreakpoints = (ref, options = {}) => {
1102
+ const { skip = false } = options;
1097
1103
  const [containerSize, setContainerSize] = react.useState(() => defaultBreakpointState);
1098
1104
  react.useEffect(() => {
1099
1105
  if (process.env.NODE_ENV === "development" && !ref.current) {
@@ -1110,6 +1116,9 @@ const useContainerBreakpoints = (ref) => {
1110
1116
  setContainerSize(createBreakpointState({ width }));
1111
1117
  }, [ref]);
1112
1118
  react.useEffect(() => {
1119
+ if (skip) {
1120
+ return;
1121
+ }
1113
1122
  const element = ref.current;
1114
1123
  if (!element) {
1115
1124
  return;
@@ -1122,7 +1131,7 @@ const useContainerBreakpoints = (ref) => {
1122
1131
  return () => {
1123
1132
  resizeObserver.disconnect();
1124
1133
  };
1125
- }, [updateContainerSize, ref]);
1134
+ }, [updateContainerSize, ref, skip]);
1126
1135
  return containerSize;
1127
1136
  };
1128
1137
 
@@ -1134,7 +1143,7 @@ const useContainerBreakpoints = (ref) => {
1134
1143
  * @param {number} options.duration - Duration of the timeout in milliseconds.
1135
1144
  * @returns {object} An object containing functions to start and stop the timeout.
1136
1145
  */
1137
- const useTimeout = ({ onTimeout, duration }) => {
1146
+ const useTimeout = ({ onTimeout, duration, }) => {
1138
1147
  const ready = react.useRef(false);
1139
1148
  const timeout = react.useRef(null);
1140
1149
  const callback = react.useRef(onTimeout);
@@ -1166,7 +1175,7 @@ const useTimeout = ({ onTimeout, duration }) => {
1166
1175
  }
1167
1176
  };
1168
1177
  }, []);
1169
- return { startTimeout, stopTimeout };
1178
+ return react.useMemo(() => ({ startTimeout, stopTimeout }), [startTimeout, stopTimeout]);
1170
1179
  };
1171
1180
 
1172
1181
  /**
@@ -1179,20 +1188,9 @@ const useTimeout = ({ onTimeout, duration }) => {
1179
1188
  * @param {number} options.maxRetries - Maximum number of retry attempts.
1180
1189
  * @returns {object} An object containing functions to start and stop the timeout, current retry count, and the timeout status.
1181
1190
  */
1182
- const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries }) => {
1191
+ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }) => {
1183
1192
  const retries = react.useRef(0);
1184
1193
  const [isRunning, setIsRunning] = react.useState(false); // Track the timeout status
1185
- const stopTimeouts = () => {
1186
- stopTimeout();
1187
- setIsRunning(false); // Update the status when stopped
1188
- };
1189
- const startTimeouts = () => {
1190
- if (isRunning) {
1191
- return; // Prevent multiple timeouts from running
1192
- }
1193
- startTimeout();
1194
- setIsRunning(true); // Update the status when started
1195
- };
1196
1194
  const { startTimeout, stopTimeout } = useTimeout({
1197
1195
  duration,
1198
1196
  onTimeout: () => {
@@ -1209,14 +1207,25 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries })
1209
1207
  }
1210
1208
  },
1211
1209
  });
1212
- return {
1210
+ const stopTimeouts = react.useCallback(() => {
1211
+ stopTimeout();
1212
+ setIsRunning(false); // Update the status when stopped
1213
+ }, [stopTimeout]);
1214
+ const startTimeouts = react.useCallback(() => {
1215
+ if (isRunning) {
1216
+ return; // Prevent multiple timeouts from running
1217
+ }
1218
+ startTimeout();
1219
+ setIsRunning(true); // Update the status when started
1220
+ }, [isRunning, startTimeout]);
1221
+ return react.useMemo(() => ({
1213
1222
  startTimeouts,
1214
1223
  stopTimeouts,
1215
1224
  isRunning,
1216
1225
  get retries() {
1217
1226
  return retries.current;
1218
1227
  },
1219
- };
1228
+ }), [startTimeouts, stopTimeouts, isRunning]);
1220
1229
  };
1221
1230
 
1222
1231
  /**
@@ -1264,8 +1273,11 @@ const useDebounce = (value, delay = 500, direction, onBounce) => {
1264
1273
  function useDevicePixelRatio(options) {
1265
1274
  const dpr = getDevicePixelRatio(options);
1266
1275
  const [currentDpr, setCurrentDpr] = react.useState(dpr);
1267
- const { defaultDpr, maxDpr, round } = options || {};
1276
+ const { defaultDpr, maxDpr, round, skip = false } = options || {};
1268
1277
  react.useEffect(() => {
1278
+ if (skip) {
1279
+ return;
1280
+ }
1269
1281
  const canListen = typeof window !== "undefined" && "matchMedia" in window;
1270
1282
  if (!canListen) {
1271
1283
  return;
@@ -1276,7 +1288,7 @@ function useDevicePixelRatio(options) {
1276
1288
  return () => {
1277
1289
  mediaMatcher.removeEventListener("change", updateDpr);
1278
1290
  };
1279
- }, [currentDpr, defaultDpr, maxDpr, round]);
1291
+ }, [currentDpr, defaultDpr, maxDpr, round, skip]);
1280
1292
  return currentDpr;
1281
1293
  }
1282
1294
  /**
@@ -1303,8 +1315,8 @@ function getDevicePixelRatio(options) {
1303
1315
  * const [state, dispatch] = useElevatedReducer(reducer, initialState, elevatedReducerState);
1304
1316
  */
1305
1317
  const useElevatedReducer = (reducer, initialState, customState) => {
1306
- const fallbackState = react.useReducer(reducer, initialState);
1307
- return customState ?? fallbackState;
1318
+ const [fallbackValue, fallbackDispatch] = react.useReducer(reducer, initialState);
1319
+ return react.useMemo(() => customState ?? [fallbackValue, fallbackDispatch], [customState, fallbackValue, fallbackDispatch]);
1308
1320
  };
1309
1321
 
1310
1322
  /**
@@ -1315,15 +1327,16 @@ const useElevatedReducer = (reducer, initialState, customState) => {
1315
1327
  * If no custom state is provided, the fallback state will be used and it works like a normal useState hook.
1316
1328
  */
1317
1329
  const useElevatedState = (initialState, customState) => {
1318
- const fallbackState = react.useState(initialState);
1319
- return react.useMemo(() => customState ?? fallbackState, [customState, fallbackState]);
1330
+ const [fallbackValue, fallbackSetter] = react.useState(initialState);
1331
+ return react.useMemo(() => customState ?? [fallbackValue, fallbackSetter], [customState, fallbackValue, fallbackSetter]);
1320
1332
  };
1321
1333
 
1334
+ const UNINITIALIZED = Symbol("UNINITIALIZED");
1322
1335
  /**
1323
1336
  * Custom hook to get the geometry of an element.
1324
1337
  * Size and position of the element relative to the viewport.
1325
1338
  */
1326
- const useGeometry = (ref, { skip = false } = {}) => {
1339
+ const useGeometry = (ref, { skip = false, onChange } = {}) => {
1327
1340
  const [geometry, setGeometry] = react.useState(() => {
1328
1341
  const rect = ref.current?.getBoundingClientRect();
1329
1342
  if (!rect) {
@@ -1351,6 +1364,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
1351
1364
  });
1352
1365
  const resizeObserver = react.useRef(null);
1353
1366
  const [element, setElement] = react.useState(ref.current);
1367
+ const prevGeometry = react.useRef(UNINITIALIZED);
1354
1368
  // Track changes to ref.current on every render
1355
1369
  if (ref.current !== element) {
1356
1370
  setElement(ref.current);
@@ -1361,7 +1375,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
1361
1375
  }
1362
1376
  // Update geometry immediately when element changes
1363
1377
  const elementRect = element.getBoundingClientRect();
1364
- setGeometry({
1378
+ const newGeometry = {
1365
1379
  width: elementRect.width,
1366
1380
  height: elementRect.height,
1367
1381
  top: elementRect.top,
@@ -1370,13 +1384,23 @@ const useGeometry = (ref, { skip = false } = {}) => {
1370
1384
  right: elementRect.right,
1371
1385
  x: elementRect.x,
1372
1386
  y: elementRect.y,
1373
- });
1387
+ };
1388
+ const prev = prevGeometry.current;
1389
+ const hasChanged = prev === UNINITIALIZED ? false : !esToolkit.isEqual(newGeometry, prev);
1390
+ if (!hasChanged && prev !== UNINITIALIZED) {
1391
+ return;
1392
+ }
1393
+ setGeometry(newGeometry);
1394
+ if (hasChanged && prev !== UNINITIALIZED) {
1395
+ onChange?.(newGeometry);
1396
+ }
1397
+ prevGeometry.current = newGeometry;
1374
1398
  const observe = () => {
1375
1399
  if (!resizeObserver.current) {
1376
1400
  resizeObserver.current = new ResizeObserver(entries => {
1377
1401
  for (const entry of entries) {
1378
1402
  const entryRect = entry.target.getBoundingClientRect();
1379
- setGeometry({
1403
+ const observedGeometry = {
1380
1404
  width: entryRect.width,
1381
1405
  height: entryRect.height,
1382
1406
  top: entryRect.top,
@@ -1385,7 +1409,16 @@ const useGeometry = (ref, { skip = false } = {}) => {
1385
1409
  right: entryRect.right,
1386
1410
  x: entryRect.x,
1387
1411
  y: entryRect.y,
1388
- });
1412
+ };
1413
+ const prevObserved = prevGeometry.current;
1414
+ const hasObservedChanged = prevObserved === UNINITIALIZED ? false : !esToolkit.isEqual(observedGeometry, prevObserved);
1415
+ if (hasObservedChanged || prevObserved === UNINITIALIZED) {
1416
+ setGeometry(observedGeometry);
1417
+ if (hasObservedChanged && prevObserved !== UNINITIALIZED) {
1418
+ onChange?.(observedGeometry);
1419
+ }
1420
+ prevGeometry.current = observedGeometry;
1421
+ }
1389
1422
  }
1390
1423
  });
1391
1424
  }
@@ -1397,7 +1430,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
1397
1430
  resizeObserver.current.disconnect();
1398
1431
  }
1399
1432
  };
1400
- }, [element, skip]);
1433
+ }, [element, onChange, skip]);
1401
1434
  return geometry;
1402
1435
  };
1403
1436
 
@@ -1412,13 +1445,17 @@ const useGeometry = (ref, { skip = false } = {}) => {
1412
1445
  const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
1413
1446
  const [hovering, setHovering] = react.useState(false);
1414
1447
  const debouncedHovering = useDebounce(hovering, delay, direction);
1415
- const onMouseEnter = () => {
1448
+ const onMouseEnter = react.useCallback(() => {
1416
1449
  setHovering(true);
1417
- };
1418
- const onMouseLeave = () => {
1450
+ }, []);
1451
+ const onMouseLeave = react.useCallback(() => {
1419
1452
  setHovering(false);
1420
- };
1421
- return { onMouseEnter, onMouseLeave, hovering: debounced ? debouncedHovering : hovering };
1453
+ }, []);
1454
+ return react.useMemo(() => ({
1455
+ onMouseEnter,
1456
+ onMouseLeave,
1457
+ hovering: debounced ? debouncedHovering : hovering,
1458
+ }), [onMouseEnter, onMouseLeave, debounced, debouncedHovering, hovering]);
1422
1459
  };
1423
1460
 
1424
1461
  const OVERSCAN = 10;
@@ -1433,6 +1470,7 @@ const DEFAULT_ROW_HEIGHT = 50;
1433
1470
  * @param props.estimateSize - Optional function to estimate item height.
1434
1471
  * @param props.overscan - Optional number of items to render outside the visible area.
1435
1472
  * @param props.onChange - Optional callback when virtualizer changes.
1473
+ * @param props.skip - Whether to skip automatic loading of more data (default: false).
1436
1474
  * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
1437
1475
  * @description
1438
1476
  * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
@@ -1446,7 +1484,7 @@ const DEFAULT_ROW_HEIGHT = 50;
1446
1484
  * estimateSize: () => 35,
1447
1485
  * });
1448
1486
  */
1449
- const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, }) => {
1487
+ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, }) => {
1450
1488
  const handleChange = react.useCallback((virtualizer) => {
1451
1489
  onChange?.(virtualizer);
1452
1490
  }, [onChange]);
@@ -1464,7 +1502,7 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
1464
1502
  const virtualItems = rowVirtualizer.getVirtualItems();
1465
1503
  // Auto-load more data based on scroll position and content height
1466
1504
  react.useEffect(() => {
1467
- if (pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
1505
+ if (skip || pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
1468
1506
  return;
1469
1507
  }
1470
1508
  const container = scrollElementRef.current;
@@ -1480,6 +1518,7 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
1480
1518
  pagination.nextPage();
1481
1519
  }
1482
1520
  }, [
1521
+ skip,
1483
1522
  pagination.pageInfo?.hasNextPage,
1484
1523
  pagination.nextPage,
1485
1524
  pagination.isLoading,
@@ -1527,6 +1566,8 @@ const useIsFullscreen = () => {
1527
1566
  * @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
1528
1567
  * @param {string} [text] - (Optional) Text used to trigger a re-check of truncation,
1529
1568
  * especially if the text is dynamic (such as an input's value).
1569
+ * @param {object} [options] - Configuration options
1570
+ * @param {boolean} [options.skip] - Whether to skip observing for truncation (default: false)
1530
1571
  * @returns {{
1531
1572
  * ref: Ref<TElement | null>;
1532
1573
  * isTooltipVisible: boolean;
@@ -1534,7 +1575,7 @@ const useIsFullscreen = () => {
1534
1575
  * - `ref`: a ref to attach to the element you want to observe for truncation.
1535
1576
  * - `isTextTruncated`: a boolean indicating if the text is truncated.
1536
1577
  */
1537
- const useIsTextTruncated = (text) => {
1578
+ const useIsTextTruncated = (text, { skip = false } = {}) => {
1538
1579
  const ref = react.useRef(null);
1539
1580
  const [isTextTruncated, setIsTextTruncated] = react.useState(false);
1540
1581
  const updateTextVisibility = react.useCallback(() => {
@@ -1545,9 +1586,11 @@ const useIsTextTruncated = (text) => {
1545
1586
  setIsTextTruncated(scrollWidth > clientWidth);
1546
1587
  }, []);
1547
1588
  react.useEffect(() => {
1548
- if (!ref.current) {
1589
+ if (skip || !ref.current) {
1549
1590
  return;
1550
1591
  }
1592
+ // Perform an immediate measurement on mount
1593
+ updateTextVisibility();
1551
1594
  // Observe resizing to check if truncation changes
1552
1595
  const observer = new ResizeObserver(() => {
1553
1596
  updateTextVisibility();
@@ -1555,15 +1598,15 @@ const useIsTextTruncated = (text) => {
1555
1598
  observer.observe(ref.current);
1556
1599
  // Clean up on unmount
1557
1600
  return () => observer.disconnect();
1558
- }, [updateTextVisibility]);
1601
+ }, [updateTextVisibility, skip]);
1559
1602
  // Re-check whenever text changes
1560
1603
  react.useEffect(() => {
1561
- if (text === undefined || text === "") {
1604
+ if (skip || text === undefined || text === "") {
1562
1605
  return;
1563
1606
  }
1564
1607
  updateTextVisibility();
1565
- }, [text, updateTextVisibility]);
1566
- return { ref, isTextTruncated };
1608
+ }, [text, updateTextVisibility, skip]);
1609
+ return react.useMemo(() => ({ ref, isTextTruncated }), [isTextTruncated]);
1567
1610
  };
1568
1611
 
1569
1612
  /**
@@ -1692,11 +1735,11 @@ const SCROLL_DEBOUNCE_TIME = 50;
1692
1735
  * Hook for detecting scroll values in horizontal or vertical direction.
1693
1736
  *
1694
1737
  * @param {useRef} elementRef - Ref hook holding the element that needs to be observed during scrolling
1695
- * @param {object} options - Options object containing direction and onScrollStateChange callback
1738
+ * @param {object} options - Options object containing direction, onScrollStateChange callback, and skip
1696
1739
  * @returns {object} An object containing if the element is scrollable, is at the beginning, is at the end, and its current scroll position.
1697
1740
  */
1698
1741
  const useScrollDetection = (elementRef, options) => {
1699
- const { direction = "vertical", onScrollStateChange } = options ?? {};
1742
+ const { direction = "vertical", onScrollStateChange, skip = false } = options ?? {};
1700
1743
  const [isScrollable, setIsScrollable] = react.useState(false);
1701
1744
  const [isAtBeginning, setIsAtBeginning] = react.useState(true);
1702
1745
  const [isAtEnd, setIsAtEnd] = react.useState(false);
@@ -1757,6 +1800,9 @@ const useScrollDetection = (elementRef, options) => {
1757
1800
  });
1758
1801
  }, [isScrollable, isAtBeginning, isAtEnd, scrollPosition, onScrollStateChange, isFirstRender]);
1759
1802
  react.useEffect(() => {
1803
+ if (skip) {
1804
+ return;
1805
+ }
1760
1806
  const element = elementRef?.current;
1761
1807
  if (!element) {
1762
1808
  return;
@@ -1776,7 +1822,7 @@ const useScrollDetection = (elementRef, options) => {
1776
1822
  }
1777
1823
  element.removeEventListener("scroll", debouncedCheckScrollPosition);
1778
1824
  };
1779
- }, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition]);
1825
+ }, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition, skip]);
1780
1826
  return react.useMemo(() => ({ isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
1781
1827
  };
1782
1828
 
@@ -1800,6 +1846,8 @@ const useSelfUpdatingRef = (initialState) => {
1800
1846
  * This hook listens to changes in the viewport size and returns an object with boolean values
1801
1847
  * indicating which breakpoints are currently active.
1802
1848
  *
1849
+ * @param {object} [options] - Configuration options
1850
+ * @param {boolean} [options.skip] - Whether to skip observing for viewport changes (default: false)
1803
1851
  * @returns {BreakpointState} An object containing boolean values for each viewport size breakpoint.
1804
1852
  * @example
1805
1853
  * const MyComponent = () => {
@@ -1812,7 +1860,8 @@ const useSelfUpdatingRef = (initialState) => {
1812
1860
  * return viewportSize.isMd ? <MediumScreenLayout /> : <SmallLayout />;
1813
1861
  * }
1814
1862
  */
1815
- const useViewportBreakpoints = () => {
1863
+ const useViewportBreakpoints = (options = {}) => {
1864
+ const { skip = false } = options;
1816
1865
  const [viewportSize, setViewportSize] = react.useState(() => {
1817
1866
  const newViewportSize = sharedUtils.objectEntries(uiDesignTokens.themeScreenSizeAsNumber).reduce((acc, [size, minWidth]) => ({
1818
1867
  ...acc,
@@ -1828,6 +1877,9 @@ const useViewportBreakpoints = () => {
1828
1877
  setViewportSize(newViewportSize);
1829
1878
  }, []);
1830
1879
  react.useEffect(() => {
1880
+ if (skip) {
1881
+ return;
1882
+ }
1831
1883
  // Initial check
1832
1884
  updateViewportSize();
1833
1885
  // Set up listeners for each breakpoint
@@ -1841,7 +1893,7 @@ const useViewportBreakpoints = () => {
1841
1893
  mql.removeEventListener("change", updateViewportSize);
1842
1894
  });
1843
1895
  };
1844
- }, [updateViewportSize]);
1896
+ }, [updateViewportSize, skip]);
1845
1897
  return viewportSize;
1846
1898
  };
1847
1899
 
@@ -1849,7 +1901,7 @@ const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
1849
1901
  /**
1850
1902
  * Use this hook to disable functionality while the tab is hidden within the browser or to react to focus or blur events
1851
1903
  */
1852
- const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: undefined }) => {
1904
+ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefined, onFocus: undefined }) => {
1853
1905
  const [focused, setFocused] = react.useState(hasFocus());
1854
1906
  const onFocusInternal = react.useCallback(() => {
1855
1907
  setFocused(hasFocus());
@@ -1860,6 +1912,9 @@ const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: u
1860
1912
  onBlur?.();
1861
1913
  }, [onBlur]);
1862
1914
  react.useEffect(() => {
1915
+ if (skip) {
1916
+ return;
1917
+ }
1863
1918
  setFocused(hasFocus()); // Focus for additional renders
1864
1919
  window.addEventListener("focus", onFocusInternal);
1865
1920
  window.addEventListener("blur", onBlurInternal);
@@ -1867,7 +1922,7 @@ const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: u
1867
1922
  window.removeEventListener("focus", onFocusInternal);
1868
1923
  window.removeEventListener("blur", onBlurInternal);
1869
1924
  };
1870
- }, [onBlurInternal, onFocusInternal]);
1925
+ }, [onBlurInternal, onFocusInternal, skip]);
1871
1926
  return react.useMemo(() => ({ focused }), [focused]);
1872
1927
  };
1873
1928
 
@@ -4484,7 +4539,7 @@ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
4484
4539
  }, prefix: prefixIconName ? jsxRuntime.jsx(Icon, { name: prefixIconName, size: "medium" }) : null, variant: variant === "secondary-danger" ? "danger" : "primary" })) : (jsxRuntime.jsx(Button, { dataTestId: dataTestId, disabled: disabled, onClick: e => {
4485
4540
  actionCallback?.(e);
4486
4541
  externalOnClick?.();
4487
- }, prefix: prefixIconName ? jsxRuntime.jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "medium", variant: variant, children: actionText }));
4542
+ }, prefix: prefixIconName ? jsxRuntime.jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "small", variant: variant, children: actionText }));
4488
4543
  // Wrap `content` with Tooltip
4489
4544
  const wrappedWithTooltip = tooltipLabel ? (jsxRuntime.jsx(Tooltip, { className: "block", label: tooltipLabel, children: content })) : (content);
4490
4545
  // Finally, wrap with Link if `to` is provided
@@ -4496,12 +4551,17 @@ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
4496
4551
  * @param {object} props - The props for the PageHeaderSecondaryActions component
4497
4552
  * @param {Array<PageHeaderSecondaryActionType>} props.actions - The secondary actions to render
4498
4553
  * @param {boolean} [props.hasPrimaryAction] - Whether there is a primary action present
4499
- * @returns {ReactElement} PageHeaderSecondaryActions component
4554
+ * @param {boolean} [props.groupActions] - Whether to group actions in a More Menu regardless of action count
4555
+ * @returns {ReactElement | null} PageHeaderSecondaryActions component
4500
4556
  */
4501
- const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction = false, }) => {
4557
+ const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction = false, groupActions = false, }) => {
4502
4558
  const enabledActions = react.useMemo(() => actions.filter(action => action.hidden === false || action.hidden === undefined), [actions]);
4503
- // If we need to render a "More Menu" because we have too many actions:
4504
- if (enabledActions.length > 2 || (hasPrimaryAction && enabledActions.length > 1)) {
4559
+ // If there are no enabled actions, don't render anything
4560
+ if (enabledActions.length === 0) {
4561
+ return null;
4562
+ }
4563
+ // If we need to render a "More Menu" because we have too many actions or grouping is requested:
4564
+ if (groupActions || enabledActions.length > 2 || (hasPrimaryAction && enabledActions.length > 1)) {
4505
4565
  // Separate them into danger vs. other
4506
4566
  const [dangerActions, otherActions] = enabledActions.reduce(([danger, others], action) => {
4507
4567
  if (action.variant === "secondary-danger") {
@@ -4561,7 +4621,7 @@ const PageHeaderTitle = ({ title, dataTestId }) => {
4561
4621
  * @param {PageHeaderProps} props - The props for the PageHeader component
4562
4622
  * @returns {ReactElement} PageHeader component
4563
4623
  */
4564
- const PageHeader = ({ className, dataTestId, secondaryActions, showLoading = false, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon = "QuestionMarkCircle", kpiMetrics, tagTooltipLabel, primaryAction, }) => {
4624
+ const PageHeader = ({ className, dataTestId, showLoading = false, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon = "QuestionMarkCircle", tagTooltipLabel, ...discriminatedProps }) => {
4565
4625
  const tagRenderer = react.useMemo(() => {
4566
4626
  if (tagLabel === undefined || tagLabel === "" || showLoading) {
4567
4627
  return null;
@@ -4572,10 +4632,14 @@ const PageHeader = ({ className, dataTestId, secondaryActions, showLoading = fal
4572
4632
  return (jsxRuntime.jsxs("div", { className: cvaPageHeaderContainer({
4573
4633
  className,
4574
4634
  withBorder: tabsList === undefined,
4575
- }), "data-testid": dataTestId, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsxRuntime.jsx(reactRouter.Link, { to: backTo, children: jsxRuntime.jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "medium", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsxRuntime.jsx(PageHeaderTitle, { dataTestId: dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxRuntime.jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
4635
+ }), "data-testid": dataTestId, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsxRuntime.jsx(reactRouter.Link, { to: backTo, children: jsxRuntime.jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsxRuntime.jsx(PageHeaderTitle, { dataTestId: dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxRuntime.jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsxRuntime.jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
4576
4636
  name: descriptionIcon,
4577
4637
  dataTestId: "page-header-description-icon",
4578
- }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxRuntime.jsxs("div", { className: "ml-auto flex gap-2", children: [kpiMetrics ? jsxRuntime.jsx(PageHeaderKpiMetrics, { kpiMetrics: kpiMetrics }) : null, Array.isArray(secondaryActions) ? (jsxRuntime.jsx(PageHeaderSecondaryActions, { actions: secondaryActions, hasPrimaryAction: !!primaryAction })) : secondaryActions !== null && secondaryActions !== undefined ? (secondaryActions) : null, primaryAction !== undefined && (primaryAction.hidden === false || primaryAction.hidden === undefined) ? (jsxRuntime.jsx(Tooltip, { disabled: primaryAction.tooltipLabel === undefined || primaryAction.tooltipLabel === "", label: primaryAction.tooltipLabel, children: jsxRuntime.jsx(Button, { dataTestId: primaryAction.dataTestId, disabled: primaryAction.disabled, loading: primaryAction.loading, onClick: () => primaryAction.actionCallback?.(), prefix: primaryAction.prefixIconName !== undefined ? (jsxRuntime.jsx(Icon, { name: primaryAction.prefixIconName, size: "small" })) : undefined, size: "medium", variant: primaryAction.variant, children: primaryAction.actionText }) })) : null] })] }), tabsList] }));
4638
+ }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxRuntime.jsxs("div", { className: "ml-auto flex gap-2", children: [discriminatedProps.accessoryType === "kpi-metrics" ? (jsxRuntime.jsx(PageHeaderKpiMetrics, { kpiMetrics: discriminatedProps.kpiMetrics })) : null, discriminatedProps.accessoryType === "actions" ? (Array.isArray(discriminatedProps.secondaryActions) ? (jsxRuntime.jsx(PageHeaderSecondaryActions, { actions: discriminatedProps.secondaryActions, groupActions: discriminatedProps.groupSecondaryActions ?? false, hasPrimaryAction: !!discriminatedProps.primaryAction })) : discriminatedProps.secondaryActions !== null && discriminatedProps.secondaryActions !== undefined ? (discriminatedProps.secondaryActions) : null) : null, discriminatedProps.accessoryType === "actions" &&
4639
+ discriminatedProps.primaryAction !== undefined &&
4640
+ (discriminatedProps.primaryAction.hidden === false ||
4641
+ discriminatedProps.primaryAction.hidden === undefined) ? (jsxRuntime.jsx(Tooltip, { disabled: discriminatedProps.primaryAction.tooltipLabel === undefined ||
4642
+ discriminatedProps.primaryAction.tooltipLabel === "", label: discriminatedProps.primaryAction.tooltipLabel, children: jsxRuntime.jsx(Button, { dataTestId: discriminatedProps.primaryAction.dataTestId, disabled: discriminatedProps.primaryAction.disabled, loading: discriminatedProps.primaryAction.loading, onClick: () => discriminatedProps.primaryAction?.actionCallback?.(), prefix: discriminatedProps.primaryAction.prefixIconName !== undefined ? (jsxRuntime.jsx(Icon, { name: discriminatedProps.primaryAction.prefixIconName, size: "small" })) : undefined, size: "small", variant: discriminatedProps.primaryAction.variant, children: discriminatedProps.primaryAction.actionText }) })) : null] })] }), tabsList] }));
4579
4643
  };
4580
4644
 
4581
4645
  const cvaPagination = cssClassVarianceUtilities.cvaMerge(["flex", "items-center", "gap-1"]);