@trackunit/react-components 1.10.42 → 1.10.43

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.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
- import { useRef, useMemo, useEffect, useState, useCallback, createElement, useReducer, forwardRef, Fragment, memo, Children, isValidElement, cloneElement, createContext, useContext, useLayoutEffect } from 'react';
2
+ import { useRef, useMemo, useEffect, useState, useCallback, createElement, useReducer, useLayoutEffect, forwardRef, Fragment, memo, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
3
3
  import { objectKeys, uuidv4, objectEntries, objectValues, nonNullable } from '@trackunit/shared-utils';
4
4
  import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, color } from '@trackunit/ui-design-tokens';
5
5
  import { iconNames } from '@trackunit/ui-icons';
@@ -14,7 +14,7 @@ import { isEqual, omit } from 'es-toolkit';
14
14
  import { useVirtualizer } from '@tanstack/react-virtual';
15
15
  import { useDebounceCallback, useCopyToClipboard } from 'usehooks-ts';
16
16
  import { Link, useBlocker } from '@tanstack/react-router';
17
- import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, FloatingPortal, useMergeRefs, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
17
+ import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, FloatingPortal, useMergeRefs as useMergeRefs$1, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
18
18
  import { twMerge } from 'tailwind-merge';
19
19
  import { HelmetProvider, Helmet } from 'react-helmet-async';
20
20
  import { Trigger, Content, List as List$1, Root } from '@radix-ui/react-tabs';
@@ -1332,34 +1332,40 @@ const useElevatedState = (initialState, customState) => {
1332
1332
  * @returns {object} The object containing the onMouseEnter, onMouseLeave and hovering props
1333
1333
  */
1334
1334
  const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
1335
- const [hovering, setHovering] = useState(false);
1336
- const [debouncedHovering, setDebouncedHovering] = useState(false);
1337
- useEffect(() => {
1338
- if (!debounced) {
1339
- setDebouncedHovering(hovering);
1340
- return undefined;
1341
- }
1342
- const shouldDebounce = direction === "both" || (direction === "in" && hovering) || (direction === "out" && !hovering);
1343
- if (shouldDebounce) {
1344
- const timer = setTimeout(() => {
1345
- setDebouncedHovering(hovering);
1346
- }, delay);
1347
- return () => clearTimeout(timer);
1348
- }
1349
- setDebouncedHovering(hovering);
1350
- return undefined;
1351
- }, [debounced, direction, delay, hovering]);
1335
+ const [isHovering, setIsHovering] = useState(false);
1336
+ const [debouncedIsHovering, setDebouncedIsHovering] = useState(false);
1352
1337
  const onMouseEnter = useCallback(() => {
1353
- setHovering(true);
1338
+ setIsHovering(true);
1354
1339
  }, []);
1355
1340
  const onMouseLeave = useCallback(() => {
1356
- setHovering(false);
1341
+ setIsHovering(false);
1357
1342
  }, []);
1343
+ // Determine if the current transition should be debounced based on direction
1344
+ const isTransitionDebounced = debounced && (direction === "both" || (direction === "in" && isHovering) || (direction === "out" && !isHovering));
1345
+ // Sync debouncedIsHovering immediately for non-debounced transitions
1346
+ useLayoutEffect(() => {
1347
+ if (!debounced || isTransitionDebounced) {
1348
+ return undefined;
1349
+ }
1350
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Synchronizing derived state for directional debouncing
1351
+ setDebouncedIsHovering(isHovering);
1352
+ }, [debounced, isTransitionDebounced, isHovering]);
1353
+ // Apply delay for debounced transitions
1354
+ useEffect(() => {
1355
+ if (!isTransitionDebounced) {
1356
+ return undefined;
1357
+ }
1358
+ const timer = setTimeout(() => {
1359
+ setDebouncedIsHovering(isHovering);
1360
+ }, delay);
1361
+ return () => clearTimeout(timer);
1362
+ }, [isTransitionDebounced, delay, isHovering]);
1363
+ const value = debounced ? (isTransitionDebounced ? debouncedIsHovering : isHovering) : isHovering;
1358
1364
  return useMemo(() => ({
1359
1365
  onMouseEnter,
1360
1366
  onMouseLeave,
1361
- hovering: debouncedHovering,
1362
- }), [onMouseEnter, onMouseLeave, debouncedHovering]);
1367
+ hovering: value,
1368
+ }), [onMouseEnter, onMouseLeave, value]);
1363
1369
  };
1364
1370
 
1365
1371
  const OVERSCAN = 10;
