@trackunit/react-components 1.10.15 → 1.10.17

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
@@ -1231,34 +1231,28 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }
1231
1231
  /**
1232
1232
  * The useDebounce hook works like useState, but adds a delay where previous values will be ignored.
1233
1233
  *
1234
- * @param {any} value The value to set
1235
- * @param {number} delay The debounce time
1236
- * @param {"in" | "out" | "both"} direction Considers truthiness of value. If set to "in", the value will only be debounced if it is truthy. If set to "out", the value will only be debounced if it is falsy. If set to "both" or if not defined, the value will be debounced regardless of truthiness.
1237
- * @returns {any} The latest value received
1234
+ * @template TValue
1235
+ * @param {TValue} value The value to set
1236
+ * @param {UseDebounceOptions<TValue>} options The debounce options
1237
+ * @param {(debouncedValue: TValue) => void} options.onBounce Callback when the value is debounced
1238
+ * @param {number} options.delay The debounce time
1239
+ * @returns {TValue} The latest value received
1238
1240
  */
1239
- /**
1240
- *
1241
- */
1242
- const useDebounce = (value, delay = 500, direction, onBounce) => {
1241
+ const useDebounce = (value, { onBounce, delay = 500 } = {}) => {
1243
1242
  const [debouncedValue, setDebouncedValue] = react.useState(value);
1244
1243
  react.useEffect(() => {
1245
- let handler;
1246
- if ((direction === "in" && !Boolean(debouncedValue) && Boolean(value)) ||
1247
- (direction === "out" && Boolean(debouncedValue) && !Boolean(value)) ||
1248
- direction === "both" ||
1249
- direction === undefined) {
1250
- handler = setTimeout(() => {
1251
- setDebouncedValue(value);
1252
- onBounce?.(value);
1253
- }, delay);
1244
+ // Skip if value hasn't changed
1245
+ if (esToolkit.isEqual(value, debouncedValue)) {
1246
+ return;
1254
1247
  }
1255
- else {
1248
+ const handler = setTimeout(() => {
1256
1249
  setDebouncedValue(value);
1257
- }
1250
+ onBounce?.(value);
1251
+ }, delay);
1258
1252
  return () => {
1259
1253
  clearTimeout(handler);
1260
1254
  };
1261
- }, [value, delay, direction, debouncedValue, onBounce]);
1255
+ }, [value, delay, debouncedValue, onBounce]);
1262
1256
  return debouncedValue;
1263
1257
  };
1264
1258
 
@@ -1444,7 +1438,22 @@ const useGeometry = (ref, { skip = false, onChange } = {}) => {
1444
1438
  */
1445
1439
  const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
1446
1440
  const [hovering, setHovering] = react.useState(false);
1447
- const debouncedHovering = useDebounce(hovering, delay, direction);
1441
+ const [debouncedHovering, setDebouncedHovering] = react.useState(false);
1442
+ react.useEffect(() => {
1443
+ if (!debounced) {
1444
+ setDebouncedHovering(hovering);
1445
+ return undefined;
1446
+ }
1447
+ const shouldDebounce = direction === "both" || (direction === "in" && hovering) || (direction === "out" && !hovering);
1448
+ if (shouldDebounce) {
1449
+ const timer = setTimeout(() => {
1450
+ setDebouncedHovering(hovering);
1451
+ }, delay);
1452
+ return () => clearTimeout(timer);
1453
+ }
1454
+ setDebouncedHovering(hovering);
1455
+ return undefined;
1456
+ }, [debounced, direction, delay, hovering]);
1448
1457
  const onMouseEnter = react.useCallback(() => {
1449
1458
  setHovering(true);
1450
1459
  }, []);
@@ -1454,8 +1463,8 @@ const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debo
1454
1463
  return react.useMemo(() => ({
1455
1464
  onMouseEnter,
1456
1465
  onMouseLeave,
1457
- hovering: debounced ? debouncedHovering : hovering,
1458
- }), [onMouseEnter, onMouseLeave, debounced, debouncedHovering, hovering]);
1466
+ hovering: debouncedHovering,
1467
+ }), [onMouseEnter, onMouseLeave, debouncedHovering]);
1459
1468
  };
1460
1469
 
1461
1470
  const OVERSCAN = 10;
@@ -3803,6 +3812,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3803
3812
  const containerRef = react.useRef(null);
3804
3813
  const listRef = react.useRef(null);
3805
3814
  const rowRefsMap = react.useRef(new Map());
3815
+ const virtualizerRef = react.useRef(null);
3816
+ const hasMeasuredOnMount = react.useRef(false);
3806
3817
  // Resolve loading indicator once to avoid unnecessary re-renders
