@dbcdk/react-components 0.0.23 → 0.0.25
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/dist/components/datetime-picker/DateTimePicker.d.ts +1 -0
- package/dist/components/datetime-picker/DateTimePicker.js +24 -3
- package/dist/components/filter-field/FilterField.js +1 -1
- package/dist/components/filter-field/FilterField.module.css +10 -0
- package/dist/components/forms/input/Input.module.css +1 -0
- package/dist/components/overlay/modal/Modal.d.ts +2 -0
- package/dist/components/overlay/modal/Modal.js +1 -1
- package/dist/components/overlay/modal/Modal.module.css +1 -1
- package/dist/components/overlay/modal/provider/ModalProvider.d.ts +2 -0
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -2
- package/dist/components/table/Table.d.ts +2 -1
- package/dist/components/table/Table.js +12 -3
- package/dist/components/table/Table.module.css +19 -3
- package/dist/components/table/hooks/useAnimatedRowIds.d.ts +9 -0
- package/dist/components/table/hooks/useAnimatedRowIds.js +76 -0
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ function defaultFormatRange(s, e, opts) {
|
|
|
52
52
|
return '';
|
|
53
53
|
}
|
|
54
54
|
const cx = (...classes) => classes.filter(Boolean).join(' ');
|
|
55
|
-
export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate, formatRange = defaultFormatRange, }, _ref) {
|
|
55
|
+
export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate, formatRange = defaultFormatRange, onOpenChange, }, _ref) {
|
|
56
56
|
void formatDate;
|
|
57
57
|
void formatRange;
|
|
58
58
|
const popRef = useRef(null);
|
|
@@ -314,6 +314,19 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
314
314
|
setDirty(false);
|
|
315
315
|
}
|
|
316
316
|
}, [text, mode, enableTime, onChangeDateOnly, onChangeIso, onChangeRange]);
|
|
317
|
+
const emitTimeChange = useCallback((nextHH, nextMM) => {
|
|
318
|
+
if (mode !== 'single' || !enableTime)
|
|
319
|
+
return;
|
|
320
|
+
if (typeof value !== 'string')
|
|
321
|
+
return;
|
|
322
|
+
const current = localDateFromIso(value);
|
|
323
|
+
if (!current)
|
|
324
|
+
return;
|
|
325
|
+
const iso = isoFromLocalParts(current.getFullYear(), current.getMonth(), current.getDate(), nextHH, nextMM);
|
|
326
|
+
if (!iso)
|
|
327
|
+
return;
|
|
328
|
+
onChangeIso(iso);
|
|
329
|
+
}, [mode, enableTime, value, onChangeIso]);
|
|
317
330
|
const clear = useCallback(() => {
|
|
318
331
|
if (mode === 'single') {
|
|
319
332
|
if (enableTime)
|
|
@@ -328,7 +341,7 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
328
341
|
setText('');
|
|
329
342
|
}, [mode, enableTime, onChangeDateOnly, onChangeIso, onChangeRange]);
|
|
330
343
|
const fallbackPlaceholder = mode === 'single' ? 'Vælg dato' : 'Vælg interval';
|
|
331
|
-
return (_jsx(Popover, { matchTriggerWidth: false, ref: popRef, trigger: toggle => {
|
|
344
|
+
return (_jsx(Popover, { matchTriggerWidth: false, ref: popRef, onOpenChange: onOpenChange, trigger: toggle => {
|
|
332
345
|
var _a, _b;
|
|
333
346
|
return (_jsx("div", { onClick: toggle, className: styles.triggerWrap, children: _jsx(Input, { ...inputProps, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: "false", placeholder: (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder) !== null && _a !== void 0 ? _a : fallbackPlaceholder, value: dirty ? text : formatted, onInput: e => {
|
|
334
347
|
setDirty(true);
|
|
@@ -374,6 +387,14 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
374
387
|
}
|
|
375
388
|
const dayNum = utcDay.getUTCDate();
|
|
376
389
|
return (_jsx("button", { role: "gridcell", "aria-selected": selected, "aria-disabled": disabledDay, onMouseEnter: () => setHoverUTC(utcDay), onClick: () => selectDay(utcDay), disabled: disabledDay, className: cx(styles.dayCell, inThisMonth ? styles.dayInMonth : styles.dayOutside, selected && styles.daySelected, inRange && styles.dayInRange, isToday && !selected && styles.dayToday, disabledDay && styles.dayDisabled), title: new Date(utcDay).toLocaleDateString(locale), children: dayNum }, idx));
|
|
377
|
-
}) }), enableTime && mode === 'single' && (_jsxs("div", { className: styles.timeRow, children: [_jsxs("div", { className: styles.timeLabel, children: [_jsx(Clock, { size: 14 }), " Tid"] }), _jsx("select", { value: timeHH, onChange: e =>
|
|
390
|
+
}) }), enableTime && mode === 'single' && (_jsxs("div", { className: styles.timeRow, children: [_jsxs("div", { className: styles.timeLabel, children: [_jsx(Clock, { size: 14 }), " Tid"] }), _jsx("select", { value: timeHH, onChange: e => {
|
|
391
|
+
const nextHH = parseInt(e.target.value, 10);
|
|
392
|
+
setTimeHH(nextHH);
|
|
393
|
+
emitTimeChange(nextHH, timeMM);
|
|
394
|
+
}, className: styles.timeSelect, children: hours.map(h => (_jsx("option", { value: h, children: String(h).padStart(2, '0') }, h))) }), _jsx("select", { value: timeMM, onChange: e => {
|
|
395
|
+
const nextMM = parseInt(e.target.value, 10);
|
|
396
|
+
setTimeMM(nextMM);
|
|
397
|
+
emitTimeChange(timeHH, nextMM);
|
|
398
|
+
}, className: styles.timeSelect, children: minutes.map(m => (_jsx("option", { value: m, children: String(m).padStart(2, '0') }, m))) })] })), mode === 'range' && (_jsxs("div", { className: styles.footer, children: [_jsx(Button, { variant: "outlined", size: "sm", onClick: clear, icon: _jsx(X, { size: 14 }), children: "Ryd" }), _jsx(Button, { variant: "primary", size: "sm", onClick: () => { var _a; return (_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close(); }, children: "OK" })] }))] })] }) }));
|
|
378
399
|
});
|
|
379
400
|
DateTimePicker.displayName = 'DateTimePicker';
|
|
@@ -56,7 +56,7 @@ function OperatorDropdown({ value, onChange, operators, size = 'sm', disabled, }
|
|
|
56
56
|
setActiveIndex(operators.indexOf(op));
|
|
57
57
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
58
58
|
};
|
|
59
|
-
return (_jsx(Popover, { ref: popRef, minWidth: "220px", trigger: (toggle, icon) => (_jsxs("button", { type: "button", onClick: toggle, disabled: disabled, "aria-label": "
|
|
59
|
+
return (_jsx(Popover, { ref: popRef, minWidth: "220px", trigger: (toggle, icon) => (_jsxs("button", { type: "button", onClick: toggle, disabled: disabled, "aria-label": "Skift operator", className: `${styles.operatorTrigger} ${styles[size]}`, children: [_jsx("span", { className: styles.operatorText, children: LABELS[value] }), icon] })), children: _jsx(Menu, { children: operators.map(op => {
|
|
60
60
|
const selected = op === value;
|
|
61
61
|
return (_jsx(Menu.Item, { active: selected, children: _jsxs("button", { type: "button", onClick: () => handleSelect(op), disabled: disabled, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: selected ? _jsx(Check, { size: 16 }) : null }), LABELS[op]] }) }, op));
|
|
62
62
|
}) }) }));
|
|
@@ -264,6 +264,10 @@
|
|
|
264
264
|
);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
.operatorTrigger svg {
|
|
268
|
+
height: var(--component-size-xxs);
|
|
269
|
+
width: var(--component-size-xxs);
|
|
270
|
+
}
|
|
267
271
|
/* =========================
|
|
268
272
|
VALUE WRAPPER
|
|
269
273
|
========================= */
|
|
@@ -302,6 +306,12 @@
|
|
|
302
306
|
|
|
303
307
|
.filterField .operatorText {
|
|
304
308
|
white-space: nowrap;
|
|
309
|
+
font-family: var(--font-family-mono);
|
|
310
|
+
font-size: var(--font-size-xs);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.filterField input {
|
|
314
|
+
height: 100%;
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
.filterField input::placeholder {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
+
import { ButtonVariant } from '../../../components/button/Button';
|
|
2
3
|
import { Severity } from '../../../constants/severity.types';
|
|
3
4
|
export type ModalActionConfig = {
|
|
4
5
|
label: string;
|
|
6
|
+
severity?: ButtonVariant;
|
|
5
7
|
onClick?: () => void;
|
|
6
8
|
icon?: ReactNode;
|
|
7
9
|
disabled?: boolean;
|
|
@@ -88,5 +88,5 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
88
88
|
const resolvedWidth = typeof width === 'number' ? `${width}px` : typeof width === 'string' ? width : undefined;
|
|
89
89
|
return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, style: resolvedWidth
|
|
90
90
|
? { ['--modal-width']: resolvedWidth }
|
|
91
|
-
: undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button",
|
|
91
|
+
: undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: primaryAction.severity || 'primary', onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
|
|
92
92
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { ButtonVariant } from '../../../../components/button/Button';
|
|
2
3
|
import { type ModalProps } from '../Modal';
|
|
3
4
|
type ModalConfig = Omit<ModalProps, 'isOpen' | 'onRequestClose'> & {
|
|
4
5
|
onRequestClose?: () => void;
|
|
5
6
|
};
|
|
6
7
|
type ConfirmModalConfig = Omit<ModalConfig, 'primaryAction' | 'secondaryAction' | 'onRequestClose'> & {
|
|
7
8
|
confirmLabel?: string;
|
|
9
|
+
confirmButtonSeverity?: ButtonVariant;
|
|
8
10
|
cancelLabel?: string;
|
|
9
11
|
};
|
|
10
12
|
type ModalContextValue = {
|
|
@@ -9,7 +9,6 @@ export function ModalProvider({ children }) {
|
|
|
9
9
|
const [config, setConfig] = useState(null);
|
|
10
10
|
const [mounted, setMounted] = useState(false);
|
|
11
11
|
useEffect(() => setMounted(true), []);
|
|
12
|
-
// Holds the resolver for the current "confirm" call, if any
|
|
13
12
|
const pendingResolverRef = useRef(null);
|
|
14
13
|
const resolvePending = useCallback((value) => {
|
|
15
14
|
if (pendingResolverRef.current) {
|
|
@@ -21,7 +20,6 @@ export function ModalProvider({ children }) {
|
|
|
21
20
|
setIsOpen(false);
|
|
22
21
|
}, []);
|
|
23
22
|
const openModal = useCallback((newConfig) => {
|
|
24
|
-
// if a confirm was in progress, resolve it as "false" (cancelled/overridden)
|
|
25
23
|
resolvePending(false);
|
|
26
24
|
setConfig(newConfig);
|
|
27
25
|
setIsOpen(true);
|
|
@@ -41,6 +39,7 @@ export function ModalProvider({ children }) {
|
|
|
41
39
|
...rest,
|
|
42
40
|
primaryAction: {
|
|
43
41
|
label: confirmLabel,
|
|
42
|
+
severity: confirmConfig.confirmButtonSeverity || 'primary',
|
|
44
43
|
onClick: () => {
|
|
45
44
|
resolvePending(true);
|
|
46
45
|
},
|
|
@@ -59,6 +59,7 @@ export type TableProps<T extends Record<string, any>> = Omit<HTMLAttributes<HTML
|
|
|
59
59
|
showFirstLast?: boolean;
|
|
60
60
|
viewMode?: ViewMode;
|
|
61
61
|
emptyConfig?: TableEmptyConfig;
|
|
62
|
+
animateNewRows?: boolean;
|
|
62
63
|
};
|
|
63
|
-
export declare function Table<T extends Record<string, any>>({ data, columns, selectedRows, onRowSelect, selectionMode, onSortChange, onRowClick, onRowMouseEnter, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement, totalItemsCount, onPageChange, loading, variant, size, getRowSeverity, showFirstLast, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }: TableProps<T>): JSX.Element;
|
|
64
|
+
export declare function Table<T extends Record<string, any>>({ data, columns, selectedRows, onRowSelect, selectionMode, onSortChange, onRowClick, onRowMouseEnter, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement, totalItemsCount, onPageChange, loading, variant, size, getRowSeverity, showFirstLast, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, animateNewRows, ...rest }: TableProps<T>): JSX.Element;
|
|
64
65
|
export {};
|
|
@@ -7,6 +7,7 @@ import { Pagination } from '../../components/pagination/Pagination';
|
|
|
7
7
|
import { SkeletonLoaderItem } from '../../components/skeleton-loader/skeleton-loader-item/SkeletonLoaderItem';
|
|
8
8
|
import { SeverityBgColor } from '../../constants/severity';
|
|
9
9
|
import { TableEmptyState } from './components/empty-state/EmptyState';
|
|
10
|
+
import { useAnimatedNewRowIds } from './hooks/useAnimatedRowIds';
|
|
10
11
|
import styles from './Table.module.css';
|
|
11
12
|
import { getAriaSort, getCellDisplayValue, getHeaderLabel, getNextSortDirection, getVisibleColumns, isModifierClick, shouldAllowWrap, shouldToggleOnKey, isActiveSort, } from './table.utils';
|
|
12
13
|
function buildDefaultGridTemplate(args) {
|
|
@@ -18,13 +19,19 @@ function buildDefaultGridTemplate(args) {
|
|
|
18
19
|
parts.push('minmax(120px, 1fr)');
|
|
19
20
|
return parts.join(' ');
|
|
20
21
|
}
|
|
21
|
-
export function Table({ data, columns, selectedRows, onRowSelect, selectionMode = 'single', onSortChange, onRowClick, onRowMouseEnter, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport = false, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement = 'bottom', totalItemsCount, onPageChange, loading, variant = 'primary', size = 'md', getRowSeverity, showFirstLast = false, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }) {
|
|
22
|
+
export function Table({ data, columns, selectedRows, onRowSelect, selectionMode = 'single', onSortChange, onRowClick, onRowMouseEnter, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport = false, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement = 'bottom', totalItemsCount, onPageChange, loading, variant = 'primary', size = 'md', getRowSeverity, showFirstLast = false, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, animateNewRows = true, ...rest }) {
|
|
22
23
|
void viewportBottomOffset;
|
|
23
24
|
void viewportMin;
|
|
24
25
|
void viewportIncludeMarginTop;
|
|
25
26
|
const filteredColumns = useMemo(() => getVisibleColumns(columns), [columns]);
|
|
26
27
|
const handlePageChange = useCallback((e) => onPageChange === null || onPageChange === void 0 ? void 0 : onPageChange(e), [onPageChange]);
|
|
27
28
|
const hasSelection = Boolean(selectedRows && onRowSelect && dataKey);
|
|
29
|
+
const newRowIds = useAnimatedNewRowIds({
|
|
30
|
+
data,
|
|
31
|
+
dataKey,
|
|
32
|
+
enabled: animateNewRows,
|
|
33
|
+
animationDurationMs: 1000,
|
|
34
|
+
});
|
|
28
35
|
const template = useMemo(() => {
|
|
29
36
|
return (gridTemplateColumns !== null && gridTemplateColumns !== void 0 ? gridTemplateColumns : buildDefaultGridTemplate({ hasSelection, colCount: filteredColumns.length }));
|
|
30
37
|
}, [gridTemplateColumns, hasSelection, filteredColumns.length]);
|
|
@@ -94,13 +101,15 @@ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode
|
|
|
94
101
|
})] }, `loading-row-${rowIndex}`))) }));
|
|
95
102
|
const dataBodyEl = (_jsx("div", { className: `${styles.body} ${striped ? styles.striped : ''}`, role: "rowgroup", children: data.map(row => {
|
|
96
103
|
const rowSeverity = getRowSeverity === null || getRowSeverity === void 0 ? void 0 : getRowSeverity(row);
|
|
97
|
-
const rowId = row[dataKey];
|
|
104
|
+
const rowId = String(row[dataKey]);
|
|
105
|
+
const isNewRow = animateNewRows && newRowIds.has(rowId);
|
|
98
106
|
const isSelected = Boolean(selectedRows === null || selectedRows === void 0 ? void 0 : selectedRows.has(rowId));
|
|
99
107
|
return (_jsxs("div", { className: [
|
|
100
108
|
styles.row,
|
|
101
109
|
onRowClick ? styles.clickableRow : '',
|
|
102
110
|
isSelected ? styles.selectedRow : '',
|
|
103
111
|
rowSeverity ? styles.severity : '',
|
|
112
|
+
isNewRow ? styles.newRow : '',
|
|
104
113
|
]
|
|
105
114
|
.filter(Boolean)
|
|
106
115
|
.join(' '), style: {
|
|
@@ -143,7 +152,7 @@ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode
|
|
|
143
152
|
]
|
|
144
153
|
.filter(Boolean)
|
|
145
154
|
.join(' '), role: "cell", "data-align": (_a = column.align) !== null && _a !== void 0 ? _a : 'left', "data-divider": column.divider, children: _jsx("div", { className: styles.cellContent, children: allowWrap ? (cellValue) : (_jsx("div", { className: styles.cellValueEllipsis, children: cellValue })) }) }, column.id));
|
|
146
|
-
})] }, `gridRow-${
|
|
155
|
+
})] }, `gridRow-${rowId}`));
|
|
147
156
|
}) }));
|
|
148
157
|
const bodyContent = loading && !data.length ? loadingBodyEl : dataBodyEl;
|
|
149
158
|
const tableClassName = [
|
|
@@ -165,7 +165,6 @@
|
|
|
165
165
|
vertical-align: top;
|
|
166
166
|
min-width: 0;
|
|
167
167
|
overflow: hidden;
|
|
168
|
-
text-overflow: ellipsis;
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
.sm .cell {
|
|
@@ -346,8 +345,8 @@
|
|
|
346
345
|
min-width: 0;
|
|
347
346
|
max-inline-size: 100%;
|
|
348
347
|
white-space: nowrap;
|
|
349
|
-
overflow: hidden;
|
|
350
|
-
text-overflow: ellipsis;
|
|
348
|
+
/* overflow: hidden; */
|
|
349
|
+
/* text-overflow: ellipsis; */
|
|
351
350
|
}
|
|
352
351
|
|
|
353
352
|
.allowWrap .cellContent {
|
|
@@ -467,3 +466,20 @@
|
|
|
467
466
|
width: 1px;
|
|
468
467
|
background: var(--table-divider);
|
|
469
468
|
}
|
|
469
|
+
|
|
470
|
+
.newRow {
|
|
471
|
+
animation: tableRowFadeIn 1000ms ease-out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.newRow {
|
|
475
|
+
animation: tableRowFadeIn 600ms ease-out;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@keyframes tableRowFadeIn {
|
|
479
|
+
from {
|
|
480
|
+
opacity: 0;
|
|
481
|
+
}
|
|
482
|
+
to {
|
|
483
|
+
opacity: 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type RowId = string;
|
|
2
|
+
type UseAnimatedNewRowIdsArgs<T> = {
|
|
3
|
+
data: T[];
|
|
4
|
+
dataKey: keyof T;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
animationDurationMs?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function useAnimatedNewRowIds<T extends Record<string, any>>({ data, dataKey, enabled, animationDurationMs, }: UseAnimatedNewRowIdsArgs<T>): Set<RowId>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
export function useAnimatedNewRowIds({ data, dataKey, enabled = true, animationDurationMs = 1000, }) {
|
|
4
|
+
const prevRowIdsRef = useRef(new Set());
|
|
5
|
+
const hasEstablishedBaselineRef = useRef(false);
|
|
6
|
+
const removeTimersRef = useRef(new Map());
|
|
7
|
+
const [newRowIds, setNewRowIds] = useState(new Set());
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const currentIds = new Set();
|
|
10
|
+
for (const row of data) {
|
|
11
|
+
currentIds.add(String(row[dataKey]));
|
|
12
|
+
}
|
|
13
|
+
if (!enabled) {
|
|
14
|
+
for (const timer of removeTimersRef.current.values()) {
|
|
15
|
+
window.clearTimeout(timer);
|
|
16
|
+
}
|
|
17
|
+
removeTimersRef.current.clear();
|
|
18
|
+
setNewRowIds(new Set());
|
|
19
|
+
prevRowIdsRef.current = currentIds;
|
|
20
|
+
hasEstablishedBaselineRef.current = currentIds.size > 0;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (currentIds.size === 0) {
|
|
24
|
+
prevRowIdsRef.current = currentIds;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!hasEstablishedBaselineRef.current) {
|
|
28
|
+
prevRowIdsRef.current = currentIds;
|
|
29
|
+
hasEstablishedBaselineRef.current = true;
|
|
30
|
+
setNewRowIds(new Set());
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const addedIds = [];
|
|
34
|
+
for (const id of currentIds) {
|
|
35
|
+
if (!prevRowIdsRef.current.has(id)) {
|
|
36
|
+
addedIds.push(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (addedIds.length > 0) {
|
|
40
|
+
setNewRowIds(prev => {
|
|
41
|
+
const next = new Set(prev);
|
|
42
|
+
for (const id of addedIds)
|
|
43
|
+
next.add(id);
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
for (const id of addedIds) {
|
|
47
|
+
const existingTimer = removeTimersRef.current.get(id);
|
|
48
|
+
if (existingTimer) {
|
|
49
|
+
window.clearTimeout(existingTimer);
|
|
50
|
+
}
|
|
51
|
+
const timer = window.setTimeout(() => {
|
|
52
|
+
setNewRowIds(prev => {
|
|
53
|
+
if (!prev.has(id))
|
|
54
|
+
return prev;
|
|
55
|
+
const next = new Set(prev);
|
|
56
|
+
next.delete(id);
|
|
57
|
+
return next;
|
|
58
|
+
});
|
|
59
|
+
removeTimersRef.current.delete(id);
|
|
60
|
+
}, animationDurationMs);
|
|
61
|
+
removeTimersRef.current.set(id, timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
prevRowIdsRef.current = currentIds;
|
|
65
|
+
}, [data, dataKey, enabled, animationDurationMs]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const timers = removeTimersRef.current;
|
|
68
|
+
return () => {
|
|
69
|
+
for (const timer of timers.values()) {
|
|
70
|
+
window.clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
timers.clear();
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
return newRowIds;
|
|
76
|
+
}
|