@trackunit/react-components 1.10.17 → 1.10.19

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
@@ -1325,109 +1325,6 @@ const useElevatedState = (initialState, customState) => {
1325
1325
  return react.useMemo(() => customState ?? [fallbackValue, fallbackSetter], [customState, fallbackValue, fallbackSetter]);
1326
1326
  };
1327
1327
 
1328
- const UNINITIALIZED = Symbol("UNINITIALIZED");
1329
- /**
1330
- * Custom hook to get the geometry of an element.
1331
- * Size and position of the element relative to the viewport.
1332
- */
1333
- const useGeometry = (ref, { skip = false, onChange } = {}) => {
1334
- const [geometry, setGeometry] = react.useState(() => {
1335
- const rect = ref.current?.getBoundingClientRect();
1336
- if (!rect) {
1337
- return {
1338
- width: 0,
1339
- height: 0,
1340
- top: 0,
1341
- bottom: 0,
1342
- left: 0,
1343
- right: 0,
1344
- x: 0,
1345
- y: 0,
1346
- };
1347
- }
1348
- return {
1349
- width: rect.width,
1350
- height: rect.height,
1351
- top: rect.top,
1352
- bottom: rect.bottom,
1353
- left: rect.left,
1354
- right: rect.right,
1355
- x: rect.x,
1356
- y: rect.y,
1357
- };
1358
- });
1359
- const resizeObserver = react.useRef(null);
1360
- const [element, setElement] = react.useState(ref.current);
1361
- const prevGeometry = react.useRef(UNINITIALIZED);
1362
- // Track changes to ref.current on every render
1363
- if (ref.current !== element) {
1364
- setElement(ref.current);
1365
- }
1366
- react.useEffect(() => {
1367
- if (skip || !element) {
1368
- return;
1369
- }
1370
- // Update geometry immediately when element changes
1371
- const elementRect = element.getBoundingClientRect();
1372
- const newGeometry = {
1373
- width: elementRect.width,
1374
- height: elementRect.height,
1375
- top: elementRect.top,
1376
- bottom: elementRect.bottom,
1377
- left: elementRect.left,
1378
- right: elementRect.right,
1379
- x: elementRect.x,
1380
- y: elementRect.y,
1381
- };
1382
- const prev = prevGeometry.current;
1383
- const hasChanged = prev === UNINITIALIZED ? false : !esToolkit.isEqual(newGeometry, prev);
1384
- if (!hasChanged && prev !== UNINITIALIZED) {
1385
- return;
1386
- }
1387
- setGeometry(newGeometry);
1388
- if (hasChanged && prev !== UNINITIALIZED) {
1389
- onChange?.(newGeometry);
1390
- }
1391
- prevGeometry.current = newGeometry;
1392
- const observe = () => {
1393
- if (!resizeObserver.current) {
1394
- resizeObserver.current = new ResizeObserver(entries => {
1395
- for (const entry of entries) {
1396
- const entryRect = entry.target.getBoundingClientRect();
1397
- const observedGeometry = {
1398
- width: entryRect.width,
1399
- height: entryRect.height,
1400
- top: entryRect.top,
1401
- bottom: entryRect.bottom,
1402
- left: entryRect.left,
1403
- right: entryRect.right,
1404
- x: entryRect.x,
1405
- y: entryRect.y,
1406
- };
1407
- const prevObserved = prevGeometry.current;
1408
- const hasObservedChanged = prevObserved === UNINITIALIZED ? false : !esToolkit.isEqual(observedGeometry, prevObserved);
1409
- if (hasObservedChanged || prevObserved === UNINITIALIZED) {
1410
- setGeometry(observedGeometry);
1411
- if (hasObservedChanged && prevObserved !== UNINITIALIZED) {
1412
- onChange?.(observedGeometry);
1413
- }
1414
- prevGeometry.current = observedGeometry;
1415
- }
1416
- }
1417
- });
1418
- }
1419
- resizeObserver.current.observe(element);
1420
- };
1421
- observe();
1422
- return () => {
1423
- if (resizeObserver.current) {
1424
- resizeObserver.current.disconnect();
1425
- }
1426
- };
1427
- }, [element, onChange, skip]);
1428
- return geometry;
1429
- };
1430
-
1431
1328
  /**
1432
1329
  * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
1433
1330
  * The boolean will be true if the element is being hovered, and false if it is not.
@@ -1618,6 +1515,96 @@ const useIsTextTruncated = (text, { skip = false } = {}) => {
1618
1515
  return react.useMemo(() => ({ ref, isTextTruncated }), [isTextTruncated]);
1619
1516
  };
1620
1517
 
1518
+ /**
1519
+ * Shared measurement logic used by both useMeasure and useMeasureElement.
1520
+ * This hook observes an element and measures its geometry.
1521
+ */
1522
+ const useMeasureShared = (element, { skip = false, onChange } = {}) => {
1523
+ const [geometry, setGeometry] = react.useState(undefined);
1524
+ const observerRef = react.useRef(null);
1525
+ const onChangeRef = react.useRef(onChange);
1526
+ const isInitialRender = react.useRef(true);
1527
+ onChangeRef.current = onChange;
1528
+ const disconnectObserver = () => {
1529
+ if (observerRef.current !== null) {
1530
+ observerRef.current.disconnect();
1531
+ observerRef.current = null;
1532
+ }
1533
+ };
1534
+ react.useEffect(() => {
1535
+ if (skip || element === null) {
1536
+ return;
1537
+ }
1538
+ const updateGeometry = (rect) => {
1539
+ const newGeometry = esToolkit.omit(rect, ["toJSON"]);
1540
+ setGeometry(prevGeometry => {
1541
+ if (esToolkit.isEqual(prevGeometry, newGeometry)) {
1542
+ return prevGeometry;
1543
+ }
1544
+ if (isInitialRender.current === false) {
1545
+ onChangeRef.current?.(newGeometry);
1546
+ }
1547
+ return newGeometry;
1548
+ });
1549
+ isInitialRender.current = false;
1550
+ };
1551
+ const handleResize = (entries) => {
1552
+ for (const entry of entries) {
1553
+ const rect = entry.target.getBoundingClientRect();
1554
+ updateGeometry(rect);
1555
+ }
1556
+ };
1557
+ disconnectObserver();
1558
+ const observer = new ResizeObserver(handleResize);
1559
+ observer.observe(element);
1560
+ observerRef.current = observer;
1561
+ const initialRect = element.getBoundingClientRect();
1562
+ updateGeometry(initialRect);
1563
+ return disconnectObserver;
1564
+ }, [element, skip]);
1565
+ return geometry;
1566
+ };
1567
+
1568
+ /**
1569
+ * Custom hook to measure the geometry of an element using a callback ref.
1570
+ * Measures the size and position of the element relative to the viewport.
1571
+ *
1572
+ * @returns {UseMeasureResult<HTMLElement>} An object containing `geometry`, `ref` callback, and `element`.
1573
+ * @template TElement extends HTMLElement
1574
+ * @example
1575
+ * ```tsx
1576
+ * const { geometry, ref } = useMeasure();
1577
+ * return <div ref={ref}>Width: {geometry.width}</div>;
1578
+ * ```
1579
+ */
1580
+ const useMeasure = ({ skip = false, onChange, } = {}) => {
1581
+ const [element, setElement] = react.useState(null);
1582
+ // Callback ref to track the element
1583
+ const ref = react.useCallback((node) => {
1584
+ setElement(node);
1585
+ }, []);
1586
+ const geometry = useMeasureShared(element, { skip, onChange });
1587
+ return { geometry, ref, element };
1588
+ };
1589
+
1590
+ /**
1591
+ * Custom hook to measure the geometry of an element from a RefObject.
1592
+ * Use this when you already have a ref (e.g., from useRef or for composition with other hooks).
1593
+ * Measures the size and position of the element relative to the viewport.
1594
+ *
1595
+ * @param ref - RefObject pointing to the element to measure
1596
+ * @returns {Geometry} The geometry of the element
1597
+ * @example
1598
+ * ```tsx
1599
+ * const ref = useRef<HTMLDivElement>(null);
1600
+ * const geometry = useMeasureElement(ref);
1601
+ * return <div ref={ref}>Width: {geometry.width}</div>;
1602
+ * ```
1603
+ */
1604
+ const useMeasureElement = (ref, { skip = false, onChange } = {}) => {
1605
+ return useMeasureShared(ref.current, { skip, onChange });
1606
+ };
1607
+
1621
1608
  /**
1622
1609
  * Hook that returns true if any modifier key (Ctrl, Alt, Shift, Meta/Cmd) is pressed
1623
1610
  *
@@ -2396,9 +2383,8 @@ const Collapse = ({ id, variant = "primary", initialExpanded = false, onToggle,
2396
2383
  return (jsxRuntime.jsxs("div", { className: cvaCollapse({ variant: variant, className }), "data-testid": dataTestId, children: [jsxRuntime.jsx("div", { "aria-controls": id, "aria-expanded": expanded, className: cvaCollapseHeader({ expanded, variant, extraPadding, className: headerClassName }), onClick: handleClick, role: "button", children: jsxRuntime.jsxs("div", { className: cvaCollapseLabelContainer({ variant }), children: [jsxRuntime.jsx(Text, { className: cvaCollapseLabel({ variant }), id: LABEL_ID, size: variant === "secondary" ? "small" : "medium", type: "span", weight: "bold", children: label }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [headerAddon !== null && headerAddon !== undefined && variant !== "secondary" ? headerAddon : null, jsxRuntime.jsx(Icon, { ariaLabelledBy: LABEL_ID, className: cvaChevronIcon({ expanded }), name: "ChevronUp", size: variant === "secondary" ? "small" : "medium" })] })] }) }), jsxRuntime.jsx(Collapsible, { expanded: expanded, extraPadding: extraPadding, id: id, variant: variant, children: expanded || animate ? children : null })] }));
2397
2384
  };
2398
2385
  const Collapsible = ({ children, expanded, id, variant, extraPadding }) => {
2399
- const ref = react.useRef(null);
2400
- const { height } = useGeometry(ref);
2401
- return (jsxRuntime.jsx("div", { className: cvaCollapseAnimated({ expanded }), id: id, style: { height: expanded ? height || "auto" : "0" }, children: jsxRuntime.jsx("div", { ref: ref, children: jsxRuntime.jsx("div", { className: cvaCollapsible({ variant, extraPadding }), children: children }) }) }));
2386
+ const { geometry, ref } = useMeasure();
2387
+ return (jsxRuntime.jsx("div", { className: cvaCollapseAnimated({ expanded }), id: id, style: { height: expanded ? (geometry?.height ?? "auto") : "0" }, children: jsxRuntime.jsx("div", { ref: ref, children: jsxRuntime.jsx("div", { className: cvaCollapsible({ variant, extraPadding }), children: children }) }) }));
2402
2388
  };
2403
2389
 
2404
2390
  /**
@@ -2845,9 +2831,9 @@ const OverflowIndicator = ({ className, dataTestId, direction, onClickScroll, })
2845
2831
  * @returns {Element} HorizontalOverflowScroller component
2846
2832
  */
