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