3807
3818
  const resolvedLoadingIndicator = react.useMemo(() => getResolvedLoadingIndicator(loadingIndicator), [loadingIndicator]);
3808
3819
  // Calculate total count including header
@@ -3906,13 +3917,16 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3906
3917
  onChange?.(instance);
3907
3918
  },
3908
3919
  });
3909
- // Measure the list on mount
3920
+ // Keep ref updated with latest virtualizer instance
3921
+ react.useEffect(() => {
3922
+ virtualizerRef.current = virtualizer;
3923
+ }, [virtualizer]);
3924
+ // Measure the list on mount only (not on subsequent virtualizer updates)
3910
3925
  react.useLayoutEffect(() => {
3911
- // Automatically measure if header is present, or if explicitly requested
3912
- virtualizer.measure();
3913
- // This is straight out of the TanStack Virtual docs, so we'll trust it.
3914
- // eslint-disable-next-line react-hooks/react-compiler
3915
- // eslint-disable-next-line react-hooks/exhaustive-deps
3926
+ if (!hasMeasuredOnMount.current) {
3927
+ virtualizerRef.current?.measure();
3928
+ hasMeasuredOnMount.current = true;
3929
+ }
3916
3930
  }, []);
3917
3931
  // Scroll to top when total count changes (filtering applied)
3918
3932
  // Don't scroll when just loading more items (infinite scroll - total stays the same)
package/index.esm.js CHANGED
@@ -1229,34 +1229,28 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }
1229
1229
  /**
1230
1230
  * The useDebounce hook works like useState, but adds a delay where previous values will be ignored.
1231
1231
  *
1232
- * @param {any} value The value to set
1233
- * @param {number} delay The debounce time
1234
- * @param {"in" | "out" | "both"} direction Considers truthiness of value. If set to "in", the value will only be debounced if it is truthy. If set to "out", the value will only be debounced if it is falsy. If set to "both" or if not defined, the value will be debounced regardless of truthiness.
1235
- * @returns {any} The latest value received
1232
+ * @template TValue
1233
+ * @param {TValue} value The value to set
1234
+ * @param {UseDebounceOptions<TValue>} options The debounce options
1235
+ * @param {(debouncedValue: TValue) => void} options.onBounce Callback when the value is debounced
1236
+ * @param {number} options.delay The debounce time
1237
+ * @returns {TValue} The latest value received
1236
1238
  */
1237
- /**
1238
- *
1239
- */
1240
- const useDebounce = (value, delay = 500, direction, onBounce) => {
1239
+ const useDebounce = (value, { onBounce, delay = 500 } = {}) => {
1241
1240
  const [debouncedValue, setDebouncedValue] = useState(value);
1242
1241
  useEffect(() => {
1243
- let handler;
1244
- if ((direction === "in" && !Boolean(debouncedValue) && Boolean(value)) ||
1245
- (direction === "out" && Boolean(debouncedValue) && !Boolean(value)) ||
1246
- direction === "both" ||
1247
- direction === undefined) {
1248
- handler = setTimeout(() => {
1249
- setDebouncedValue(value);
1250
- onBounce?.(value);
1251
- }, delay);
1242
+ // Skip if value hasn't changed
1243
+ if (isEqual(value, debouncedValue)) {
1244
+ return;
1252
1245
  }
1253
- else {
1246
+ const handler = setTimeout(() => {
1254
1247
  setDebouncedValue(value);
1255
- }
1248
+ onBounce?.(value);
1249
+ }, delay);
1256
1250
  return () => {
1257
1251
  clearTimeout(handler);
1258
1252
  };
1259
- }, [value, delay, direction, debouncedValue, onBounce]);
1253
+ }, [value, delay, debouncedValue, onBounce]);
1260
1254
  return debouncedValue;
1261
1255
  };
1262
1256
 
