@drivenets/design-system 0.11.1 → 0.12.0
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/CHANGELOG.md +14 -0
- package/dist/index.cjs +360 -130
- package/dist/index.d.cts +167 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +167 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +357 -131
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +2 -1
- package/src/components/ds-autocomplete/ds-autocomplete.module.scss +1 -1
- package/src/components/ds-dropdown-menu/ds-dropdown-menu.module.scss +5 -0
- package/src/components/ds-key-value-pair/ds-key-value-pair.module.scss +0 -5
- package/src/components/ds-status-badge/ds-status-badge.types.ts +15 -0
- package/src/components/ds-status-badge-v2/ds-status-badge-v2.module.scss +109 -0
- package/src/components/ds-status-badge-v2/ds-status-badge-v2.tsx +77 -0
- package/src/components/ds-status-badge-v2/ds-status-badge-v2.types.ts +79 -0
- package/src/components/ds-status-badge-v2/ds-status-badge-v2.unit.test.ts +25 -0
- package/src/components/ds-status-badge-v2/index.ts +13 -0
- package/src/components/ds-status-badge-v2/phase-config.ts +15 -0
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +16 -8
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +61 -0
- package/src/components/ds-table/components/ds-table-body-virtualized/use-infinite-scroll.ts +54 -0
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +4 -9
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +12 -26
- package/src/components/ds-table/components/ds-table-header-selectable-cell/ds-table-header-selectable-cell.tsx +22 -0
- package/src/components/ds-table/components/ds-table-header-selectable-cell/ds-table-header-selectable-cell.types.ts +9 -0
- package/src/components/ds-table/components/ds-table-header-selectable-cell/index.ts +2 -0
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +4 -9
- package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +20 -15
- package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.tsx +9 -18
- package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.types.ts +2 -11
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +3 -6
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -6
- package/src/components/ds-table/ds-table.tsx +57 -17
- package/src/components/ds-table/ds-table.types.ts +27 -1
- package/src/components/ds-table/styles/_variables.scss +0 -1
- package/src/components/ds-table/styles/shared/ds-table-shared.module.scss +1 -0
- package/src/components/ds-table/utils/constants.ts +21 -0
- package/src/index.ts +1 -0
- package/src/styles/shared/_dropdown.scss +1 -1
- package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.module.scss +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drivenets/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.cts",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@rolldown/plugin-babel": "^0.2.2",
|
|
69
69
|
"@storybook/addon-a11y": "^10.2.19",
|
|
70
70
|
"@storybook/addon-docs": "^10.2.19",
|
|
71
|
+
"@storybook/addon-mcp": "^0.6.0",
|
|
71
72
|
"@storybook/addon-vitest": "^10.2.19",
|
|
72
73
|
"@storybook/react-vite": "^10.2.19",
|
|
73
74
|
"@tanstack/react-query": "^5.90.21",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.root[data-orientation='horizontal'] .valueContainer {
|
|
38
|
-
justify-items: end;
|
|
39
38
|
min-width: 0;
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -50,10 +49,6 @@
|
|
|
50
49
|
overflow: hidden;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
.root[data-orientation='horizontal'] .valueDisplay {
|
|
54
|
-
text-align: end;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
52
|
.editorSlot {
|
|
58
53
|
display: none;
|
|
59
54
|
min-width: 0;
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react';
|
|
2
2
|
import type { IconType } from '../ds-icon';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `statusBadgeV2Phases` and `StatusBadgeV2Phase` from `DsStatusBadgeV2` instead.
|
|
6
|
+
*/
|
|
4
7
|
export const dsStatuses = ['active', 'running', 'pending', 'draft', 'inactive', 'warning', 'failed'] as const;
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Use `StatusBadgeV2Phase` from `DsStatusBadgeV2` instead.
|
|
11
|
+
*/
|
|
6
12
|
export type DsStatus = (typeof dsStatuses)[number];
|
|
7
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Use `statusBadgeV2Sizes` and `StatusBadgeV2Size` from `DsStatusBadgeV2` instead.
|
|
16
|
+
*/
|
|
8
17
|
export const statusBadgeSizes = ['medium', 'small'] as const;
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Use `StatusBadgeV2Size` from `DsStatusBadgeV2` instead.
|
|
20
|
+
*/
|
|
9
21
|
export type StatusBadgeSize = (typeof statusBadgeSizes)[number];
|
|
10
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use `DsStatusBadgeV2Props` and `DsStatusBadgeV2` instead.
|
|
25
|
+
*/
|
|
11
26
|
export interface DsStatusBadgeProps {
|
|
12
27
|
/**
|
|
13
28
|
* The icon of the status badge
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
@mixin phase($bg, $icon-color, $font-color: var(--font-main)) {
|
|
2
|
+
background-color: $bg;
|
|
3
|
+
color: $font-color;
|
|
4
|
+
--_icon-color: #{$icon-color};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
$size-medium: 22px;
|
|
8
|
+
$size-small: 18px;
|
|
9
|
+
$badge-icon-size-small: 12px;
|
|
10
|
+
|
|
11
|
+
.root {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
border-radius: 9999px;
|
|
15
|
+
width: max-content;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.icon {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
color: var(--_icon-color);
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.label::first-letter {
|
|
25
|
+
text-transform: capitalize;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.medium {
|
|
29
|
+
height: $size-medium;
|
|
30
|
+
padding: 0 var(--xs) 0 var(--3xs);
|
|
31
|
+
gap: var(--3xs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.small {
|
|
35
|
+
height: $size-small;
|
|
36
|
+
padding: 0 var(--2xs) 0 var(--3xs);
|
|
37
|
+
gap: var(--3xs);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.small .icon {
|
|
41
|
+
--_badge-icon-size: #{$badge-icon-size-small};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.iconOnly {
|
|
45
|
+
padding: 0;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
|
|
48
|
+
&.medium {
|
|
49
|
+
width: $size-medium;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&.small {
|
|
53
|
+
width: $size-small;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.textOnly {
|
|
58
|
+
&.medium {
|
|
59
|
+
padding: 0 var(--xs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&.small {
|
|
63
|
+
padding: 0 var(--2xs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.not-started {
|
|
68
|
+
@include phase(var(--background-secondary), var(--icon-secondary), var(--font-secondary));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.temporary {
|
|
72
|
+
@include phase(var(--background-secondary), var(--icon-secondary), var(--font-secondary));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.in-review {
|
|
76
|
+
@include phase(var(--background-secondary), var(--icon-secondary), var(--font-secondary));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pending {
|
|
80
|
+
@include phase(var(--background-pending), var(--icon-pending));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.active {
|
|
84
|
+
@include phase(var(--background-active-status), var(--icon-action));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.execution {
|
|
88
|
+
@include phase(var(--background-execution), var(--icon-execution));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.result-succeeded {
|
|
92
|
+
@include phase(var(--background-success), var(--icon-success));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.result-warning {
|
|
96
|
+
@include phase(var(--background-warning), var(--icon-warning));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.result-failed {
|
|
100
|
+
@include phase(var(--background-error-secondary-hover), var(--icon-error));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.deprecated {
|
|
104
|
+
@include phase(var(--background-secondary), var(--icon-secondary), var(--font-secondary));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.secondary {
|
|
108
|
+
background-color: transparent;
|
|
109
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import styles from './ds-status-badge-v2.module.scss';
|
|
4
|
+
import type { DsStatusBadgeV2BaseProps } from './ds-status-badge-v2.types';
|
|
5
|
+
import { phaseIconMap } from './phase-config';
|
|
6
|
+
import { DsIcon } from '../ds-icon';
|
|
7
|
+
import { DsTooltip } from '../ds-tooltip';
|
|
8
|
+
import { DsTypography } from '../ds-typography';
|
|
9
|
+
|
|
10
|
+
const DsStatusBadgeV2 = ({
|
|
11
|
+
phase,
|
|
12
|
+
label,
|
|
13
|
+
icon,
|
|
14
|
+
iconOnly = false,
|
|
15
|
+
variant = 'primary',
|
|
16
|
+
size = 'medium',
|
|
17
|
+
className,
|
|
18
|
+
style,
|
|
19
|
+
ref,
|
|
20
|
+
'aria-label': ariaLabel,
|
|
21
|
+
}: DsStatusBadgeV2BaseProps) => {
|
|
22
|
+
const resolvedIcon = icon === null ? null : (icon ?? phaseIconMap[phase]);
|
|
23
|
+
const hasIcon = resolvedIcon !== null;
|
|
24
|
+
const isTextOnly = !hasIcon;
|
|
25
|
+
const tooltipContent = iconOnly ? label : undefined;
|
|
26
|
+
|
|
27
|
+
const iconStyle =
|
|
28
|
+
size === 'small'
|
|
29
|
+
? {
|
|
30
|
+
fontSize: 'var(--_badge-icon-size)',
|
|
31
|
+
width: 'var(--_badge-icon-size)',
|
|
32
|
+
height: 'var(--_badge-icon-size)',
|
|
33
|
+
}
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
36
|
+
const rootClass = classNames(
|
|
37
|
+
styles.root,
|
|
38
|
+
styles[phase],
|
|
39
|
+
styles[size],
|
|
40
|
+
{
|
|
41
|
+
[styles.secondary]: variant === 'secondary',
|
|
42
|
+
[styles.iconOnly]: iconOnly,
|
|
43
|
+
[styles.textOnly]: isTextOnly,
|
|
44
|
+
},
|
|
45
|
+
className,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const badge = (
|
|
49
|
+
<div
|
|
50
|
+
ref={ref as Ref<HTMLDivElement>}
|
|
51
|
+
className={rootClass}
|
|
52
|
+
style={style}
|
|
53
|
+
role="status"
|
|
54
|
+
aria-label={ariaLabel ?? label}
|
|
55
|
+
>
|
|
56
|
+
{hasIcon && (
|
|
57
|
+
<span className={styles.icon}>
|
|
58
|
+
<DsIcon icon={resolvedIcon} size="tiny" filled aria-hidden="true" style={iconStyle} />
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{!iconOnly && (
|
|
63
|
+
<DsTypography className={styles.label} variant="body-xs-reg">
|
|
64
|
+
{label}
|
|
65
|
+
</DsTypography>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (iconOnly) {
|
|
71
|
+
return <DsTooltip content={tooltipContent}>{badge}</DsTooltip>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return badge;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default DsStatusBadgeV2;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CSSProperties, Ref } from 'react';
|
|
2
|
+
import type { IconType } from '../ds-icon';
|
|
3
|
+
import type { ResponsiveValue } from '../../utils/responsive';
|
|
4
|
+
|
|
5
|
+
export const statusBadgeV2Phases = [
|
|
6
|
+
'not-started',
|
|
7
|
+
'temporary',
|
|
8
|
+
'in-review',
|
|
9
|
+
'pending',
|
|
10
|
+
'active',
|
|
11
|
+
'execution',
|
|
12
|
+
'result-succeeded',
|
|
13
|
+
'result-warning',
|
|
14
|
+
'result-failed',
|
|
15
|
+
'deprecated',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type StatusBadgeV2Phase = (typeof statusBadgeV2Phases)[number];
|
|
19
|
+
|
|
20
|
+
export const statusBadgeV2Variants = ['primary', 'secondary'] as const;
|
|
21
|
+
export type StatusBadgeV2Variant = (typeof statusBadgeV2Variants)[number];
|
|
22
|
+
|
|
23
|
+
export const statusBadgeV2Sizes = ['medium', 'small'] as const;
|
|
24
|
+
export type StatusBadgeV2Size = (typeof statusBadgeV2Sizes)[number];
|
|
25
|
+
|
|
26
|
+
export interface DsStatusBadgeV2SharedProps {
|
|
27
|
+
/**
|
|
28
|
+
* Lifecycle phase that determines color and default icon.
|
|
29
|
+
* Each phase maps to a color palette and a default Material Symbols icon.
|
|
30
|
+
*/
|
|
31
|
+
phase: StatusBadgeV2Phase;
|
|
32
|
+
|
|
33
|
+
label: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Visual variant controlling background treatment.
|
|
37
|
+
* - `'primary'`: tinted background pill or circle
|
|
38
|
+
* - `'secondary'`: no background, inline presentation
|
|
39
|
+
* @default 'primary'
|
|
40
|
+
*/
|
|
41
|
+
variant?: StatusBadgeV2Variant;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Badge size controlling height, padding, and icon dimensions. Responsive.
|
|
45
|
+
* @default 'medium'
|
|
46
|
+
*/
|
|
47
|
+
size?: StatusBadgeV2Size;
|
|
48
|
+
|
|
49
|
+
className?: string;
|
|
50
|
+
|
|
51
|
+
style?: CSSProperties;
|
|
52
|
+
|
|
53
|
+
ref?: Ref<HTMLElement>;
|
|
54
|
+
|
|
55
|
+
'aria-label'?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When `iconOnly` is true, `icon` cannot be `null` — the badge must have an icon
|
|
60
|
+
* (either the phase default or a custom one). When `iconOnly` is false or omitted,
|
|
61
|
+
* `icon` can be `null` to force a text-only badge.
|
|
62
|
+
*/
|
|
63
|
+
export type StatusBadgeV2IconOnlyProps =
|
|
64
|
+
| {
|
|
65
|
+
/** When true, hides the label text and renders icon-only. The label is used as tooltip content instead. */
|
|
66
|
+
iconOnly: true;
|
|
67
|
+
/** Override the default phase icon. When omitted, the default icon for the phase is used. */
|
|
68
|
+
icon?: IconType;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
iconOnly?: false;
|
|
72
|
+
icon?: IconType | null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type DsStatusBadgeV2BaseProps = DsStatusBadgeV2SharedProps & StatusBadgeV2IconOnlyProps;
|
|
76
|
+
|
|
77
|
+
export type DsStatusBadgeV2Props = Omit<DsStatusBadgeV2SharedProps, 'size'> & {
|
|
78
|
+
size?: ResponsiveValue<StatusBadgeV2Size>;
|
|
79
|
+
} & StatusBadgeV2IconOnlyProps;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* eslint-disable vitest/expect-expect */
|
|
2
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
3
|
+
import type { StatusBadgeV2IconOnlyProps } from './ds-status-badge-v2.types';
|
|
4
|
+
|
|
5
|
+
describe('DsStatusBadgeV2 types', () => {
|
|
6
|
+
it('allows iconOnly with a custom icon', () => {
|
|
7
|
+
expectTypeOf<{ iconOnly: true; icon: () => null }>().toExtend<StatusBadgeV2IconOnlyProps>();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('allows iconOnly without icon (phase default)', () => {
|
|
11
|
+
expectTypeOf<{ iconOnly: true }>().toExtend<StatusBadgeV2IconOnlyProps>();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('allows icon=null when iconOnly is false', () => {
|
|
15
|
+
expectTypeOf<{ iconOnly: false; icon: null }>().toExtend<StatusBadgeV2IconOnlyProps>();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('allows icon=null when iconOnly is omitted', () => {
|
|
19
|
+
expectTypeOf<{ icon: null }>().toExtend<StatusBadgeV2IconOnlyProps>();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('prevents iconOnly=true with icon=null', () => {
|
|
23
|
+
expectTypeOf<{ iconOnly: true; icon: null }>().not.toExtend<StatusBadgeV2IconOnlyProps>();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { withResponsiveProps } from '../../utils/responsive';
|
|
2
|
+
import DsStatusBadgeV2Base from './ds-status-badge-v2';
|
|
3
|
+
|
|
4
|
+
export const DsStatusBadgeV2 = withResponsiveProps(DsStatusBadgeV2Base, ['size']);
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
StatusBadgeV2Phase,
|
|
8
|
+
StatusBadgeV2Variant,
|
|
9
|
+
StatusBadgeV2Size,
|
|
10
|
+
DsStatusBadgeV2BaseProps,
|
|
11
|
+
DsStatusBadgeV2Props,
|
|
12
|
+
} from './ds-status-badge-v2.types';
|
|
13
|
+
export { statusBadgeV2Phases, statusBadgeV2Variants, statusBadgeV2Sizes } from './ds-status-badge-v2.types';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IconType } from '../ds-icon';
|
|
2
|
+
import type { StatusBadgeV2Phase } from './ds-status-badge-v2.types';
|
|
3
|
+
|
|
4
|
+
export const phaseIconMap: Record<StatusBadgeV2Phase, IconType> = Object.freeze({
|
|
5
|
+
'not-started': 'lasso_select',
|
|
6
|
+
temporary: 'hourglass_empty',
|
|
7
|
+
'in-review': 'document_search',
|
|
8
|
+
pending: 'timer_pause',
|
|
9
|
+
active: 'power_settings_circle',
|
|
10
|
+
execution: 'timelapse',
|
|
11
|
+
'result-succeeded': 'verified',
|
|
12
|
+
'result-warning': 'warning',
|
|
13
|
+
'result-failed': 'cancel',
|
|
14
|
+
deprecated: 'inventory_2',
|
|
15
|
+
});
|
package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { DsTableRowVirtualized } from '../ds-table-row-virtualized';
|
|
|
6
6
|
import type { DsTableBodyVirtualizedProps } from './ds-table-body-virtualized.types';
|
|
7
7
|
import { TableBody, TableRow, TableCell } from '../core-table';
|
|
8
8
|
import { EMPTY_TABLE_STATE_TEXT } from '../../utils/constants';
|
|
9
|
+
import { useInfiniteScroll } from './use-infinite-scroll';
|
|
9
10
|
|
|
10
11
|
export const DsTableBodyVirtualized = <TData,>({
|
|
11
12
|
table,
|
|
@@ -15,6 +16,7 @@ export const DsTableBodyVirtualized = <TData,>({
|
|
|
15
16
|
overscan,
|
|
16
17
|
onScroll,
|
|
17
18
|
rowSelection,
|
|
19
|
+
infiniteScroll,
|
|
18
20
|
}: DsTableBodyVirtualizedProps<TData>) => {
|
|
19
21
|
const rowsMapRef = useRef(new Map<string, HTMLTableRowElement>());
|
|
20
22
|
const rowHeightsMapRef = useRef(new Map<string, number>());
|
|
@@ -30,6 +32,8 @@ export const DsTableBodyVirtualized = <TData,>({
|
|
|
30
32
|
return item ? `${item.row.id}${item.isExpandedRowContent ? '-expanded-content' : ''}` : String(index);
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
const { loadDataIfNeeded } = useInfiniteScroll(tableContainerRef, rows.length, infiniteScroll);
|
|
36
|
+
|
|
33
37
|
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
|
34
38
|
count: rowsAndExpandedRowContent.length,
|
|
35
39
|
estimateSize: (index) => {
|
|
@@ -56,17 +60,21 @@ export const DsTableBodyVirtualized = <TData,>({
|
|
|
56
60
|
});
|
|
57
61
|
});
|
|
58
62
|
|
|
59
|
-
if (sync
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
if (sync) {
|
|
64
|
+
if (onScroll) {
|
|
65
|
+
const scrollOffset = instance.scrollOffset || 0;
|
|
66
|
+
const totalContentHeight = instance.getTotalSize();
|
|
67
|
+
const viewportHeight = instance.scrollElement?.clientHeight;
|
|
68
|
+
const scrollDirection = instance.scrollDirection;
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
if (viewportHeight) {
|
|
71
|
+
const bottomOffset = totalContentHeight - (scrollOffset + viewportHeight);
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
onScroll({ scrollOffset, totalContentHeight, viewportHeight, bottomOffset, scrollDirection });
|
|
74
|
+
}
|
|
69
75
|
}
|
|
76
|
+
|
|
77
|
+
loadDataIfNeeded();
|
|
70
78
|
}
|
|
71
79
|
},
|
|
72
80
|
});
|
|
@@ -36,6 +36,12 @@ export interface DsTableBodyVirtualizedProps<TData> {
|
|
|
36
36
|
* Current row selection state from TanStack Table. Used to style selected rows.
|
|
37
37
|
*/
|
|
38
38
|
rowSelection: RowSelectionState;
|
|
39
|
+
/**
|
|
40
|
+
* Optional infinite-scroll configuration. When provided, the body requests
|
|
41
|
+
* `onLoadMore` as the user nears the bottom of the scroll container and
|
|
42
|
+
* runs an auto-fill loop until the viewport becomes scrollable.
|
|
43
|
+
*/
|
|
44
|
+
infiniteScroll?: InfiniteScrollConfig;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/**
|
|
@@ -53,3 +59,58 @@ export interface ScrollParams {
|
|
|
53
59
|
/** The direction of the scroll movement */
|
|
54
60
|
scrollDirection: 'forward' | 'backward' | null;
|
|
55
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for infinite scroll on a virtualized table.
|
|
65
|
+
*
|
|
66
|
+
* The Table is responsible for detecting when more rows should be requested
|
|
67
|
+
* (proximity to the bottom of the scroll container, and the auto-fill loop
|
|
68
|
+
* when the rendered content does not fill the viewport). The consumer is
|
|
69
|
+
* responsible for fetching, pagination cursors, error handling, retry, and
|
|
70
|
+
* for reflecting loading state back via `isLoadingMore`.
|
|
71
|
+
*/
|
|
72
|
+
export interface InfiniteScrollConfig {
|
|
73
|
+
/**
|
|
74
|
+
* Whether more rows can be loaded. When `false`, no further `onLoadMore`
|
|
75
|
+
* calls are made. Flip this off when the end of the data set is reached.
|
|
76
|
+
*/
|
|
77
|
+
hasMore: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Called when the Table wants the next page of rows. May be synchronous or
|
|
81
|
+
* return a `Promise` (e.g. pass an async fetcher such as React Query's
|
|
82
|
+
* `fetchNextPage` directly); the Table does not await the return value.
|
|
83
|
+
* Reflect the loading state via `isLoadingMore` so the Table can guard
|
|
84
|
+
* against duplicate calls.
|
|
85
|
+
*/
|
|
86
|
+
onLoadMore: (() => void) | (() => Promise<unknown>);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a fetch is currently in flight. While `true`, the Table will not
|
|
90
|
+
* call `onLoadMore` again. Set this synchronously inside your handler (or
|
|
91
|
+
* track it with a state hook flipped before the fetch starts) if
|
|
92
|
+
* `onLoadMore` is not idempotent and you need one-call-per-page semantics;
|
|
93
|
+
* consumers using idempotent fetchers (e.g. React Query's `fetchNextPage`)
|
|
94
|
+
* can safely leave this unset.
|
|
95
|
+
*
|
|
96
|
+
* @default false
|
|
97
|
+
*/
|
|
98
|
+
isLoadingMore?: boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Distance in pixels from the bottom of the scroll container at which
|
|
102
|
+
* `onLoadMore` is requested.
|
|
103
|
+
*
|
|
104
|
+
* @default 500
|
|
105
|
+
*/
|
|
106
|
+
thresholdPx?: number;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* When `true`, the Table will continue to request more rows as long as the
|
|
110
|
+
* rendered content does not fill the viewport (so the user can actually
|
|
111
|
+
* start scrolling). Disable only when you want strictly user-driven loading.
|
|
112
|
+
*
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
autoFill?: boolean;
|
|
116
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from 'react';
|
|
2
|
+
import type { InfiniteScrollConfig } from './ds-table-body-virtualized.types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_THRESHOLD_PX = 500;
|
|
5
|
+
const DEFAULT_AUTO_FILL = true;
|
|
6
|
+
|
|
7
|
+
interface UseInfiniteScrollResult {
|
|
8
|
+
/**
|
|
9
|
+
* Evaluates the current scroll position and, if appropriate, calls
|
|
10
|
+
* `onLoadMore`. Safe to call from scroll/`onChange` handlers and effects.
|
|
11
|
+
*/
|
|
12
|
+
loadDataIfNeeded: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Owns the infinite-scroll trigger logic for `DsTable`:
|
|
17
|
+
* threshold detection and the auto-fill loop for short initial pages.
|
|
18
|
+
*/
|
|
19
|
+
export const useInfiniteScroll = (
|
|
20
|
+
scrollElementRef: RefObject<HTMLElement | null>,
|
|
21
|
+
rowCount: number,
|
|
22
|
+
config: InfiniteScrollConfig | undefined,
|
|
23
|
+
): UseInfiniteScrollResult => {
|
|
24
|
+
const hasMore = config?.hasMore ?? false;
|
|
25
|
+
const isLoadingMore = config?.isLoadingMore ?? false;
|
|
26
|
+
const thresholdPx = config?.thresholdPx ?? DEFAULT_THRESHOLD_PX;
|
|
27
|
+
const autoFill = config?.autoFill ?? DEFAULT_AUTO_FILL;
|
|
28
|
+
|
|
29
|
+
const loadDataIfNeeded = () => {
|
|
30
|
+
const el = scrollElementRef.current;
|
|
31
|
+
|
|
32
|
+
if (!config || !hasMore || isLoadingMore || !el) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isScrollable = el.scrollHeight > el.clientHeight;
|
|
37
|
+
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
38
|
+
const isNearBottom = isScrollable && distanceToBottom <= thresholdPx;
|
|
39
|
+
const isNotScrollable = autoFill && !isScrollable;
|
|
40
|
+
|
|
41
|
+
if (isNearBottom || isNotScrollable) {
|
|
42
|
+
void config.onLoadMore();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
loadDataIfNeeded();
|
|
48
|
+
// loadDataIfNeeded reads the latest config values from closure; deps cover
|
|
49
|
+
// the meaningful inputs (data growth, hasMore, loading state).
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, [rowCount, hasMore, isLoadingMore]);
|
|
52
|
+
|
|
53
|
+
return { loadDataIfNeeded };
|
|
54
|
+
};
|
|
@@ -15,11 +15,6 @@
|
|
|
15
15
|
.headerRow {
|
|
16
16
|
.headerCell {
|
|
17
17
|
border-bottom: 1px solid var(--color-dap-gray-400);
|
|
18
|
-
|
|
19
|
-
&.selectColumn {
|
|
20
|
-
width: vars.$select-column-width;
|
|
21
|
-
padding: 0;
|
|
22
|
-
}
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
&.headerRowNoBorder {
|
|
@@ -34,10 +29,6 @@
|
|
|
34
29
|
width: 100%;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
.reorderColumn {
|
|
38
|
-
width: vars.$reorder-column-width;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
32
|
.headerCell {
|
|
42
33
|
@include typography.body-sm-md;
|
|
43
34
|
color: var(--font-main);
|
|
@@ -64,6 +55,10 @@
|
|
|
64
55
|
}
|
|
65
56
|
}
|
|
66
57
|
|
|
58
|
+
.selectHeaderCell {
|
|
59
|
+
padding: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
67
62
|
.headerSortContainer {
|
|
68
63
|
display: flex;
|
|
69
64
|
align-items: center;
|