@@ -1389,10 +1395,13 @@ const DEFAULT_ROW_HEIGHT = 50;
1389
1395
  * });
1390
1396
  */
1391
1397
  const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, }) => {
1398
+ "use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
1392
1399
  const handleChange = useCallback((virtualizer) => {
1393
1400
  onChange?.(virtualizer);
1394
1401
  }, [onChange]);
1395
1402
  const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
1403
+ //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
1404
+ // eslint-disable-next-line react-hooks/incompatible-library
1396
1405
  const rowVirtualizer = useVirtualizer({
1397
1406
  count,
1398
1407
  getScrollElement: () => scrollElementRef.current,
@@ -1441,8 +1450,10 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
1441
1450
  */
1442
1451
  const useIsFirstRender = () => {
1443
1452
  const [isFirstRender, setIsFirstRender] = useState(true);
1444
- useEffect(() => {
1445
- setIsFirstRender(false);
1453
+ useLayoutEffect(() => {
1454
+ queueMicrotask(() => {
1455
+ setIsFirstRender(false);
1456
+ });
1446
1457
  }, []);
1447
1458
  return isFirstRender;
1448
1459
  };
@@ -1514,27 +1525,51 @@ const useIsTextTruncated = (text, { skip = false } = {}) => {
1514
1525
  };
1515
1526
 
1516
1527
  /**
1517
- * Shared measurement logic used by both useMeasure and useMeasureElement.
1518
- * This hook observes an element and measures its geometry.
1528
+ * Custom hook to measure the geometry of an element using a callback ref.
1529
+ * Measures the size and position of the element relative to the viewport.
1530
+ *
1531
+ * @template TElement extends HTMLElement
1532
+ * @returns {UseMeasureResult<HTMLElement>} An object containing `geometry`, `ref` callback, and `element`.
1533
+ * @example
1534
+ * ```tsx
1535
+ * const { geometry, ref } = useMeasure();
1536
+ * return <div ref={ref}>Width: {geometry.width}</div>;
1537
+ * ```
1519
1538
  */
1520
- const useMeasureShared = (element, { skip = false, onChange } = {}) => {
1539
+ const useMeasure = ({ skip = false, onChange, } = {}) => {
1540
+ const [element, setElement] = useState(null);
1521
1541
  const [geometry, setGeometry] = useState(undefined);
1522
1542
  const observerRef = useRef(null);
1523
1543
  const onChangeRef = useRef(onChange);
1524
1544
  const isInitialRender = useRef(true);
1525
- onChangeRef.current = onChange;
1545
+ // Callback ref to track the element
1546
+ const ref = useCallback((node) => {
1547
+ setElement(node);
1548
+ }, []);
1526
1549
  const disconnectObserver = () => {
1527
1550
  if (observerRef.current !== null) {
1528
1551
  observerRef.current.disconnect();
1529
1552
  observerRef.current = null;
1530
1553
  }
1531
1554
  };
1555
+ useEffect(() => {
1556
+ onChangeRef.current = onChange;
1557
+ }, [onChange]);
1532
1558
  useEffect(() => {
1533
1559
  if (skip || element === null) {
1534
1560
  return;
1535
1561
  }
1536
1562
  const updateGeometry = (rect) => {
1537
- const newGeometry = omit(rect, ["toJSON"]);
1563
+ const newGeometry = {
1564
+ width: rect.width,
1565
+ height: rect.height,
1566
+ top: rect.top,
1567
+ bottom: rect.bottom,
1568
+ left: rect.left,
1569
+ right: rect.right,
1570
+ x: rect.x,
1571
+ y: rect.y,
1572
+ };
1538
1573
  setGeometry(prevGeometry => {
1539
1574
  if (isEqual(prevGeometry, newGeometry)) {
1540
1575
  return prevGeometry;
@@ -1560,48 +1595,72 @@ const useMeasureShared = (element, { skip = false, onChange } = {}) => {
1560
1595
  updateGeometry(initialRect);
1561
1596
  return disconnectObserver;
1562
1597
  }, [element, skip]);
1563
- return geometry;
1598
+ return useMemo(() => ({ geometry, ref, element }), [geometry, ref, element]);
1564
1599
  };
1565
1600
 
1601
+ // This hook is _heavily_ inspired by floating-ui's useMergeRefs
1566
1602
  /**
1567
- * Custom hook to measure the geometry of an element using a callback ref.
1568
- * Measures the size and position of the element relative to the viewport.
1603
+ * Merges an array of refs into a single memoized callback ref or `null`.
1604
+ * Useful when you need to attach multiple refs to the same element,
1605
+ * such as when composing multiple hooks that each need a ref.
1569
1606
  *
1570
- * @returns {UseMeasureResult<HTMLElement>} An object containing `geometry`, `ref` callback, and `element`.
1571
- * @template TElement extends HTMLElement
1607
+ * @template TInstance - The type of the element instance
1608
+ * @param {ReadonlyArray<MergeableRef<TInstance> | undefined>} refs - Array of refs to merge (can be RefObjects, RefCallbacks, or undefined)
1609
+ * @returns {null | RefCallback<TInstance>} A single ref callback that will update all provided refs, or null if all refs are null
1572
1610
  * @example
1573
1611
  * ```tsx
1574
- * const { geometry, ref } = useMeasure();
1575
- * return <div ref={ref}>Width: {geometry.width}</div>;
1576
- * ```
1577
- */
1578
- const useMeasure = ({ skip = false, onChange, } = {}) => {
1579
- const [element, setElement] = useState(null);
1580
- // Callback ref to track the element
1581
- const ref = useCallback((node) => {
1582
- setElement(node);
1583
- }, []);
1584
- const geometry = useMeasureShared(element, { skip, onChange });
1585
- return { geometry, ref, element };
1586
- };
1587
-
1588
- /**
1589
- * Custom hook to measure the geometry of an element from a RefObject.
1590
- * Use this when you already have a ref (e.g., from useRef or for composition with other hooks).
1591
- * Measures the size and position of the element relative to the viewport.
1612
+ * const { ref: measureRef } = useMeasure();
1613
+ * const { ref: scrollRef } = useScrollDetection();
1614
+ * const mergedRef = useMergeRefs([measureRef, scrollRef]);
1592
1615
  *
1593
- * @param ref - RefObject pointing to the element to measure
1594
- * @returns {Geometry} The geometry of the element
1595
- * @example
1596
- * ```tsx
1597
- * const ref = useRef<HTMLDivElement>(null);
1598
- * const geometry = useMeasureElement(ref);
1599
- * return <div ref={ref}>Width: {geometry.width}</div>;
1616
+ * return <div ref={mergedRef}>Content</div>;
1600
1617
  * ```
1601
1618
  */
1602
- const useMeasureElement = (ref, { skip = false, onChange } = {}) => {
1603
- return useMeasureShared(ref.current, { skip, onChange });
1604
- };
1619
+ function useMergeRefs(refs) {
1620
+ const cleanupRef = useRef(undefined);
1621
+ const refsRef = useRef(refs);
1622
+ useEffect(() => {
1623
+ refsRef.current = refs;
1624
+ }, [refs]);
1625
+ const refEffect = useCallback((instance) => {
1626
+ const cleanups = refsRef.current.map(ref => {
1627
+ if (ref === null || ref === undefined) {
1628
+ return;
1629
+ }
1630
+ if (typeof ref === "function") {
1631
+ const refCallback = ref;
1632
+ const refCleanup = refCallback(instance);
1633
+ return typeof refCleanup === "function"
1634
+ ? refCleanup
1635
+ : () => {
1636
+ refCallback(null);
1637
+ };
1638
+ }
1639
+ ref.current = instance;
1640
+ return () => {
1641
+ ref.current = null;
1642
+ };
1643
+ });
1644
+ return () => {
1645
+ cleanups.forEach(refCleanup => refCleanup?.());
1646
+ };
1647
+ }, []);
1648
+ return useMemo(() => {
1649
+ // eslint-disable-next-line react-hooks/refs
1650
+ if (refsRef.current.every(ref => ref === null)) {
1651
+ return null;
1652
+ }
1653
+ return (value) => {
1654
+ if (cleanupRef.current) {
1655
+ cleanupRef.current();
1656
+ cleanupRef.current = undefined;
1657
+ }
1658
+ if (value !== null) {
1659
+ cleanupRef.current = refEffect(value);
1660
+ }
1661
+ };
1662
+ }, [refEffect]);
1663
+ }
1605
1664
 
1606
1665
  /**
1607
1666
  * Hook that returns true if any modifier key (Ctrl, Alt, Shift, Meta/Cmd) is pressed
@@ -1641,22 +1700,24 @@ const useRelayPagination = ({ onReset, pageSize } = { pageSize: defaultPageSize
1641
1700
  const [variables, setVariables] = useState({ first: pageSize });
1642
1701
  const [pageInfo, setPageInfo] = useState();
1643
1702
  const [isLoading, setIsLoading] = useState(true);
1703
+ // Destructure pageInfo properties early to avoid depending on the entire object
1704
+ const { hasNextPage, endCursor, hasPreviousPage, startCursor } = pageInfo ?? {};
1644
1705
  const nextPage = useCallback(() => {
1645
- if (pageInfo?.hasNextPage === true) {
1706
+ if (hasNextPage === true) {
1646
1707
  setVariables({
1647
- after: pageInfo.endCursor === null ? undefined : pageInfo.endCursor,
1708
+ after: endCursor === null ? undefined : endCursor,
1648
1709
  first: pageSize,
1649
1710
  });
1650
1711
  }
1651
- }, [pageInfo?.endCursor, pageInfo?.hasNextPage, pageSize]);
1712
+ }, [hasNextPage, endCursor, pageSize]);
1652
1713
  const previousPage = useCallback(() => {
1653
- if (pageInfo?.hasPreviousPage === true) {
1714
+ if (hasPreviousPage === true) {
1654
1715
  setVariables({
1655
- before: pageInfo.startCursor === null ? undefined : pageInfo.startCursor,
1716
+ before: startCursor === null ? undefined : startCursor,
1656
1717
  last: pageSize,
1657
1718
  });
1658
1719
  }
1659
- }, [pageInfo?.hasPreviousPage, pageInfo?.startCursor, pageSize]);
1720
+ }, [hasPreviousPage, startCursor, pageSize]);
1660
1721
  const reset = useCallback(() => {
1661
1722
  setVariables({
1662
1723
  last: undefined,
@@ -1871,21 +1932,31 @@ const useScrollBlock = (scrollContainer = typeof document !== "undefined" ? docu
1871
1932
  const SCROLL_DEBOUNCE_TIME = 50;
1872
1933
  /**
1873
1934
  * Hook for detecting scroll values in horizontal or vertical direction.
1935
+ * Returns a ref callback to attach to the element you want to observe.
1874
1936
  *
1875
- * @param {useRef} elementRef - Ref hook holding the element that needs to be observed during scrolling
1876
1937
  * @param {object} options - Options object containing direction, onScrollStateChange callback, and skip
1877
- * @returns {object} An object containing if the element is scrollable, is at the beginning, is at the end, and its current scroll position.
1938
+ * @returns {object} An object containing ref callback, element, and scroll state (isScrollable, isAtBeginning, isAtEnd, scrollPosition)
1939
+ * @template TElement extends HTMLElement
1940
+ * @example
1941
+ * ```tsx
1942
+ * const { ref, isScrollable, isAtBeginning } = useScrollDetection({ direction: "horizontal" });
1943
+ * return <div ref={ref}>Scrollable content</div>;
1944
+ * ```
1878
1945
  */
1879
- const useScrollDetection = (elementRef, options) => {
1946
+ const useScrollDetection = (options) => {
1880
1947
  const { direction = "vertical", onScrollStateChange, skip = false } = options ?? {};
1948
+ const [element, setElement] = useState(null);
1881
1949
  const [isScrollable, setIsScrollable] = useState(false);
1882
1950
  const [isAtBeginning, setIsAtBeginning] = useState(true);
1883
1951
  const [isAtEnd, setIsAtEnd] = useState(false);
1884
1952
  const [scrollPosition, setScrollPosition] = useState({ start: 0, end: 0 });
1885
1953
  const observerRef = useRef(null);
1886
1954
  const isFirstRender = useIsFirstRender();
1955
+ // Callback ref to track the element
1956
+ const ref = useCallback((node) => {
1957
+ setElement(node);
1958
+ }, []);
1887
1959
  const checkScrollable = useCallback(() => {
1888
- const element = elementRef?.current;
1889
1960
  if (!element) {
1890
1961
  return;
1891
1962
  }
@@ -1899,9 +1970,8 @@ const useScrollDetection = (elementRef, options) => {
1899
1970
  }
1900
1971
  return prev;
1901
1972
  });
1902
- }, [elementRef, direction]);
1973
+ }, [element, direction]);
1903
1974
  const checkScrollPosition = useCallback(() => {
1904
- const element = elementRef?.current;
1905
1975
  if (!element) {
1906
1976
  return;
1907
1977
  }
@@ -1923,8 +1993,16 @@ const useScrollDetection = (elementRef, options) => {
1923
1993
  setIsAtEnd(newIsAtEnd);
1924
1994
  setScrollPosition(newScrollPosition);
1925
1995
  }
1926
- }, [elementRef, direction]);
1996
+ }, [element, direction]);
1927
1997
  const debouncedCheckScrollPosition = useDebounceCallback(checkScrollPosition, SCROLL_DEBOUNCE_TIME);
1998
+ const checkScrollableRef = useRef(checkScrollable);
1999
+ useEffect(() => {
2000
+ checkScrollableRef.current = checkScrollable;
2001
+ }, [checkScrollable]);
2002
+ const checkScrollPositionRef = useRef(checkScrollPosition);
2003
+ useEffect(() => {
2004
+ checkScrollPositionRef.current = checkScrollPosition;
2005
+ }, [checkScrollPosition]);
1928
2006
  // Notify about state changes whenever any state value changes
1929
2007
  useEffect(() => {
1930
2008
  if (isFirstRender) {
@@ -1938,19 +2016,15 @@ const useScrollDetection = (elementRef, options) => {
1938
2016
  });
1939
2017
  }, [isScrollable, isAtBeginning, isAtEnd, scrollPosition, onScrollStateChange, isFirstRender]);
1940
2018
  useEffect(() => {
1941
- if (skip) {
1942
- return;
1943
- }
1944
- const element = elementRef?.current;
1945
- if (!element) {
2019
+ if (skip || !element) {
1946
2020
  return;
1947
2021
  }
1948
2022
  // Initial checks
1949
- checkScrollable();
1950
- checkScrollPosition();
2023
+ checkScrollableRef.current();
2024
+ checkScrollPositionRef.current();
1951
2025
  observerRef.current = new ResizeObserver(() => {
1952
- checkScrollable();
1953
- checkScrollPosition();
2026
+ checkScrollableRef.current();
2027
+ checkScrollPositionRef.current();
1954
2028
  });
1955
2029
  observerRef.current.observe(element);
1956
2030
  element.addEventListener("scroll", debouncedCheckScrollPosition);
@@ -1960,8 +2034,8 @@ const useScrollDetection = (elementRef, options) => {
1960
2034
  }
1961
2035
  element.removeEventListener("scroll", debouncedCheckScrollPosition);
1962
2036
  };
1963
- }, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition, skip]);
1964
- return useMemo(() => ({ isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
2037
+ }, [element, debouncedCheckScrollPosition, skip]);
2038
+ return useMemo(() => ({ ref, element, isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [ref, element, isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
1965
2039
  };
1966
2040
 
1967
2041
  /**
@@ -2014,24 +2088,28 @@ const useViewportBreakpoints = (options = {}) => {
2014
2088
  }), { ...defaultBreakpointState });
2015
2089
  setViewportSize(newViewportSize);
2016
2090
  }, []);
2091
+ const updateViewportSizeRef = useRef(updateViewportSize);
2092
+ useEffect(() => {
2093
+ updateViewportSizeRef.current = updateViewportSize;
2094
+ }, [updateViewportSize]);
2017
2095
  useEffect(() => {
2018
2096
  if (skip) {
2019
2097
  return;
2020
2098
  }
2021
2099
  // Initial check
2022
- updateViewportSize();
2100
+ updateViewportSizeRef.current();
2023
2101
  // Set up listeners for each breakpoint
2024
2102
  const mediaQueryLists = objectEntries(themeScreenSizeAsNumber).map(([_, minWidth]) => window.matchMedia(`(min-width: ${minWidth}px)`));
2025
2103
  mediaQueryLists.forEach(mql => {
2026
- mql.addEventListener("change", updateViewportSize);
2104
+ mql.addEventListener("change", updateViewportSizeRef.current);
2027
2105
  });
2028
2106
  // Cleanup
2029
2107
  return () => {
2030
2108
  mediaQueryLists.forEach(mql => {
2031
- mql.removeEventListener("change", updateViewportSize);
2109
+ mql.removeEventListener("change", updateViewportSizeRef.current);
2032
2110
  });
2033
2111
  };
2034
- }, [updateViewportSize, skip]);
2112
+ }, [skip]);
2035
2113
  return viewportSize;
2036
2114
  };
2037
2115
 
@@ -2049,11 +2127,15 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
2049
2127
  setFocused(hasFocus());
2050
2128
  onBlur?.();
2051
2129
  }, [onBlur]);
2130
+ const setFocusedRef = useRef(setFocused);
2131
+ useEffect(() => {
2132
+ setFocusedRef.current = setFocused;
2133
+ }, [setFocused]);
2052
2134
  useEffect(() => {
2053
2135
  if (skip) {
2054
2136
  return;
2055
2137
  }
2056
- setFocused(hasFocus()); // Focus for additional renders
2138
+ setFocusedRef.current(hasFocus());
2057
2139
  window.addEventListener("focus", onFocusInternal);
2058
2140
  window.addEventListener("blur", onBlurInternal);
2059
2141
  return () => {
@@ -2967,16 +3049,20 @@ const OverflowIndicator = ({ className, dataTestId, direction, onClickScroll, })
2967
3049
  };
2968
3050
 
2969
3051
  /**
2970
- * Container for displaying components in a horizontal layout with overflow detection
3052
+ * Container for displaying components in a horizontal layout with overflow detection.
3053
+ * Provides visual indicators when content overflows and can be scrolled.
2971
3054
  *
2972
- * @param {HorizontalOverflowScrollerProps} props - The props for the component
2973
- * @returns {Element} HorizontalOverflowScroller component
3055
+ * @param props - Component properties
3056
+ * @param props.children - The content to display in the horizontal scroller
3057
+ * @param props.className - Optional CSS class name for styling
3058
+ * @param props.dataTestId - Optional test ID for testing purposes
3059
+ * @param props.onScrollStateChange - Optional callback fired when scroll state changes
3060
+ * @returns {ReactElement} A horizontal overflow scroller component with visual indicators
2974
3061
  */
2975
3062
  const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollStateChange, }) => {
2976
3063
  const childrenArray = Children.toArray(children);
2977
- const containerRef = useRef(null);
2978
- const containerGeometry = useMeasureElement(containerRef);
2979
- const { isScrollable, isAtBeginning, isAtEnd } = useScrollDetection(containerRef, {
3064
+ const { geometry: containerGeometry, ref: measureRef, element } = useMeasure();
3065
+ const { ref: scrollRef, isScrollable, isAtBeginning, isAtEnd, } = useScrollDetection({
2980
3066
  direction: "horizontal",
2981
3067
  onScrollStateChange: onScrollStateChange
2982
3068
  ? (state) => onScrollStateChange({
@@ -2986,23 +3072,24 @@ const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollS
2986
3072
  })
2987
3073
  : undefined,
2988
3074
  });
3075
+ const mergedRef = useMergeRefs([measureRef, scrollRef]);
2989
3076
  const handleScrollLeft = () => {
2990
- if (!containerRef.current || containerGeometry?.width === undefined)
3077
+ if (!element || containerGeometry?.width === undefined)
2991
3078
  return;
2992
- containerRef.current.scrollBy({
3079
+ element.scrollBy({
2993
3080
  left: -containerGeometry.width,
2994
3081
  behavior: "smooth",
2995
3082
  });
2996
3083
  };
2997
3084
  const handleScrollRight = () => {
2998
- if (!containerRef.current || containerGeometry?.width === undefined)
3085
+ if (!element || containerGeometry?.width === undefined)
2999
3086
  return;
3000
- containerRef.current.scrollBy({
3087
+ element.scrollBy({
3001
3088
  left: containerGeometry.width,
3002
3089
  behavior: "smooth",
3003
3090
  });
3004
3091
  };
3005
- return (jsxs(ZStack, { className: cvaHorizontalOverflowScrollerAndIndicatorsContainer({ className }), children: [jsx("div", { className: cvaHorizontalOverflowScroller(), "data-testid": dataTestId, ref: containerRef, children: childrenArray.map((child, index) => (jsx(Fragment, { children: child }, index))) }), isScrollable && !isAtBeginning ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-left-indicator`, direction: "left", onClickScroll: handleScrollLeft })) : null, isScrollable && !isAtEnd ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-right-indicator`, direction: "right", onClickScroll: handleScrollRight })) : null] }));
3092
+ return (jsxs(ZStack, { className: cvaHorizontalOverflowScrollerAndIndicatorsContainer({ className }), children: [jsx("div", { className: cvaHorizontalOverflowScroller(), "data-testid": dataTestId, ref: mergedRef, children: childrenArray.map((child, index) => (jsx(Fragment, { children: child }, index))) }), isScrollable && !isAtBeginning ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-left-indicator`, direction: "left", onClickScroll: handleScrollLeft })) : null, isScrollable && !isAtEnd ? (jsx(OverflowIndicator, { dataTestId: `${dataTestId}-right-indicator`, direction: "right", onClickScroll: handleScrollRight })) : null] }));
3006
3093
  };
3007
3094
 
3008
3095
  const PADDING = 12;
@@ -3282,7 +3369,7 @@ const cvaPopoverTitleText = cvaMerge(["flex-1", "text-neutral-500"]);
3282
3369
  */
3283
3370
  const PopoverContent = function PopoverContent({ className, dataTestId, children, portalId, ref: propRef, ...props }) {
3284
3371
  const { context: floatingContext, customProps, ...context } = usePopoverContext();
3285
- const ref = useMergeRefs([context.refs.setFloating, propRef]);
3372
+ const ref = useMergeRefs$1([context.refs.setFloating, propRef]);
3286
3373
  return (jsx(Portal, { id: portalId, children: context.isOpen === true ? (jsx(FloatingFocusManager, { closeOnFocusOut: false, context: floatingContext, guards: true, modal: context.isModal, order: ["reference", "content"], returnFocus: true, children: jsx("div", { "aria-describedby": context.descriptionId, "aria-labelledby": context.labelId, className: cvaPopoverContainer({ className: className ?? customProps.className }), "data-testid": dataTestId ?? customProps.dataTestId ?? "popover-content", ref: ref, style: {
3287
3374
  position: context.strategy,
3288
3375
  top: context.y,
@@ -3310,14 +3397,16 @@ const PopoverTitle = ({ children, action, divider = false, className, dataTestId
3310
3397
  */
3311
3398
  const PopoverTrigger = function PopoverTrigger({ children, renderButton = false, ref: propRef, ...props }) {
3312
3399
  const context = usePopoverContext();
3313
- const ref = useMergeRefs([context.refs.setReference, propRef]);
3400
+ const ref = useMergeRefs$1([context.refs.setReference, propRef]);
3314
3401
  if (!renderButton && isValidElement(children)) {
3315
- return cloneElement(children, context.getReferenceProps({
3316
- ref,
3402
+ const referenceProps = context.getReferenceProps({
3317
3403
  ...props,
3318
3404
  ...children.props,
3319
3405
  "data-state": context.isOpen === true ? "open" : "closed",
3320
- }));
3406
+ });
3407
+ const cloneProps = { ...referenceProps };
3408
+ cloneProps.ref = ref;
3409
+ return cloneElement(children, cloneProps);
3321
3410
  }
3322
3411
  return (jsx(Button, { "data-state": context.isOpen === true ? "open" : "closed", ref: ref, type: "button", ...context.getReferenceProps(props), children: children }));
3323
3412
  };
@@ -4131,7 +4220,10 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
4131
4220
  const isAtTop = useMemo(() => virtualizer.scrollOffset === 0, [virtualizer.scrollOffset]);
4132
4221
  // totalSize must be out from from useMemo since otherwise we'll not be able to have the the totalSize as a dependency for the contentFillsContainer
4133
4222
  const totalSize = virtualizer.getTotalSize();
4134
- const contentFillsContainer = useMemo(() => (containerRef.current ? totalSize >= containerRef.current.clientHeight : false), [totalSize]);
4223
+ const contentFillsContainer = useMemo(() => {
4224
+ // eslint-disable-next-line react-hooks/refs
4225
+ return containerRef.current ? totalSize >= containerRef.current.clientHeight : false;
4226
+ }, [totalSize]);
4135
4227
  return {
4136
4228
  ...virtualizer,
4137
4229
  containerRef,
@@ -4797,12 +4889,20 @@ const Pagination = ({ previousPage, nextPage, canPreviousPage = false, canNextPa
4797
4889
  if (!loading && pageCount === undefined) {
4798
4890
  throw Error("Pagination should be provided with a pageCount");
4799
4891
  }
4892
+ const setCurrentPageRef = useRef(setCurrentPage);
4893
+ useEffect(() => {
4894
+ setCurrentPageRef.current = setCurrentPage;
4895
+ }, [setCurrentPage]);
4896
+ const setPageRef = useRef(setPage);
4897
+ useEffect(() => {
4898
+ setPageRef.current = setPage;
4899
+ }, [setPage]);
4800
4900
  useEffect(() => {
4801
4901
  if (pageIndex !== undefined && (isNaN(pageIndex) || pageIndex < 0)) {
4802
- setPage(pageIndex);
4803
- setCurrentPage(String(pageIndex + 1));
4902
+ setPageRef.current(pageIndex);
4903
+ setCurrentPageRef.current(String(pageIndex + 1));
4804
4904
  }
4805
- }, [pageIndex, setPage, setCurrentPage, pageCount]);
4905
+ }, [pageIndex, pageCount]);
4806
4906
  const handlePageChange = useCallback((action) => {
4807
4907
  const from = page;
4808
4908
  let to = from;
@@ -5302,17 +5402,25 @@ const cvaToggleItemContent = cvaMerge([], {
5302
5402
  */
5303
5403
  const ToggleGroup = ({ list, selected, setSelected, onChange, disabled = false, size = "medium", isIconOnly = false, className, dataTestId, }) => {
5304
5404
  const [isMounted, setIsMounted] = useState(false);
5405
+ const [slidingLeft, setSlidingLeft] = useState(0);
5406
+ const [slidingWidth, setSlidingWidth] = useState(0);
5305
5407
  const buttonRefs = useRef([]);
5306
5408
  const selectedIndex = list.findIndex(item => item.id === selected);
5307
5409
  const validIndex = selectedIndex >= 0 ? selectedIndex : 0;
5308
- const containerPadding = 2; // p-0.5 = 2px
5309
- const gap = 4;
5310
- const slidingLeft = containerPadding +
5311
- buttonRefs.current.slice(0, validIndex).reduce((sum, ref) => sum + (ref?.offsetWidth ?? 0) + gap, 0);
5312
- const slidingWidth = buttonRefs.current[validIndex]?.offsetWidth ?? 0;
5313
- useEffect(() => {
5314
- setIsMounted(true);
5410
+ useLayoutEffect(() => {
5411
+ queueMicrotask(() => {
5412
+ setIsMounted(true);
5413
+ });
5315
5414
  }, []);
5415
+ useEffect(() => {
5416
+ const containerPadding = 2; // p-0.5 = 2px
5417
+ const gap = 4;
5418
+ const left = containerPadding +
5419
+ buttonRefs.current.slice(0, validIndex).reduce((sum, ref) => sum + (ref?.offsetWidth ?? 0) + gap, 0);
5420
+ const width = buttonRefs.current[validIndex]?.offsetWidth ?? 0;
5421
+ setSlidingLeft(left);
5422
+ setSlidingWidth(width);
5423
+ }, [validIndex]);
5316
5424
  return (jsx("div", { className: twMerge(cvaToggleGroup({ className }), cvaToggleGroupWithSlidingBackground({ isMounted })), "data-testid": dataTestId, style:
5317
5425
  // eslint-disable-next-line local-rules/no-typescript-assertion
5318
5426
  {
@@ -5487,4 +5595,4 @@ const cvaClickable = cvaMerge([
5487
5595
  },
5488
5596
  });
5489
5597
 
5490
- export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useMeasure, useMeasureElement, useModifierKey, useOverflowItems, usePopoverContext, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWindowActivity };
5598
+ export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWindowActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.10.42",
3
+ "version": "1.10.43",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -15,10 +15,10 @@
15
15
  "@floating-ui/react": "^0.26.25",
16
16
  "string-ts": "^2.0.0",
17
17
  "tailwind-merge": "^2.0.0",
18
- "@trackunit/ui-design-tokens": "1.7.72",
19
- "@trackunit/css-class-variance-utilities": "1.7.72",
20
- "@trackunit/shared-utils": "1.9.72",
21
- "@trackunit/ui-icons": "1.7.73",
18
+ "@trackunit/ui-design-tokens": "1.7.73",
19
+ "@trackunit/css-class-variance-utilities": "1.7.73",
20
+ "@trackunit/shared-utils": "1.9.73",
21
+ "@trackunit/ui-icons": "1.7.74",
22
22
  "@tanstack/react-router": "1.114.29",
23
23
  "es-toolkit": "^1.39.10",
24
24
  "@tanstack/react-virtual": "3.13.12"
@@ -13,9 +13,14 @@ export interface HorizontalOverflowScrollerProps extends CommonProps, Styleable
13
13
  }) => void;
14
14
  }
15
15
  /**
16
- * Container for displaying components in a horizontal layout with overflow detection
16
+ * Container for displaying components in a horizontal layout with overflow detection.
17
+ * Provides visual indicators when content overflows and can be scrolled.
17
18
  *
18
- * @param {HorizontalOverflowScrollerProps} props - The props for the component
19
- * @returns {Element} HorizontalOverflowScroller component
19
+ * @param props - Component properties
20
+ * @param props.children - The content to display in the horizontal scroller
21
+ * @param props.className - Optional CSS class name for styling
22
+ * @param props.dataTestId - Optional test ID for testing purposes
23
+ * @param props.onScrollStateChange - Optional callback fired when scroll state changes
24
+ * @returns {ReactElement} A horizontal overflow scroller component with visual indicators
20
25
  */
21
26
  export declare const HorizontalOverflowScroller: ({ className, dataTestId, children, onScrollStateChange, }: HorizontalOverflowScrollerProps) => ReactElement;