@@ -1442,7 +1436,22 @@ const useGeometry = (ref, { skip = false, onChange } = {}) => {
1442
1436
  */
1443
1437
  const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
1444
1438
  const [hovering, setHovering] = useState(false);
1445
- const debouncedHovering = useDebounce(hovering, delay, direction);
1439
+ const [debouncedHovering, setDebouncedHovering] = useState(false);
1440
+ useEffect(() => {
1441
+ if (!debounced) {
1442
+ setDebouncedHovering(hovering);
1443
+ return undefined;
1444
+ }
1445
+ const shouldDebounce = direction === "both" || (direction === "in" && hovering) || (direction === "out" && !hovering);
1446
+ if (shouldDebounce) {
1447
+ const timer = setTimeout(() => {
1448
+ setDebouncedHovering(hovering);
1449
+ }, delay);
1450
+ return () => clearTimeout(timer);
1451
+ }
1452
+ setDebouncedHovering(hovering);
1453
+ return undefined;
1454
+ }, [debounced, direction, delay, hovering]);
1446
1455
  const onMouseEnter = useCallback(() => {
1447
1456
  setHovering(true);
1448
1457
  }, []);
@@ -1452,8 +1461,8 @@ const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debo
1452
1461
  return useMemo(() => ({
1453
1462
  onMouseEnter,
1454
1463
  onMouseLeave,
1455
- hovering: debounced ? debouncedHovering : hovering,
1456
- }), [onMouseEnter, onMouseLeave, debounced, debouncedHovering, hovering]);
1464
+ hovering: debouncedHovering,
1465
+ }), [onMouseEnter, onMouseLeave, debouncedHovering]);
1457
1466
  };
1458
1467
 
1459
1468
  const OVERSCAN = 10;
@@ -3801,6 +3810,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3801
3810
  const containerRef = useRef(null);
3802
3811
  const listRef = useRef(null);
3803
3812
  const rowRefsMap = useRef(new Map());
3813
+ const virtualizerRef = useRef(null);
3814
+ const hasMeasuredOnMount = useRef(false);
3804
3815
  // Resolve loading indicator once to avoid unnecessary re-renders
3805
3816
  const resolvedLoadingIndicator = useMemo(() => getResolvedLoadingIndicator(loadingIndicator), [loadingIndicator]);
3806
3817
  // Calculate total count including header
@@ -3904,13 +3915,16 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3904
3915
  onChange?.(instance);
3905
3916
  },
3906
3917
  });
3907
- // Measure the list on mount
3918
+ // Keep ref updated with latest virtualizer instance
3919
+ useEffect(() => {
3920
+ virtualizerRef.current = virtualizer;
3921
+ }, [virtualizer]);
3922
+ // Measure the list on mount only (not on subsequent virtualizer updates)
3908
3923
  useLayoutEffect(() => {
3909
- // Automatically measure if header is present, or if explicitly requested
3910
- virtualizer.measure();
3911
- // This is straight out of the TanStack Virtual docs, so we'll trust it.
3912
- // eslint-disable-next-line react-hooks/react-compiler
3913
- // eslint-disable-next-line react-hooks/exhaustive-deps
3924
+ if (!hasMeasuredOnMount.current) {
3925
+ virtualizerRef.current?.measure();
3926
+ hasMeasuredOnMount.current = true;
3927
+ }
3914
3928
  }, []);
3915
3929
  // Scroll to top when total count changes (filtering applied)
3916
3930
  // Don't scroll when just loading more items (infinite scroll - total stays the same)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.10.15",
3
+ "version": "1.10.17",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -1,13 +1,16 @@
1
- export type DebounceDirection = "in" | "out" | "both";
1
+ type UseDebounceOptions<TValue> = {
2
+ onBounce?: (debouncedValue: TValue) => void;
3
+ delay?: number;
4
+ };
2
5
  /**
3
6
  * The useDebounce hook works like useState, but adds a delay where previous values will be ignored.
4
7
  *
5
- * @param {any} value The value to set
6
- * @param {number} delay The debounce time
7
- * @param {"in" | "out" | "both"} direction Considers truthiness of value. If set to "in", the value will only be debounced if it is truthy. If set to "out", the value will only be debounced if it is falsy. If set to "both" or if not defined, the value will be debounced regardless of truthiness.
8
- * @returns {any} The latest value received
8
+ * @template TValue
9
+ * @param {TValue} value The value to set
10
+ * @param {UseDebounceOptions<TValue>} options The debounce options
11
+ * @param {(debouncedValue: TValue) => void} options.onBounce Callback when the value is debounced
12
+ * @param {number} options.delay The debounce time
13
+ * @returns {TValue} The latest value received
9
14
  */
10
- /**
11
- *
12
- */
13
- export declare const useDebounce: <TValue>(value: TValue, delay?: number, direction?: DebounceDirection, onBounce?: (debouncedValue: TValue) => void) => TValue;
15
+ export declare const useDebounce: <TValue>(value: TValue, { onBounce, delay }?: UseDebounceOptions<TValue>) => TValue;
16
+ export {};
@@ -1,4 +1,4 @@
1
- import { DebounceDirection } from "./useDebounce";
1
+ export type DebounceDirection = "in" | "out" | "both";
2
2
  type UseHoverProps = {
3
3
  debounced: true;
4
4
  delay?: number;