2847
2833
  const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollStateChange, }) => {
2848
- const containerRef = react.useRef(null);
2849
2834
  const childrenArray = react.Children.toArray(children);
2850
- const { width: containerWidth } = useGeometry(containerRef);
2835
+ const containerRef = react.useRef(null);
2836
+ const containerGeometry = useMeasureElement(containerRef);
2851
2837
  const { isScrollable, isAtBeginning, isAtEnd } = useScrollDetection(containerRef, {
2852
2838
  direction: "horizontal",
2853
2839
  onScrollStateChange: onScrollStateChange
@@ -2859,20 +2845,18 @@ const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollS
2859
2845
  : undefined,
2860
2846
  });
2861
2847
  const handleScrollLeft = () => {
2862
- const element = containerRef.current;
2863
- if (!element || !containerWidth)
2848
+ if (!containerRef.current || containerGeometry?.width === undefined)
2864
2849
  return;
2865
- element.scrollBy({
2866
- left: -containerWidth,
2850
+ containerRef.current.scrollBy({
2851
+ left: -containerGeometry.width,
2867
2852
  behavior: "smooth",
2868
2853
  });
2869
2854
  };
2870
2855
  const handleScrollRight = () => {
2871
- const element = containerRef.current;
2872
- if (!element || !containerWidth)
2856
+ if (!containerRef.current || containerGeometry?.width === undefined)
2873
2857
  return;
2874
- element.scrollBy({
2875
- left: containerWidth,
2858
+ containerRef.current.scrollBy({
2859
+ left: containerGeometry.width,
2876
2860
  behavior: "smooth",
2877
2861
  });
2878
2862
  };
@@ -5477,7 +5461,6 @@ exports.useDebounce = useDebounce;
5477
5461
  exports.useDevicePixelRatio = useDevicePixelRatio;
5478
5462
  exports.useElevatedReducer = useElevatedReducer;
5479
5463
  exports.useElevatedState = useElevatedState;
5480
- exports.useGeometry = useGeometry;
5481
5464
  exports.useHover = useHover;
5482
5465
  exports.useInfiniteScroll = useInfiniteScroll;
5483
5466
  exports.useIsFirstRender = useIsFirstRender;
@@ -5485,6 +5468,8 @@ exports.useIsFullscreen = useIsFullscreen;
5485
5468
  exports.useIsTextTruncated = useIsTextTruncated;
5486
5469
  exports.useList = useList;
5487
5470
  exports.useListItemHeight = useListItemHeight;
5471
+ exports.useMeasure = useMeasure;
5472
+ exports.useMeasureElement = useMeasureElement;
5488
5473
  exports.useModifierKey = useModifierKey;
5489
5474
  exports.useOverflowItems = useOverflowItems;
5490
5475
  exports.usePopoverContext = usePopoverContext;
package/index.esm.js CHANGED
@@ -1323,109 +1323,6 @@ const useElevatedState = (initialState, customState) => {
1323
1323
  return useMemo(() => customState ?? [fallbackValue, fallbackSetter], [customState, fallbackValue, fallbackSetter]);
1324
1324
  };
1325
1325
 
1326
- const UNINITIALIZED = Symbol("UNINITIALIZED");
1327
- /**
1328
- * Custom hook to get the geometry of an element.
1329
- * Size and position of the element relative to the viewport.
1330
- */
1331
- const useGeometry = (ref, { skip = false, onChange } = {}) => {
1332
- const [geometry, setGeometry] = useState(() => {
1333
- const rect = ref.current?.getBoundingClientRect();
1334
- if (!rect) {
1335
- return {
1336
- width: 0,
1337
- height: 0,
1338
- top: 0,
1339
- bottom: 0,
1340
- left: 0,
1341
- right: 0,
1342
- x: 0,
1343
- y: 0,
1344
- };
1345
- }
1346
- return {
1347
- width: rect.width,
1348
- height: rect.height,
1349
- top: rect.top,
1350
- bottom: rect.bottom,
1351
- left: rect.left,
1352
- right: rect.right,
1353
- x: rect.x,
1354
- y: rect.y,
1355
- };
1356
- });
1357
- const resizeObserver = useRef(null);
1358
- const [element, setElement] = useState(ref.current);
1359
- const prevGeometry = useRef(UNINITIALIZED);
1360
- // Track changes to ref.current on every render
1361
- if (ref.current !== element) {
1362
- setElement(ref.current);
1363
- }
1364
- useEffect(() => {
1365
- if (skip || !element) {
1366
- return;
1367
- }
1368
- // Update geometry immediately when element changes
1369
- const elementRect = element.getBoundingClientRect();
1370
- const newGeometry = {
1371
- width: elementRect.width,
1372
- height: elementRect.height,
1373
- top: elementRect.top,
1374
- bottom: elementRect.bottom,
1375
- left: elementRect.left,
1376
- right: elementRect.right,
1377
- x: elementRect.x,
1378
- y: elementRect.y,
1379
- };
1380
- const prev = prevGeometry.current;
1381
- const hasChanged = prev === UNINITIALIZED ? false : !isEqual(newGeometry, prev);
1382
- if (!hasChanged && prev !== UNINITIALIZED) {
1383
- return;
1384
- }
1385
- setGeometry(newGeometry);
1386
- if (hasChanged && prev !== UNINITIALIZED) {
1387
- onChange?.(newGeometry);
1388
- }
1389
- prevGeometry.current = newGeometry;
1390
- const observe = () => {
1391
- if (!resizeObserver.current) {
1392
- resizeObserver.current = new ResizeObserver(entries => {
1393
- for (const entry of entries) {
1394
- const entryRect = entry.target.getBoundingClientRect();
1395
- const observedGeometry = {
1396
- width: entryRect.width,
1397
- height: entryRect.height,
1398
- top: entryRect.top,
1399
- bottom: entryRect.bottom,
1400
- left: entryRect.left,
1401
- right: entryRect.right,
1402
- x: entryRect.x,
1403
- y: entryRect.y,
1404
- };
1405
- const prevObserved = prevGeometry.current;
1406
- const hasObservedChanged = prevObserved === UNINITIALIZED ? false : !isEqual(observedGeometry, prevObserved);
1407
- if (hasObservedChanged || prevObserved === UNINITIALIZED) {
1408
- setGeometry(observedGeometry);
1409
- if (hasObservedChanged && prevObserved !== UNINITIALIZED) {
1410
- onChange?.(observedGeometry);
1411
- }
1412
- prevGeometry.current = observedGeometry;
1413
- }
1414
- }
1415
- });
1416
- }
1417
- resizeObserver.current.observe(element);
1418
- };
1419
- observe();
1420
- return () => {
1421
- if (resizeObserver.current) {
1422
- resizeObserver.current.disconnect();
1423
- }
1424
- };
1425
- }, [element, onChange, skip]);
1426
- return geometry;
1427
- };
1428
-
1429
1326
  /**
1430
1327
  * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
1431
1328
  * The boolean will be true if the element is being hovered, and false if it is not.
@@ -1616,6 +1513,96 @@ const useIsTextTruncated = (text, { skip = false } = {}) => {
1616
1513
  return useMemo(() => ({ ref, isTextTruncated }), [isTextTruncated]);
1617
1514
  };
1618
1515
 
1516
+ /**
1517
+ * Shared measurement logic used by both useMeasure and useMeasureElement.
1518
+ * This hook observes an element and measures its geometry.
1519
+ */
1520
+ const useMeasureShared = (element, { skip = false, onChange } = {}) => {
1521
+ const [geometry, setGeometry] = useState(undefined);
1522
+ const observerRef = useRef(null);
1523
+ const onChangeRef = useRef(onChange);
1524
+ const isInitialRender = useRef(true);
1525
+ onChangeRef.current = onChange;
1526
+ const disconnectObserver = () => {
1527
+ if (observerRef.current !== null) {
1528
+ observerRef.current.disconnect();
1529
+ observerRef.current = null;
1530
+ }
1531
+ };
1532
+ useEffect(() => {
1533
+ if (skip || element === null) {
1534
+ return;
1535
+ }
1536
+ const updateGeometry = (rect) => {
1537
+ const newGeometry = omit(rect, ["toJSON"]);
1538
+ setGeometry(prevGeometry => {
1539
+ if (isEqual(prevGeometry, newGeometry)) {
1540
+ return prevGeometry;
1541
+ }
1542
+ if (isInitialRender.current === false) {
1543
+ onChangeRef.current?.(newGeometry);
1544
+ }
1545
+ return newGeometry;
1546
+ });
1547
+ isInitialRender.current = false;
1548
+ };
1549
+ const handleResize = (entries) => {
1550
+ for (const entry of entries) {
1551
+ const rect = entry.target.getBoundingClientRect();
1552
+ updateGeometry(rect);
1553
+ }
1554
+ };
1555
+ disconnectObserver();
1556
+ const observer = new ResizeObserver(handleResize);
1557
+ observer.observe(element);
1558
+ observerRef.current = observer;
1559
+ const initialRect = element.getBoundingClientRect();
1560
+ updateGeometry(initialRect);
1561
+ return disconnectObserver;
1562
+ }, [element, skip]);
1563
+ return geometry;
1564
+ };
1565
+
1566
+ /**
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.
1569
+ *
1570
+ * @returns {UseMeasureResult<HTMLElement>} An object containing `geometry`, `ref` callback, and `element`.
1571
+ * @template TElement extends HTMLElement
1572
+ * @example
1573
+ * ```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.
1592
+ *
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>;
1600
+ * ```
1601
+ */
1602
+ const useMeasureElement = (ref, { skip = false, onChange } = {}) => {
1603
+ return useMeasureShared(ref.current, { skip, onChange });
1604
+ };
1605
+
1619
1606
  /**
1620
1607
  * Hook that returns true if any modifier key (Ctrl, Alt, Shift, Meta/Cmd) is pressed
1621
1608
  *
@@ -2394,9 +2381,8 @@ const Collapse = ({ id, variant = "primary", initialExpanded = false, onToggle,
2394
2381
  return (jsxs("div", { className: cvaCollapse({ variant: variant, className }), "data-testid": dataTestId, children: [jsx("div", { "aria-controls": id, "aria-expanded": expanded, className: cvaCollapseHeader({ expanded, variant, extraPadding, className: headerClassName }), onClick: handleClick, role: "button", children: jsxs("div", { className: cvaCollapseLabelContainer({ variant }), children: [jsx(Text, { className: cvaCollapseLabel({ variant }), id: LABEL_ID, size: variant === "secondary" ? "small" : "medium", type: "span", weight: "bold", children: label }), jsxs("div", { className: "flex items-center gap-2", children: [headerAddon !== null && headerAddon !== undefined && variant !== "secondary" ? headerAddon : null, jsx(Icon, { ariaLabelledBy: LABEL_ID, className: cvaChevronIcon({ expanded }), name: "ChevronUp", size: variant === "secondary" ? "small" : "medium" })] })] }) }), jsx(Collapsible, { expanded: expanded, extraPadding: extraPadding, id: id, variant: variant, children: expanded || animate ? children : null })] }));
2395
2382
  };
2396
2383
  const Collapsible = ({ children, expanded, id, variant, extraPadding }) => {
2397
- const ref = useRef(null);
2398
- const { height } = useGeometry(ref);
2399
- return (jsx("div", { className: cvaCollapseAnimated({ expanded }), id: id, style: { height: expanded ? height || "auto" : "0" }, children: jsx("div", { ref: ref, children: jsx("div", { className: cvaCollapsible({ variant, extraPadding }), children: children }) }) }));
2384
+ const { geometry, ref } = useMeasure();
2385
+ return (jsx("div", { className: cvaCollapseAnimated({ expanded }), id: id, style: { height: expanded ? (geometry?.height ?? "auto") : "0" }, children: jsx("div", { ref: ref, children: jsx("div", { className: cvaCollapsible({ variant, extraPadding }), children: children }) }) }));
2400
2386
  };
2401
2387
 
2402
2388
  /**
@@ -2843,9 +2829,9 @@ const OverflowIndicator = ({ className, dataTestId, direction, onClickScroll, })
2843
2829
  * @returns {Element} HorizontalOverflowScroller component
2844
2830
  */
2845
2831
  const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollStateChange, }) => {
2846
- const containerRef = useRef(null);
2847
2832
  const childrenArray = Children.toArray(children);
2848
- const { width: containerWidth } = useGeometry(containerRef);
2833
+ const containerRef = useRef(null);
2834
+ const containerGeometry = useMeasureElement(containerRef);
2849
2835
  const { isScrollable, isAtBeginning, isAtEnd } = useScrollDetection(containerRef, {
2850
2836
  direction: "horizontal",
2851
2837
  onScrollStateChange: onScrollStateChange
@@ -2857,20 +2843,18 @@ const HorizontalOverflowScroller = ({ className, dataTestId, children, onScrollS
2857
2843
  : undefined,
2858
2844
  });
2859
2845
  const handleScrollLeft = () => {
2860
- const element = containerRef.current;
2861
- if (!element || !containerWidth)
2846
+ if (!containerRef.current || containerGeometry?.width === undefined)
2862
2847
  return;
2863
- element.scrollBy({
2864
- left: -containerWidth,
2848
+ containerRef.current.scrollBy({
2849
+ left: -containerGeometry.width,
2865
2850
  behavior: "smooth",
2866
2851
  });
2867
2852
  };
2868
2853
  const handleScrollRight = () => {
2869
- const element = containerRef.current;
2870
- if (!element || !containerWidth)
2854
+ if (!containerRef.current || containerGeometry?.width === undefined)
2871
2855
  return;
2872
- element.scrollBy({
2873
- left: containerWidth,
2856
+ containerRef.current.scrollBy({
2857
+ left: containerGeometry.width,
2874
2858
  behavior: "smooth",
2875
2859
  });
2876
2860
  };
@@ -5359,4 +5343,4 @@ const cvaClickable = cvaMerge([
5359
5343
  },
5360
5344
  });
5361
5345
 
5362
- 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, useGeometry, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useModifierKey, useOverflowItems, usePopoverContext, usePrompt, useRelayPagination, useResize, useScrollDetection, useSelfUpdatingRef, useTimeout, useViewportBreakpoints, useWindowActivity };
5346
+ 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, 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.17",
3
+ "version": "1.10.19",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -16,11 +16,11 @@
16
16
  "@floating-ui/react": "^0.26.25",
17
17
  "string-ts": "^2.0.0",
18
18
  "tailwind-merge": "^2.0.0",
19
- "@trackunit/ui-design-tokens": "1.7.49",
20
- "@trackunit/css-class-variance-utilities": "1.7.49",
21
- "@trackunit/shared-utils": "1.9.49",
22
- "@trackunit/ui-icons": "1.7.50",
23
- "@trackunit/react-test-setup": "1.4.49",
19
+ "@trackunit/ui-design-tokens": "1.7.51",
20
+ "@trackunit/css-class-variance-utilities": "1.7.51",
21
+ "@trackunit/shared-utils": "1.9.51",
22
+ "@trackunit/ui-icons": "1.7.52",
23
+ "@trackunit/react-test-setup": "1.4.51",
24
24
  "@tanstack/react-router": "1.114.29",
25
25
  "es-toolkit": "^1.39.10",
26
26
  "@tanstack/react-virtual": "3.13.12"
@@ -6,12 +6,14 @@ export * from "./useDebounce";
6
6
  export * from "./useDevicePixelRatio";
7
7
  export * from "./useElevatedReducer";
8
8
  export * from "./useElevatedState";
9
- export * from "./useGeometry";
10
9
  export * from "./useHover";
11
10
  export * from "./useInfiniteScroll";
12
11
  export * from "./useIsFirstRender";
13
12
  export * from "./useIsFullScreen";
14
13
  export * from "./useIsTextTruncated";
14
+ export * from "./useMeasure/useMeasure";
15
+ export * from "./useMeasure/useMeasureElement";
16
+ export type { Geometry } from "./useMeasure/useMeasureShared";
15
17
  export * from "./useModifierKey";
16
18
  export * from "./useRelayPagination";
17
19
  export * from "./useResize";
@@ -0,0 +1,22 @@
1
+ import { RefCallback } from "react";
2
+ import { Geometry, UseMeasureSharedOptions } from "./useMeasureShared";
3
+ type UseMeasureOptions = UseMeasureSharedOptions;
4
+ type UseMeasureResult<TElement extends HTMLElement = HTMLElement> = {
5
+ geometry: Geometry | undefined;
6
+ ref: RefCallback<TElement>;
7
+ element: TElement | null;
8
+ };
9
+ /**
10
+ * Custom hook to measure the geometry of an element using a callback ref.
11
+ * Measures the size and position of the element relative to the viewport.
12
+ *
13
+ * @returns {UseMeasureResult<HTMLElement>} An object containing `geometry`, `ref` callback, and `element`.
14
+ * @template TElement extends HTMLElement
15
+ * @example
16
+ * ```tsx
17
+ * const { geometry, ref } = useMeasure();
18
+ * return <div ref={ref}>Width: {geometry.width}</div>;
19
+ * ```
20
+ */
21
+ export declare const useMeasure: <TElement extends HTMLElement = HTMLElement>({ skip, onChange, }?: UseMeasureOptions) => UseMeasureResult<TElement>;
22
+ export {};
@@ -0,0 +1,19 @@
1
+ import { RefObject } from "react";
2
+ import { Geometry, UseMeasureSharedOptions } from "./useMeasureShared";
3
+ type UseMeasureElementOptions = UseMeasureSharedOptions;
4
+ /**
5
+ * Custom hook to measure the geometry of an element from a RefObject.
6
+ * Use this when you already have a ref (e.g., from useRef or for composition with other hooks).
7
+ * Measures the size and position of the element relative to the viewport.
8
+ *
9
+ * @param ref - RefObject pointing to the element to measure
10
+ * @returns {Geometry} The geometry of the element
11
+ * @example
12
+ * ```tsx
13
+ * const ref = useRef<HTMLDivElement>(null);
14
+ * const geometry = useMeasureElement(ref);
15
+ * return <div ref={ref}>Width: {geometry.width}</div>;
16
+ * ```
17
+ */
18
+ export declare const useMeasureElement: <TElement extends HTMLElement = HTMLElement>(ref: RefObject<TElement | null>, { skip, onChange }?: UseMeasureElementOptions) => Geometry | undefined;
19
+ export {};
@@ -0,0 +1,19 @@
1
+ export type Geometry = {
2
+ readonly height: number;
3
+ readonly width: number;
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly bottom: number;
7
+ readonly left: number;
8
+ readonly right: number;
9
+ readonly top: number;
10
+ };
11
+ export type UseMeasureSharedOptions = {
12
+ skip?: boolean;
13
+ onChange?: (geometry: Geometry) => void;
14
+ };
15
+ /**
16
+ * Shared measurement logic used by both useMeasure and useMeasureElement.
17
+ * This hook observes an element and measures its geometry.
18
+ */
19
+ export declare const useMeasureShared: <TElement extends HTMLElement>(element: TElement | null, { skip, onChange }?: UseMeasureSharedOptions) => Geometry | undefined;
@@ -1,21 +0,0 @@
1
- import { RefObject } from "react";
2
- type Geometry = {
3
- width: number;
4
- height: number;
5
- top: number;
6
- bottom: number;
7
- left: number;
8
- right: number;
9
- x: number;
10
- y: number;
11
- };
12
- interface UseGeometryOptions {
13
- skip?: boolean;
14
- onChange?: (geometry: Geometry) => void;
15
- }
16
- /**
17
- * Custom hook to get the geometry of an element.
18
- * Size and position of the element relative to the viewport.
19
- */
20
- export declare const useGeometry: (ref: RefObject<HTMLElement | null>, { skip, onChange }?: UseGeometryOptions) => Geometry;
21
- export {};