@trackunit/react-components 1.10.5 → 1.10.11
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 +129 -65
- package/index.esm.js +129 -65
- package/package.json +6 -6
- package/src/components/PageHeader/PageHeader.d.ts +1 -1
- package/src/components/PageHeader/components/PageHeaderSecondaryActions.d.ts +8 -5
- package/src/components/PageHeader/types.d.ts +42 -19
- package/src/hooks/useContainerBreakpoints.d.ts +7 -1
- package/src/hooks/useContinuousTimeout.d.ts +1 -1
- package/src/hooks/useDevicePixelRatio.d.ts +6 -0
- package/src/hooks/useGeometry.d.ts +10 -8
- package/src/hooks/useInfiniteScroll.d.ts +3 -1
- package/src/hooks/useIsTextTruncated.d.ts +7 -1
- package/src/hooks/useIsTextWrapping.d.ts +7 -1
- package/src/hooks/useScrollDetection.d.ts +2 -1
- package/src/hooks/useTimeout.d.ts +1 -1
- package/src/hooks/useViewportBreakpoints.d.ts +7 -1
- package/src/hooks/useWindowActivity.d.ts +2 -1
package/index.esm.js
CHANGED
|
@@ -10,11 +10,11 @@ import IconSpriteSolid from '@trackunit/ui-icons/icons-sprite-solid.svg';
|
|
|
10
10
|
import { snakeCase, titleCase } from 'string-ts';
|
|
11
11
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
12
12
|
import { Slottable, Slot } from '@radix-ui/react-slot';
|
|
13
|
+
import { isEqual, omit } from 'es-toolkit';
|
|
13
14
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
14
15
|
import { useDebounceCallback, useCopyToClipboard } from 'usehooks-ts';
|
|
15
16
|
import { Link, useBlocker } from '@tanstack/react-router';
|
|
16
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 { omit } from 'es-toolkit';
|
|
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';
|
|
@@ -252,14 +252,14 @@ const Tag = ({ className, dataTestId, children, size = "medium", onClose, color
|
|
|
252
252
|
return false;
|
|
253
253
|
}, [color]);
|
|
254
254
|
const layout = useMemo(() => {
|
|
255
|
-
if (onClose !== undefined && isSupportedDismissColor) {
|
|
255
|
+
if (onClose !== undefined && isSupportedDismissColor && !disabled) {
|
|
256
256
|
return "containsDismiss";
|
|
257
257
|
}
|
|
258
258
|
if (icon !== null && icon !== undefined) {
|
|
259
259
|
return "containsIcon";
|
|
260
260
|
}
|
|
261
261
|
return "default";
|
|
262
|
-
}, [onClose,
|
|
262
|
+
}, [onClose, isSupportedDismissColor, disabled, icon]);
|
|
263
263
|
return (jsxs("div", { className: cvaTag({ className, size, color, layout, border: color === "white" ? "default" : "none" }), "data-testid": dataTestId, onMouseEnter: onMouseEnter, ref: ref, children: [icon !== null && icon !== undefined && size === "medium" ? (jsx("div", { className: cvaTagIconContainer(), children: icon })) : null, jsx("span", { className: cvaTagText(), children: children }), Boolean(onClose) && isSupportedDismissColor && size === "medium" && !disabled ? (
|
|
264
264
|
// a fix for multiselect deselecting tags working together with fade out animation
|
|
265
265
|
jsx("div", { className: cvaTagIconContainer(), onMouseDown: onClose, children: jsx(Icon, { className: cvaTagIcon(), dataTestId: dataTestId + "Icon", name: "XCircle", size: "small", style: { WebkitTransition: "-webkit-transform 0.150s" }, type: "solid" }) })) : null] }));
|
|
@@ -277,6 +277,8 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
|
|
|
277
277
|
* Hook to detect if text content is wrapping to multiple lines
|
|
278
278
|
*
|
|
279
279
|
* @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
|
|
280
|
+
* @param {object} [options] - Configuration options
|
|
281
|
+
* @param {boolean} [options.skip] - Whether to skip observing for wrapping (default: false)
|
|
280
282
|
* @returns {{
|
|
281
283
|
* ref: RefObject<TElement | null>;
|
|
282
284
|
* isTooltipVisible: boolean;
|
|
@@ -284,7 +286,8 @@ const PackageNameStoryComponent = ({ packageJSON }) => {
|
|
|
284
286
|
* - `ref`: a ref to attach to the element you want to observe for truncation.
|
|
285
287
|
* - `isTextWrapping`: a boolean indicating if the text is wrapping.
|
|
286
288
|
*/
|
|
287
|
-
const useIsTextWrapping = () => {
|
|
289
|
+
const useIsTextWrapping = (options = {}) => {
|
|
290
|
+
const { skip = false } = options;
|
|
288
291
|
const ref = useRef(null);
|
|
289
292
|
const [isTextWrapping, setIsTextWrapping] = useState(false);
|
|
290
293
|
const setTextWrappingState = useCallback(() => {
|
|
@@ -295,7 +298,7 @@ const useIsTextWrapping = () => {
|
|
|
295
298
|
setIsTextWrapping(clientHeight > scrollHeight / 2);
|
|
296
299
|
}, []);
|
|
297
300
|
useEffect(() => {
|
|
298
|
-
if (!ref.current) {
|
|
301
|
+
if (skip || !ref.current) {
|
|
299
302
|
return;
|
|
300
303
|
}
|
|
301
304
|
// Perform an immediate measurement on mount.
|
|
@@ -309,8 +312,8 @@ const useIsTextWrapping = () => {
|
|
|
309
312
|
observer.observe(ref.current);
|
|
310
313
|
// Clean up on unmount
|
|
311
314
|
return () => observer.disconnect();
|
|
312
|
-
}, [setTextWrappingState]);
|
|
313
|
-
return { ref, isTextWrapping };
|
|
315
|
+
}, [setTextWrappingState, skip]);
|
|
316
|
+
return useMemo(() => ({ ref, isTextWrapping }), [isTextWrapping]);
|
|
314
317
|
};
|
|
315
318
|
|
|
316
319
|
const cvaText = cvaMerge(["text-black", "m-0", "relative", "text-sm", "font-normal"], {
|
|
@@ -1072,6 +1075,8 @@ const createBreakpointState = ({ width }) => {
|
|
|
1072
1075
|
* an object with boolean values indicating which breakpoints are currently active.
|
|
1073
1076
|
*
|
|
1074
1077
|
* @param {RefObject<HTMLElement>} ref - Reference to the container element to observe
|
|
1078
|
+
* @param {object} [options] - Configuration options
|
|
1079
|
+
* @param {boolean} [options.skip] - Whether to skip observing for breakpoint changes (default: false)
|
|
1075
1080
|
* @returns {BreakpointState} An object containing boolean values for each container size breakpoint.
|
|
1076
1081
|
* @example
|
|
1077
1082
|
* const MyComponent = () => {
|
|
@@ -1091,7 +1096,8 @@ const createBreakpointState = ({ width }) => {
|
|
|
1091
1096
|
* );
|
|
1092
1097
|
* }
|
|
1093
1098
|
*/
|
|
1094
|
-
const useContainerBreakpoints = (ref) => {
|
|
1099
|
+
const useContainerBreakpoints = (ref, options = {}) => {
|
|
1100
|
+
const { skip = false } = options;
|
|
1095
1101
|
const [containerSize, setContainerSize] = useState(() => defaultBreakpointState);
|
|
1096
1102
|
useEffect(() => {
|
|
1097
1103
|
if (process.env.NODE_ENV === "development" && !ref.current) {
|
|
@@ -1108,6 +1114,9 @@ const useContainerBreakpoints = (ref) => {
|
|
|
1108
1114
|
setContainerSize(createBreakpointState({ width }));
|
|
1109
1115
|
}, [ref]);
|
|
1110
1116
|
useEffect(() => {
|
|
1117
|
+
if (skip) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1111
1120
|
const element = ref.current;
|
|
1112
1121
|
if (!element) {
|
|
1113
1122
|
return;
|
|
@@ -1120,7 +1129,7 @@ const useContainerBreakpoints = (ref) => {
|
|
|
1120
1129
|
return () => {
|
|
1121
1130
|
resizeObserver.disconnect();
|
|
1122
1131
|
};
|
|
1123
|
-
}, [updateContainerSize, ref]);
|
|
1132
|
+
}, [updateContainerSize, ref, skip]);
|
|
1124
1133
|
return containerSize;
|
|
1125
1134
|
};
|
|
1126
1135
|
|
|
@@ -1132,7 +1141,7 @@ const useContainerBreakpoints = (ref) => {
|
|
|
1132
1141
|
* @param {number} options.duration - Duration of the timeout in milliseconds.
|
|
1133
1142
|
* @returns {object} An object containing functions to start and stop the timeout.
|
|
1134
1143
|
*/
|
|
1135
|
-
const useTimeout = ({ onTimeout, duration }) => {
|
|
1144
|
+
const useTimeout = ({ onTimeout, duration, }) => {
|
|
1136
1145
|
const ready = useRef(false);
|
|
1137
1146
|
const timeout = useRef(null);
|
|
1138
1147
|
const callback = useRef(onTimeout);
|
|
@@ -1164,7 +1173,7 @@ const useTimeout = ({ onTimeout, duration }) => {
|
|
|
1164
1173
|
}
|
|
1165
1174
|
};
|
|
1166
1175
|
}, []);
|
|
1167
|
-
return { startTimeout, stopTimeout };
|
|
1176
|
+
return useMemo(() => ({ startTimeout, stopTimeout }), [startTimeout, stopTimeout]);
|
|
1168
1177
|
};
|
|
1169
1178
|
|
|
1170
1179
|
/**
|
|
@@ -1177,20 +1186,9 @@ const useTimeout = ({ onTimeout, duration }) => {
|
|
|
1177
1186
|
* @param {number} options.maxRetries - Maximum number of retry attempts.
|
|
1178
1187
|
* @returns {object} An object containing functions to start and stop the timeout, current retry count, and the timeout status.
|
|
1179
1188
|
*/
|
|
1180
|
-
const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries }) => {
|
|
1189
|
+
const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }) => {
|
|
1181
1190
|
const retries = useRef(0);
|
|
1182
1191
|
const [isRunning, setIsRunning] = useState(false); // Track the timeout status
|
|
1183
|
-
const stopTimeouts = () => {
|
|
1184
|
-
stopTimeout();
|
|
1185
|
-
setIsRunning(false); // Update the status when stopped
|
|
1186
|
-
};
|
|
1187
|
-
const startTimeouts = () => {
|
|
1188
|
-
if (isRunning) {
|
|
1189
|
-
return; // Prevent multiple timeouts from running
|
|
1190
|
-
}
|
|
1191
|
-
startTimeout();
|
|
1192
|
-
setIsRunning(true); // Update the status when started
|
|
1193
|
-
};
|
|
1194
1192
|
const { startTimeout, stopTimeout } = useTimeout({
|
|
1195
1193
|
duration,
|
|
1196
1194
|
onTimeout: () => {
|
|
@@ -1207,14 +1205,25 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries })
|
|
|
1207
1205
|
}
|
|
1208
1206
|
},
|
|
1209
1207
|
});
|
|
1210
|
-
|
|
1208
|
+
const stopTimeouts = useCallback(() => {
|
|
1209
|
+
stopTimeout();
|
|
1210
|
+
setIsRunning(false); // Update the status when stopped
|
|
1211
|
+
}, [stopTimeout]);
|
|
1212
|
+
const startTimeouts = useCallback(() => {
|
|
1213
|
+
if (isRunning) {
|
|
1214
|
+
return; // Prevent multiple timeouts from running
|
|
1215
|
+
}
|
|
1216
|
+
startTimeout();
|
|
1217
|
+
setIsRunning(true); // Update the status when started
|
|
1218
|
+
}, [isRunning, startTimeout]);
|
|
1219
|
+
return useMemo(() => ({
|
|
1211
1220
|
startTimeouts,
|
|
1212
1221
|
stopTimeouts,
|
|
1213
1222
|
isRunning,
|
|
1214
1223
|
get retries() {
|
|
1215
1224
|
return retries.current;
|
|
1216
1225
|
},
|
|
1217
|
-
};
|
|
1226
|
+
}), [startTimeouts, stopTimeouts, isRunning]);
|
|
1218
1227
|
};
|
|
1219
1228
|
|
|
1220
1229
|
/**
|
|
@@ -1262,8 +1271,11 @@ const useDebounce = (value, delay = 500, direction, onBounce) => {
|
|
|
1262
1271
|
function useDevicePixelRatio(options) {
|
|
1263
1272
|
const dpr = getDevicePixelRatio(options);
|
|
1264
1273
|
const [currentDpr, setCurrentDpr] = useState(dpr);
|
|
1265
|
-
const { defaultDpr, maxDpr, round } = options || {};
|
|
1274
|
+
const { defaultDpr, maxDpr, round, skip = false } = options || {};
|
|
1266
1275
|
useEffect(() => {
|
|
1276
|
+
if (skip) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1267
1279
|
const canListen = typeof window !== "undefined" && "matchMedia" in window;
|
|
1268
1280
|
if (!canListen) {
|
|
1269
1281
|
return;
|
|
@@ -1274,7 +1286,7 @@ function useDevicePixelRatio(options) {
|
|
|
1274
1286
|
return () => {
|
|
1275
1287
|
mediaMatcher.removeEventListener("change", updateDpr);
|
|
1276
1288
|
};
|
|
1277
|
-
}, [currentDpr, defaultDpr, maxDpr, round]);
|
|
1289
|
+
}, [currentDpr, defaultDpr, maxDpr, round, skip]);
|
|
1278
1290
|
return currentDpr;
|
|
1279
1291
|
}
|
|
1280
1292
|
/**
|
|
@@ -1301,8 +1313,8 @@ function getDevicePixelRatio(options) {
|
|
|
1301
1313
|
* const [state, dispatch] = useElevatedReducer(reducer, initialState, elevatedReducerState);
|
|
1302
1314
|
*/
|
|
1303
1315
|
const useElevatedReducer = (reducer, initialState, customState) => {
|
|
1304
|
-
const
|
|
1305
|
-
return customState ??
|
|
1316
|
+
const [fallbackValue, fallbackDispatch] = useReducer(reducer, initialState);
|
|
1317
|
+
return useMemo(() => customState ?? [fallbackValue, fallbackDispatch], [customState, fallbackValue, fallbackDispatch]);
|
|
1306
1318
|
};
|
|
1307
1319
|
|
|
1308
1320
|
/**
|
|
@@ -1313,15 +1325,16 @@ const useElevatedReducer = (reducer, initialState, customState) => {
|
|
|
1313
1325
|
* If no custom state is provided, the fallback state will be used and it works like a normal useState hook.
|
|
1314
1326
|
*/
|
|
1315
1327
|
const useElevatedState = (initialState, customState) => {
|
|
1316
|
-
const
|
|
1317
|
-
return useMemo(() => customState ??
|
|
1328
|
+
const [fallbackValue, fallbackSetter] = useState(initialState);
|
|
1329
|
+
return useMemo(() => customState ?? [fallbackValue, fallbackSetter], [customState, fallbackValue, fallbackSetter]);
|
|
1318
1330
|
};
|
|
1319
1331
|
|
|
1332
|
+
const UNINITIALIZED = Symbol("UNINITIALIZED");
|
|
1320
1333
|
/**
|
|
1321
1334
|
* Custom hook to get the geometry of an element.
|
|
1322
1335
|
* Size and position of the element relative to the viewport.
|
|
1323
1336
|
*/
|
|
1324
|
-
const useGeometry = (ref, { skip = false } = {}) => {
|
|
1337
|
+
const useGeometry = (ref, { skip = false, onChange } = {}) => {
|
|
1325
1338
|
const [geometry, setGeometry] = useState(() => {
|
|
1326
1339
|
const rect = ref.current?.getBoundingClientRect();
|
|
1327
1340
|
if (!rect) {
|
|
@@ -1349,6 +1362,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1349
1362
|
});
|
|
1350
1363
|
const resizeObserver = useRef(null);
|
|
1351
1364
|
const [element, setElement] = useState(ref.current);
|
|
1365
|
+
const prevGeometry = useRef(UNINITIALIZED);
|
|
1352
1366
|
// Track changes to ref.current on every render
|
|
1353
1367
|
if (ref.current !== element) {
|
|
1354
1368
|
setElement(ref.current);
|
|
@@ -1359,7 +1373,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1359
1373
|
}
|
|
1360
1374
|
// Update geometry immediately when element changes
|
|
1361
1375
|
const elementRect = element.getBoundingClientRect();
|
|
1362
|
-
|
|
1376
|
+
const newGeometry = {
|
|
1363
1377
|
width: elementRect.width,
|
|
1364
1378
|
height: elementRect.height,
|
|
1365
1379
|
top: elementRect.top,
|
|
@@ -1368,13 +1382,23 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1368
1382
|
right: elementRect.right,
|
|
1369
1383
|
x: elementRect.x,
|
|
1370
1384
|
y: elementRect.y,
|
|
1371
|
-
}
|
|
1385
|
+
};
|
|
1386
|
+
const prev = prevGeometry.current;
|
|
1387
|
+
const hasChanged = prev === UNINITIALIZED ? false : !isEqual(newGeometry, prev);
|
|
1388
|
+
if (!hasChanged && prev !== UNINITIALIZED) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
setGeometry(newGeometry);
|
|
1392
|
+
if (hasChanged && prev !== UNINITIALIZED) {
|
|
1393
|
+
onChange?.(newGeometry);
|
|
1394
|
+
}
|
|
1395
|
+
prevGeometry.current = newGeometry;
|
|
1372
1396
|
const observe = () => {
|
|
1373
1397
|
if (!resizeObserver.current) {
|
|
1374
1398
|
resizeObserver.current = new ResizeObserver(entries => {
|
|
1375
1399
|
for (const entry of entries) {
|
|
1376
1400
|
const entryRect = entry.target.getBoundingClientRect();
|
|
1377
|
-
|
|
1401
|
+
const observedGeometry = {
|
|
1378
1402
|
width: entryRect.width,
|
|
1379
1403
|
height: entryRect.height,
|
|
1380
1404
|
top: entryRect.top,
|
|
@@ -1383,7 +1407,16 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1383
1407
|
right: entryRect.right,
|
|
1384
1408
|
x: entryRect.x,
|
|
1385
1409
|
y: entryRect.y,
|
|
1386
|
-
}
|
|
1410
|
+
};
|
|
1411
|
+
const prevObserved = prevGeometry.current;
|
|
1412
|
+
const hasObservedChanged = prevObserved === UNINITIALIZED ? false : !isEqual(observedGeometry, prevObserved);
|
|
1413
|
+
if (hasObservedChanged || prevObserved === UNINITIALIZED) {
|
|
1414
|
+
setGeometry(observedGeometry);
|
|
1415
|
+
if (hasObservedChanged && prevObserved !== UNINITIALIZED) {
|
|
1416
|
+
onChange?.(observedGeometry);
|
|
1417
|
+
}
|
|
1418
|
+
prevGeometry.current = observedGeometry;
|
|
1419
|
+
}
|
|
1387
1420
|
}
|
|
1388
1421
|
});
|
|
1389
1422
|
}
|
|
@@ -1395,7 +1428,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1395
1428
|
resizeObserver.current.disconnect();
|
|
1396
1429
|
}
|
|
1397
1430
|
};
|
|
1398
|
-
}, [element, skip]);
|
|
1431
|
+
}, [element, onChange, skip]);
|
|
1399
1432
|
return geometry;
|
|
1400
1433
|
};
|
|
1401
1434
|
|
|
@@ -1410,13 +1443,17 @@ const useGeometry = (ref, { skip = false } = {}) => {
|
|
|
1410
1443
|
const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
|
|
1411
1444
|
const [hovering, setHovering] = useState(false);
|
|
1412
1445
|
const debouncedHovering = useDebounce(hovering, delay, direction);
|
|
1413
|
-
const onMouseEnter = () => {
|
|
1446
|
+
const onMouseEnter = useCallback(() => {
|
|
1414
1447
|
setHovering(true);
|
|
1415
|
-
};
|
|
1416
|
-
const onMouseLeave = () => {
|
|
1448
|
+
}, []);
|
|
1449
|
+
const onMouseLeave = useCallback(() => {
|
|
1417
1450
|
setHovering(false);
|
|
1418
|
-
};
|
|
1419
|
-
return
|
|
1451
|
+
}, []);
|
|
1452
|
+
return useMemo(() => ({
|
|
1453
|
+
onMouseEnter,
|
|
1454
|
+
onMouseLeave,
|
|
1455
|
+
hovering: debounced ? debouncedHovering : hovering,
|
|
1456
|
+
}), [onMouseEnter, onMouseLeave, debounced, debouncedHovering, hovering]);
|
|
1420
1457
|
};
|
|
1421
1458
|
|
|
1422
1459
|
const OVERSCAN = 10;
|
|
@@ -1431,6 +1468,7 @@ const DEFAULT_ROW_HEIGHT = 50;
|
|
|
1431
1468
|
* @param props.estimateSize - Optional function to estimate item height.
|
|
1432
1469
|
* @param props.overscan - Optional number of items to render outside the visible area.
|
|
1433
1470
|
* @param props.onChange - Optional callback when virtualizer changes.
|
|
1471
|
+
* @param props.skip - Whether to skip automatic loading of more data (default: false).
|
|
1434
1472
|
* @returns {Virtualizer} The virtualizer instance with all its properties and methods.
|
|
1435
1473
|
* @description
|
|
1436
1474
|
* This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
|
|
@@ -1444,7 +1482,7 @@ const DEFAULT_ROW_HEIGHT = 50;
|
|
|
1444
1482
|
* estimateSize: () => 35,
|
|
1445
1483
|
* });
|
|
1446
1484
|
*/
|
|
1447
|
-
const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, }) => {
|
|
1485
|
+
const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, }) => {
|
|
1448
1486
|
const handleChange = useCallback((virtualizer) => {
|
|
1449
1487
|
onChange?.(virtualizer);
|
|
1450
1488
|
}, [onChange]);
|
|
@@ -1462,7 +1500,7 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
|
|
|
1462
1500
|
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
1463
1501
|
// Auto-load more data based on scroll position and content height
|
|
1464
1502
|
useEffect(() => {
|
|
1465
|
-
if (pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
|
|
1503
|
+
if (skip || pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
|
|
1466
1504
|
return;
|
|
1467
1505
|
}
|
|
1468
1506
|
const container = scrollElementRef.current;
|
|
@@ -1478,6 +1516,7 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
|
|
|
1478
1516
|
pagination.nextPage();
|
|
1479
1517
|
}
|
|
1480
1518
|
}, [
|
|
1519
|
+
skip,
|
|
1481
1520
|
pagination.pageInfo?.hasNextPage,
|
|
1482
1521
|
pagination.nextPage,
|
|
1483
1522
|
pagination.isLoading,
|
|
@@ -1525,6 +1564,8 @@ const useIsFullscreen = () => {
|
|
|
1525
1564
|
* @template TElement - The type of the HTML element being observed (e.g., HTMLDivElement).
|
|
1526
1565
|
* @param {string} [text] - (Optional) Text used to trigger a re-check of truncation,
|
|
1527
1566
|
* especially if the text is dynamic (such as an input's value).
|
|
1567
|
+
* @param {object} [options] - Configuration options
|
|
1568
|
+
* @param {boolean} [options.skip] - Whether to skip observing for truncation (default: false)
|
|
1528
1569
|
* @returns {{
|
|
1529
1570
|
* ref: Ref<TElement | null>;
|
|
1530
1571
|
* isTooltipVisible: boolean;
|
|
@@ -1532,7 +1573,7 @@ const useIsFullscreen = () => {
|
|
|
1532
1573
|
* - `ref`: a ref to attach to the element you want to observe for truncation.
|
|
1533
1574
|
* - `isTextTruncated`: a boolean indicating if the text is truncated.
|
|
1534
1575
|
*/
|
|
1535
|
-
const useIsTextTruncated = (text) => {
|
|
1576
|
+
const useIsTextTruncated = (text, { skip = false } = {}) => {
|
|
1536
1577
|
const ref = useRef(null);
|
|
1537
1578
|
const [isTextTruncated, setIsTextTruncated] = useState(false);
|
|
1538
1579
|
const updateTextVisibility = useCallback(() => {
|
|
@@ -1543,9 +1584,11 @@ const useIsTextTruncated = (text) => {
|
|
|
1543
1584
|
setIsTextTruncated(scrollWidth > clientWidth);
|
|
1544
1585
|
}, []);
|
|
1545
1586
|
useEffect(() => {
|
|
1546
|
-
if (!ref.current) {
|
|
1587
|
+
if (skip || !ref.current) {
|
|
1547
1588
|
return;
|
|
1548
1589
|
}
|
|
1590
|
+
// Perform an immediate measurement on mount
|
|
1591
|
+
updateTextVisibility();
|
|
1549
1592
|
// Observe resizing to check if truncation changes
|
|
1550
1593
|
const observer = new ResizeObserver(() => {
|
|
1551
1594
|
updateTextVisibility();
|
|
@@ -1553,15 +1596,15 @@ const useIsTextTruncated = (text) => {
|
|
|
1553
1596
|
observer.observe(ref.current);
|
|
1554
1597
|
// Clean up on unmount
|
|
1555
1598
|
return () => observer.disconnect();
|
|
1556
|
-
}, [updateTextVisibility]);
|
|
1599
|
+
}, [updateTextVisibility, skip]);
|
|
1557
1600
|
// Re-check whenever text changes
|
|
1558
1601
|
useEffect(() => {
|
|
1559
|
-
if (text === undefined || text === "") {
|
|
1602
|
+
if (skip || text === undefined || text === "") {
|
|
1560
1603
|
return;
|
|
1561
1604
|
}
|
|
1562
1605
|
updateTextVisibility();
|
|
1563
|
-
}, [text, updateTextVisibility]);
|
|
1564
|
-
return { ref, isTextTruncated };
|
|
1606
|
+
}, [text, updateTextVisibility, skip]);
|
|
1607
|
+
return useMemo(() => ({ ref, isTextTruncated }), [isTextTruncated]);
|
|
1565
1608
|
};
|
|
1566
1609
|
|
|
1567
1610
|
/**
|
|
@@ -1690,11 +1733,11 @@ const SCROLL_DEBOUNCE_TIME = 50;
|
|
|
1690
1733
|
* Hook for detecting scroll values in horizontal or vertical direction.
|
|
1691
1734
|
*
|
|
1692
1735
|
* @param {useRef} elementRef - Ref hook holding the element that needs to be observed during scrolling
|
|
1693
|
-
* @param {object} options - Options object containing direction
|
|
1736
|
+
* @param {object} options - Options object containing direction, onScrollStateChange callback, and skip
|
|
1694
1737
|
* @returns {object} An object containing if the element is scrollable, is at the beginning, is at the end, and its current scroll position.
|
|
1695
1738
|
*/
|
|
1696
1739
|
const useScrollDetection = (elementRef, options) => {
|
|
1697
|
-
const { direction = "vertical", onScrollStateChange } = options ?? {};
|
|
1740
|
+
const { direction = "vertical", onScrollStateChange, skip = false } = options ?? {};
|
|
1698
1741
|
const [isScrollable, setIsScrollable] = useState(false);
|
|
1699
1742
|
const [isAtBeginning, setIsAtBeginning] = useState(true);
|
|
1700
1743
|
const [isAtEnd, setIsAtEnd] = useState(false);
|
|
@@ -1755,6 +1798,9 @@ const useScrollDetection = (elementRef, options) => {
|
|
|
1755
1798
|
});
|
|
1756
1799
|
}, [isScrollable, isAtBeginning, isAtEnd, scrollPosition, onScrollStateChange, isFirstRender]);
|
|
1757
1800
|
useEffect(() => {
|
|
1801
|
+
if (skip) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1758
1804
|
const element = elementRef?.current;
|
|
1759
1805
|
if (!element) {
|
|
1760
1806
|
return;
|
|
@@ -1774,7 +1820,7 @@ const useScrollDetection = (elementRef, options) => {
|
|
|
1774
1820
|
}
|
|
1775
1821
|
element.removeEventListener("scroll", debouncedCheckScrollPosition);
|
|
1776
1822
|
};
|
|
1777
|
-
}, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition]);
|
|
1823
|
+
}, [elementRef, checkScrollable, checkScrollPosition, debouncedCheckScrollPosition, skip]);
|
|
1778
1824
|
return useMemo(() => ({ isScrollable, isAtBeginning, isAtEnd, scrollPosition }), [isScrollable, isAtBeginning, isAtEnd, scrollPosition]);
|
|
1779
1825
|
};
|
|
1780
1826
|
|
|
@@ -1798,6 +1844,8 @@ const useSelfUpdatingRef = (initialState) => {
|
|
|
1798
1844
|
* This hook listens to changes in the viewport size and returns an object with boolean values
|
|
1799
1845
|
* indicating which breakpoints are currently active.
|
|
1800
1846
|
*
|
|
1847
|
+
* @param {object} [options] - Configuration options
|
|
1848
|
+
* @param {boolean} [options.skip] - Whether to skip observing for viewport changes (default: false)
|
|
1801
1849
|
* @returns {BreakpointState} An object containing boolean values for each viewport size breakpoint.
|
|
1802
1850
|
* @example
|
|
1803
1851
|
* const MyComponent = () => {
|
|
@@ -1810,7 +1858,8 @@ const useSelfUpdatingRef = (initialState) => {
|
|
|
1810
1858
|
* return viewportSize.isMd ? <MediumScreenLayout /> : <SmallLayout />;
|
|
1811
1859
|
* }
|
|
1812
1860
|
*/
|
|
1813
|
-
const useViewportBreakpoints = () => {
|
|
1861
|
+
const useViewportBreakpoints = (options = {}) => {
|
|
1862
|
+
const { skip = false } = options;
|
|
1814
1863
|
const [viewportSize, setViewportSize] = useState(() => {
|
|
1815
1864
|
const newViewportSize = objectEntries(themeScreenSizeAsNumber).reduce((acc, [size, minWidth]) => ({
|
|
1816
1865
|
...acc,
|
|
@@ -1826,6 +1875,9 @@ const useViewportBreakpoints = () => {
|
|
|
1826
1875
|
setViewportSize(newViewportSize);
|
|
1827
1876
|
}, []);
|
|
1828
1877
|
useEffect(() => {
|
|
1878
|
+
if (skip) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1829
1881
|
// Initial check
|
|
1830
1882
|
updateViewportSize();
|
|
1831
1883
|
// Set up listeners for each breakpoint
|
|
@@ -1839,7 +1891,7 @@ const useViewportBreakpoints = () => {
|
|
|
1839
1891
|
mql.removeEventListener("change", updateViewportSize);
|
|
1840
1892
|
});
|
|
1841
1893
|
};
|
|
1842
|
-
}, [updateViewportSize]);
|
|
1894
|
+
}, [updateViewportSize, skip]);
|
|
1843
1895
|
return viewportSize;
|
|
1844
1896
|
};
|
|
1845
1897
|
|
|
@@ -1847,7 +1899,7 @@ const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
|
|
|
1847
1899
|
/**
|
|
1848
1900
|
* Use this hook to disable functionality while the tab is hidden within the browser or to react to focus or blur events
|
|
1849
1901
|
*/
|
|
1850
|
-
const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: undefined }) => {
|
|
1902
|
+
const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefined, onFocus: undefined }) => {
|
|
1851
1903
|
const [focused, setFocused] = useState(hasFocus());
|
|
1852
1904
|
const onFocusInternal = useCallback(() => {
|
|
1853
1905
|
setFocused(hasFocus());
|
|
@@ -1858,6 +1910,9 @@ const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: u
|
|
|
1858
1910
|
onBlur?.();
|
|
1859
1911
|
}, [onBlur]);
|
|
1860
1912
|
useEffect(() => {
|
|
1913
|
+
if (skip) {
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1861
1916
|
setFocused(hasFocus()); // Focus for additional renders
|
|
1862
1917
|
window.addEventListener("focus", onFocusInternal);
|
|
1863
1918
|
window.addEventListener("blur", onBlurInternal);
|
|
@@ -1865,7 +1920,7 @@ const useWindowActivity = ({ onFocus, onBlur } = { onBlur: undefined, onFocus: u
|
|
|
1865
1920
|
window.removeEventListener("focus", onFocusInternal);
|
|
1866
1921
|
window.removeEventListener("blur", onBlurInternal);
|
|
1867
1922
|
};
|
|
1868
|
-
}, [onBlurInternal, onFocusInternal]);
|
|
1923
|
+
}, [onBlurInternal, onFocusInternal, skip]);
|
|
1869
1924
|
return useMemo(() => ({ focused }), [focused]);
|
|
1870
1925
|
};
|
|
1871
1926
|
|
|
@@ -4482,7 +4537,7 @@ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
|
|
|
4482
4537
|
}, prefix: prefixIconName ? jsx(Icon, { name: prefixIconName, size: "medium" }) : null, variant: variant === "secondary-danger" ? "danger" : "primary" })) : (jsx(Button, { dataTestId: dataTestId, disabled: disabled, onClick: e => {
|
|
4483
4538
|
actionCallback?.(e);
|
|
4484
4539
|
externalOnClick?.();
|
|
4485
|
-
}, prefix: prefixIconName ? jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "
|
|
4540
|
+
}, prefix: prefixIconName ? jsx(Icon, { name: prefixIconName, size: "small" }) : undefined, size: "small", variant: variant, children: actionText }));
|
|
4486
4541
|
// Wrap `content` with Tooltip
|
|
4487
4542
|
const wrappedWithTooltip = tooltipLabel ? (jsx(Tooltip, { className: "block", label: tooltipLabel, children: content })) : (content);
|
|
4488
4543
|
// Finally, wrap with Link if `to` is provided
|
|
@@ -4494,12 +4549,17 @@ function ActionRenderer({ action, isMenuItem = false, externalOnClick }) {
|
|
|
4494
4549
|
* @param {object} props - The props for the PageHeaderSecondaryActions component
|
|
4495
4550
|
* @param {Array<PageHeaderSecondaryActionType>} props.actions - The secondary actions to render
|
|
4496
4551
|
* @param {boolean} [props.hasPrimaryAction] - Whether there is a primary action present
|
|
4497
|
-
* @
|
|
4552
|
+
* @param {boolean} [props.groupActions] - Whether to group actions in a More Menu regardless of action count
|
|
4553
|
+
* @returns {ReactElement | null} PageHeaderSecondaryActions component
|
|
4498
4554
|
*/
|
|
4499
|
-
const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction = false, }) => {
|
|
4555
|
+
const PageHeaderSecondaryActions = ({ actions, hasPrimaryAction = false, groupActions = false, }) => {
|
|
4500
4556
|
const enabledActions = useMemo(() => actions.filter(action => action.hidden === false || action.hidden === undefined), [actions]);
|
|
4501
|
-
// If
|
|
4502
|
-
if (enabledActions.length
|
|
4557
|
+
// If there are no enabled actions, don't render anything
|
|
4558
|
+
if (enabledActions.length === 0) {
|
|
4559
|
+
return null;
|
|
4560
|
+
}
|
|
4561
|
+
// If we need to render a "More Menu" because we have too many actions or grouping is requested:
|
|
4562
|
+
if (groupActions || enabledActions.length > 2 || (hasPrimaryAction && enabledActions.length > 1)) {
|
|
4503
4563
|
// Separate them into danger vs. other
|
|
4504
4564
|
const [dangerActions, otherActions] = enabledActions.reduce(([danger, others], action) => {
|
|
4505
4565
|
if (action.variant === "secondary-danger") {
|
|
@@ -4559,7 +4619,7 @@ const PageHeaderTitle = ({ title, dataTestId }) => {
|
|
|
4559
4619
|
* @param {PageHeaderProps} props - The props for the PageHeader component
|
|
4560
4620
|
* @returns {ReactElement} PageHeader component
|
|
4561
4621
|
*/
|
|
4562
|
-
const PageHeader = ({ className, dataTestId,
|
|
4622
|
+
const PageHeader = ({ className, dataTestId, showLoading = false, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon = "QuestionMarkCircle", tagTooltipLabel, ...discriminatedProps }) => {
|
|
4563
4623
|
const tagRenderer = useMemo(() => {
|
|
4564
4624
|
if (tagLabel === undefined || tagLabel === "" || showLoading) {
|
|
4565
4625
|
return null;
|
|
@@ -4570,10 +4630,14 @@ const PageHeader = ({ className, dataTestId, secondaryActions, showLoading = fal
|
|
|
4570
4630
|
return (jsxs("div", { className: cvaPageHeaderContainer({
|
|
4571
4631
|
className,
|
|
4572
4632
|
withBorder: tabsList === undefined,
|
|
4573
|
-
}), "data-testid": dataTestId, children: [jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsx(Link, { to: backTo, children: jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "
|
|
4633
|
+
}), "data-testid": dataTestId, children: [jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsx(Link, { to: backTo, children: jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsx(PageHeaderTitle, { dataTestId: dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsx(Tooltip, { dataTestId: dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
|
|
4574
4634
|
name: descriptionIcon,
|
|
4575
4635
|
dataTestId: "page-header-description-icon",
|
|
4576
|
-
}, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxs("div", { className: "ml-auto flex gap-2", children: [
|
|
4636
|
+
}, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxs("div", { className: "ml-auto flex gap-2", children: [discriminatedProps.accessoryType === "kpi-metrics" ? (jsx(PageHeaderKpiMetrics, { kpiMetrics: discriminatedProps.kpiMetrics })) : null, discriminatedProps.accessoryType === "actions" ? (Array.isArray(discriminatedProps.secondaryActions) ? (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" &&
|
|
4637
|
+
discriminatedProps.primaryAction !== undefined &&
|
|
4638
|
+
(discriminatedProps.primaryAction.hidden === false ||
|
|
4639
|
+
discriminatedProps.primaryAction.hidden === undefined) ? (jsx(Tooltip, { disabled: discriminatedProps.primaryAction.tooltipLabel === undefined ||
|
|
4640
|
+
discriminatedProps.primaryAction.tooltipLabel === "", label: discriminatedProps.primaryAction.tooltipLabel, children: jsx(Button, { dataTestId: discriminatedProps.primaryAction.dataTestId, disabled: discriminatedProps.primaryAction.disabled, loading: discriminatedProps.primaryAction.loading, onClick: () => discriminatedProps.primaryAction?.actionCallback?.(), prefix: discriminatedProps.primaryAction.prefixIconName !== undefined ? (jsx(Icon, { name: discriminatedProps.primaryAction.prefixIconName, size: "small" })) : undefined, size: "small", variant: discriminatedProps.primaryAction.variant, children: discriminatedProps.primaryAction.actionText }) })) : null] })] }), tabsList] }));
|
|
4577
4641
|
};
|
|
4578
4642
|
|
|
4579
4643
|
const cvaPagination = cvaMerge(["flex", "items-center", "gap-1"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.11",
|
|
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.
|
|
20
|
-
"@trackunit/css-class-variance-utilities": "1.7.
|
|
21
|
-
"@trackunit/shared-utils": "1.9.
|
|
22
|
-
"@trackunit/ui-icons": "1.7.
|
|
23
|
-
"@trackunit/react-test-setup": "1.4.
|
|
19
|
+
"@trackunit/ui-design-tokens": "1.7.46",
|
|
20
|
+
"@trackunit/css-class-variance-utilities": "1.7.46",
|
|
21
|
+
"@trackunit/shared-utils": "1.9.46",
|
|
22
|
+
"@trackunit/ui-icons": "1.7.47",
|
|
23
|
+
"@trackunit/react-test-setup": "1.4.46",
|
|
24
24
|
"@tanstack/react-router": "1.114.29",
|
|
25
25
|
"es-toolkit": "^1.39.10",
|
|
26
26
|
"@tanstack/react-virtual": "3.13.12"
|
|
@@ -10,4 +10,4 @@ import { PageHeaderProps } from "./types";
|
|
|
10
10
|
* @param {PageHeaderProps} props - The props for the PageHeader component
|
|
11
11
|
* @returns {ReactElement} PageHeader component
|
|
12
12
|
*/
|
|
13
|
-
export declare const PageHeader: ({ className, dataTestId,
|
|
13
|
+
export declare const PageHeader: ({ className, dataTestId, showLoading, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon, tagTooltipLabel, ...discriminatedProps }: PageHeaderProps) => ReactElement;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ReactElement } from "react";
|
|
2
2
|
import { PageHeaderSecondaryActionType } from "../types";
|
|
3
|
+
export type PageHeaderSecondaryActionsProps = {
|
|
4
|
+
actions: Array<PageHeaderSecondaryActionType>;
|
|
5
|
+
hasPrimaryAction?: boolean;
|
|
6
|
+
groupActions?: boolean;
|
|
7
|
+
};
|
|
3
8
|
type ActionRendererProps = {
|
|
4
9
|
action: PageHeaderSecondaryActionType;
|
|
5
10
|
/**
|
|
@@ -26,10 +31,8 @@ export declare function ActionRenderer({ action, isMenuItem, externalOnClick }:
|
|
|
26
31
|
* @param {object} props - The props for the PageHeaderSecondaryActions component
|
|
27
32
|
* @param {Array<PageHeaderSecondaryActionType>} props.actions - The secondary actions to render
|
|
28
33
|
* @param {boolean} [props.hasPrimaryAction] - Whether there is a primary action present
|
|
29
|
-
* @
|
|
34
|
+
* @param {boolean} [props.groupActions] - Whether to group actions in a More Menu regardless of action count
|
|
35
|
+
* @returns {ReactElement | null} PageHeaderSecondaryActions component
|
|
30
36
|
*/
|
|
31
|
-
export declare const PageHeaderSecondaryActions: ({ actions, hasPrimaryAction, }:
|
|
32
|
-
actions: Array<PageHeaderSecondaryActionType>;
|
|
33
|
-
hasPrimaryAction?: boolean;
|
|
34
|
-
}) => ReactElement;
|
|
37
|
+
export declare const PageHeaderSecondaryActions: ({ actions, hasPrimaryAction, groupActions, }: PageHeaderSecondaryActionsProps) => ReactElement | null;
|
|
35
38
|
export {};
|