@drivenets/design-system 0.13.0 → 0.15.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 +29 -0
- package/README.md +1 -1
- package/dist/index.cjs +1155 -381
- package/dist/index.d.cts +371 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +371 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1151 -381
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +16 -13
- package/src/components/ds-button-v3/ds-button-v3.figma.ts +99 -0
- package/src/components/ds-button-v3/ds-button-v3.module.scss +22 -22
- package/src/components/ds-card/figma/ds-card-content.figma.ts +15 -0
- package/src/components/ds-card/figma/ds-card-footer.figma.ts +15 -0
- package/src/components/ds-card/figma/ds-card-header.figma.ts +15 -0
- package/src/components/ds-card/figma/ds-card.figma.ts +72 -0
- package/src/components/ds-date-picker/ds-date-picker.figma.ts +19 -0
- package/src/components/ds-drawer/ds-drawer.module.scss +14 -0
- package/src/components/ds-drawer/ds-drawer.tsx +20 -1
- package/src/components/ds-drawer/ds-drawer.types.ts +7 -0
- package/src/components/ds-file-upload/hooks/use-file-upload.ts +7 -0
- package/src/components/ds-form-control/ds-form-control.figma.ts +76 -0
- package/src/components/ds-icon/figma/ds-icon-material.figma.batch.ts +10 -0
- package/src/components/ds-icon/figma/ds-icon-special.figma.batch.ts +18 -0
- package/src/components/ds-icon/figma/ds-icon.figma.batch.json +2873 -0
- package/src/components/ds-modal/figma/ds-modal-content.figma.ts +18 -0
- package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +19 -0
- package/src/components/ds-modal/figma/ds-modal-header.figma.ts +21 -0
- package/src/components/ds-modal/figma/ds-modal.figma.ts +36 -0
- package/src/components/ds-popover/ds-popover.module.scss +58 -16
- package/src/components/ds-popover/ds-popover.tsx +148 -24
- package/src/components/ds-popover/ds-popover.types.ts +95 -12
- package/src/components/ds-popover/index.ts +1 -1
- package/src/components/ds-progress-task-bar/ds-progress-task-bar.figma.ts +29 -0
- package/src/components/ds-progress-task-bar/ds-progress-task-bar.module.scss +39 -0
- package/src/components/ds-progress-task-bar/ds-progress-task-bar.tsx +122 -0
- package/src/components/ds-progress-task-bar/ds-progress-task-bar.types.ts +65 -0
- package/src/components/ds-progress-task-bar/index.ts +2 -0
- package/src/components/ds-progress-task-bar/utils.ts +51 -0
- package/src/components/ds-select/ds-select.figma.ts +53 -0
- package/src/components/ds-split-button/ds-split-button.figma.ts +95 -0
- package/src/components/ds-status-badge-v2/ds-status-badge-v2.figma.ts +41 -0
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +8 -3
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +31 -0
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +48 -0
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.types.ts +12 -0
- package/src/components/ds-table/components/ds-table-group-header-cell/index.ts +2 -0
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +4 -0
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +55 -39
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +5 -1
- package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +4 -0
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +5 -1
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +4 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +14 -2
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +19 -0
- package/src/components/ds-table/context/ds-table-context.ts +4 -0
- package/src/components/ds-table/ds-table-column.types.ts +24 -0
- package/src/components/ds-table/ds-table.tsx +48 -6
- package/src/components/ds-table/ds-table.types.ts +62 -0
- package/src/components/ds-table/figma/ds-table-expandable.figma.ts +17 -0
- package/src/components/ds-table/figma/ds-table.figma.ts +16 -0
- package/src/components/ds-table/grouping/hooks/use-column-groups.ts +73 -0
- package/src/components/ds-table/grouping/index.ts +5 -0
- package/src/components/ds-table/grouping/types/column-group-meta.augmentation.ts +26 -0
- package/src/components/ds-table/grouping/types/column-group.types.ts +35 -0
- package/src/components/ds-table/grouping/utils/column-group.ts +119 -0
- package/src/components/ds-table/index.ts +4 -0
- package/src/components/ds-table/styles/_variables.scss +1 -0
- package/src/components/ds-table/utils/constants.ts +5 -0
- package/src/components/ds-tabs/figma/ds-tabs-tab-vertical.figma.ts +78 -0
- package/src/components/ds-tabs/figma/ds-tabs-tab.figma.ts +89 -0
- package/src/components/ds-tabs/figma/ds-tabs-vertical.figma.ts +31 -0
- package/src/components/ds-tabs/figma/ds-tabs.figma.ts +29 -0
- package/src/components/ds-tag/ds-tag.module.scss +22 -1
- package/src/components/ds-tag/ds-tag.tsx +16 -3
- package/src/components/ds-tag/ds-tag.types.ts +22 -8
- package/src/components/ds-tag/ds-tag.unit.test.ts +32 -0
- package/src/components/ds-tag-filter/ds-tag-filter.tsx +14 -22
- package/src/components/ds-text-input/ds-text-input.figma.ts +60 -0
- package/src/components/ds-time-picker/ds-time-picker.figma.ts +18 -0
- package/src/index.ts +1 -0
- package/src/styles/_root_new.scss +2 -0
- package/src/styles/_scrollbars.scss +27 -50
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CSSProperties, Ref } from 'react';
|
|
2
|
+
|
|
3
|
+
export const taskBarStatuses = ['completed', 'running', 'failed'] as const;
|
|
4
|
+
export type TaskBarStatus = (typeof taskBarStatuses)[number];
|
|
5
|
+
|
|
6
|
+
export interface DsProgressTaskBarLocale {
|
|
7
|
+
/** @default 'Completed' */
|
|
8
|
+
completed?: string;
|
|
9
|
+
/** @default 'Running' */
|
|
10
|
+
running?: string;
|
|
11
|
+
/** @default 'Failed' */
|
|
12
|
+
failed?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Builds the trailing total label from the already-formatted total value.
|
|
15
|
+
* @default (total) => `of ${total}`
|
|
16
|
+
*/
|
|
17
|
+
total?: (total: string) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DsProgressTaskBarProps {
|
|
21
|
+
/**
|
|
22
|
+
* Number of completed tasks.
|
|
23
|
+
* @default 0
|
|
24
|
+
*/
|
|
25
|
+
completed?: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Number of running tasks.
|
|
29
|
+
* @default 0
|
|
30
|
+
*/
|
|
31
|
+
running?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Number of failed tasks.
|
|
35
|
+
* @default 0
|
|
36
|
+
*/
|
|
37
|
+
failed?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Total task count used as the denominator for segment widths and shown as
|
|
41
|
+
* the trailing "of {total}" label. Defaults to the sum of the status counts
|
|
42
|
+
* (fully filled bar).
|
|
43
|
+
*/
|
|
44
|
+
total?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Localized strings (status tooltips and total label).
|
|
48
|
+
*/
|
|
49
|
+
locale?: DsProgressTaskBarLocale;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Additional CSS class names
|
|
53
|
+
*/
|
|
54
|
+
className?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Additional styles to apply to the component
|
|
58
|
+
*/
|
|
59
|
+
style?: CSSProperties;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ref to the root element
|
|
63
|
+
*/
|
|
64
|
+
ref?: Ref<HTMLDivElement>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface CountUnit {
|
|
2
|
+
value: number;
|
|
3
|
+
suffix: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const COUNT_UNITS: readonly CountUnit[] = [
|
|
7
|
+
{ value: 1_000_000_000, suffix: 'B' },
|
|
8
|
+
{ value: 1_000_000, suffix: 'M' },
|
|
9
|
+
{ value: 1_000, suffix: 'K' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// Counts below this are shown verbatim; at or above it they are abbreviated.
|
|
13
|
+
const ABBREVIATION_FLOOR = 1_000;
|
|
14
|
+
|
|
15
|
+
// Below this multiple of a unit, show one decimal place (e.g. 1.1K); at or
|
|
16
|
+
// above it, show a whole number (e.g. 87K).
|
|
17
|
+
const DECIMAL_THRESHOLD = 10;
|
|
18
|
+
|
|
19
|
+
const stripTrailingZero = (value: number): string => (value % 1 === 0 ? String(value) : value.toFixed(1));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format a task count for the legend, following the design's abbreviation rules:
|
|
23
|
+
* - `< 1000` → exact number (e.g. `999`)
|
|
24
|
+
* - single-digit unit → one decimal (e.g. `1.1K`, `1.2M`)
|
|
25
|
+
* - multi-digit unit → whole number, truncated so `999,999 → 999K`
|
|
26
|
+
*/
|
|
27
|
+
export const formatCount = (count: number): string => {
|
|
28
|
+
if (!Number.isFinite(count)) {
|
|
29
|
+
return '0';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rounded = Math.max(0, Math.round(count));
|
|
33
|
+
|
|
34
|
+
if (rounded < ABBREVIATION_FLOOR) {
|
|
35
|
+
return String(rounded);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const unit = COUNT_UNITS.find(({ value }) => rounded >= value);
|
|
39
|
+
|
|
40
|
+
if (unit === undefined) {
|
|
41
|
+
return String(rounded);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const scaled = rounded / unit.value;
|
|
45
|
+
|
|
46
|
+
if (scaled < DECIMAL_THRESHOLD) {
|
|
47
|
+
return `${stripTrailingZero(Math.round(scaled * 10) / 10)}${unit.suffix}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${String(Math.floor(scaled))}${unit.suffix}`;
|
|
51
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=13773-1189
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-select
|
|
3
|
+
// component=DsSelect
|
|
4
|
+
import figma from 'figma';
|
|
5
|
+
|
|
6
|
+
const instance = figma.selectedInstance;
|
|
7
|
+
|
|
8
|
+
const size = instance.getEnum('Size', {
|
|
9
|
+
Default: 'default',
|
|
10
|
+
Small: 'small',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const state = instance.getEnum('State', {
|
|
14
|
+
Default: 'default',
|
|
15
|
+
Hover: 'hover',
|
|
16
|
+
Focus: 'focus',
|
|
17
|
+
Active: 'active',
|
|
18
|
+
Disable: 'disable',
|
|
19
|
+
Error: 'error',
|
|
20
|
+
'Error - Focus': 'errorFocus',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const disabled = state === 'disable';
|
|
24
|
+
|
|
25
|
+
const sizeAttr = size === 'small' ? 'size="small"' : '';
|
|
26
|
+
|
|
27
|
+
// Multi-select is driven by the nested dropdown menu's `Type` (Checkbox = multi). The menu
|
|
28
|
+
// lives inside the select tree even when collapsed, so the designer's intent is read in any
|
|
29
|
+
// state; an absent menu (ErrorHandle) safely defaults to single-select.
|
|
30
|
+
const menu = instance.findInstance('DAP_dropdown menu_v01', { traverseInstances: true });
|
|
31
|
+
const multiple =
|
|
32
|
+
menu.type === 'INSTANCE' && menu.getEnum('Type', { default: false, Checkbox: true, Radio: false }) === true;
|
|
33
|
+
|
|
34
|
+
// `value` and `options` are required by `DsSelectProps` but are data, not derivable from
|
|
35
|
+
// Figma, so emit placeholders for the developer to fill in (the discriminated union needs
|
|
36
|
+
// an array `value` when `multiple`). The same string is surfaced as `fcProps` so a parent
|
|
37
|
+
// `DsFormControl` template can emit `<DsFormControl.Select ${fcProps} />` uniformly.
|
|
38
|
+
const attrs = [
|
|
39
|
+
sizeAttr,
|
|
40
|
+
disabled ? 'disabled' : '',
|
|
41
|
+
multiple ? 'multiple' : '',
|
|
42
|
+
'options={[]}',
|
|
43
|
+
multiple ? 'value={[]}' : 'value=""',
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ');
|
|
47
|
+
|
|
48
|
+
export default {
|
|
49
|
+
example: figma.code`<DsSelect ${attrs} />`,
|
|
50
|
+
imports: ["import { DsSelect } from '@drivenets/design-system'"],
|
|
51
|
+
id: 'ds-select',
|
|
52
|
+
metadata: { nestable: true, props: { fcMember: 'Select', fcProps: attrs } },
|
|
53
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=32830-9950
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-split-button
|
|
3
|
+
// component=DsSplitButton
|
|
4
|
+
import figma from 'figma';
|
|
5
|
+
|
|
6
|
+
const instance = figma.selectedInstance;
|
|
7
|
+
|
|
8
|
+
const state = instance.getEnum('state', {
|
|
9
|
+
default: 'default',
|
|
10
|
+
hover: 'hover',
|
|
11
|
+
select: 'select',
|
|
12
|
+
focus: 'focus',
|
|
13
|
+
disabled: 'disabled',
|
|
14
|
+
running: 'running',
|
|
15
|
+
});
|
|
16
|
+
const disabled = state === 'disabled';
|
|
17
|
+
const loading = state === 'running';
|
|
18
|
+
|
|
19
|
+
const button = instance.findInstance('DAP_Button_v03', { traverseInstances: true });
|
|
20
|
+
const size =
|
|
21
|
+
button.type === 'INSTANCE'
|
|
22
|
+
? button.getEnum('Size', { tiny: 'small', small: 'small', medium: 'medium', large: 'medium' })
|
|
23
|
+
: 'medium';
|
|
24
|
+
|
|
25
|
+
const structure = instance.findInstance('.button-structure', { traverseInstances: true });
|
|
26
|
+
const showIcon = structure.type === 'INSTANCE' ? structure.getBoolean('Show icon') : false;
|
|
27
|
+
|
|
28
|
+
// `DsButtonV3.icon` is an icon-name string. Swapped icons keep the placeholder layer
|
|
29
|
+
// name (e.g. `DAP_GM_O_check_circle`), so resolve via Code Connect ID or metadata.
|
|
30
|
+
const toIconName = (name: string): string =>
|
|
31
|
+
name
|
|
32
|
+
.replace(/^DAP_DAP_/, 'DAP_')
|
|
33
|
+
.replace(/^DAP_GM_[OS]_/, '')
|
|
34
|
+
.replace(/^DAP_outline_icon\//, '')
|
|
35
|
+
.replace(/^DAP_/, '')
|
|
36
|
+
.replace(/^\/+/, '')
|
|
37
|
+
.replace(/[\s/]+/g, '_')
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
|
|
40
|
+
const iconNameFromCodeConnectId = (id: string | null): string | undefined => {
|
|
41
|
+
if (!id) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (id.startsWith('ds-icon-o-')) {
|
|
45
|
+
return id.slice('ds-icon-o-'.length);
|
|
46
|
+
}
|
|
47
|
+
if (id.startsWith('ds-icon-s-')) {
|
|
48
|
+
return id.slice('ds-icon-s-'.length);
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const resolveIcon = (iconInstance: figma.InstanceHandle): string | undefined => {
|
|
54
|
+
const fromCcId = iconNameFromCodeConnectId(iconInstance.codeConnectId());
|
|
55
|
+
if (fromCcId) {
|
|
56
|
+
return fromCcId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const iconFromMetadata = iconInstance.executeTemplate().metadata?.props?.icon;
|
|
60
|
+
if (typeof iconFromMetadata === 'string') {
|
|
61
|
+
return iconFromMetadata;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return toIconName(iconInstance.name);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const leadingIcon = structure.type === 'INSTANCE' ? structure.getInstanceSwap('Leading icon') : undefined;
|
|
68
|
+
const iconFromSwap = leadingIcon?.type === 'INSTANCE' ? resolveIcon(leadingIcon) : undefined;
|
|
69
|
+
const iconFromChild =
|
|
70
|
+
structure.type === 'INSTANCE'
|
|
71
|
+
? structure
|
|
72
|
+
.findConnectedInstances((node) => node.codeConnectId()?.startsWith('ds-icon-') ?? false, {
|
|
73
|
+
traverseInstances: true,
|
|
74
|
+
})
|
|
75
|
+
.find((node): node is figma.InstanceHandle => node.type === 'INSTANCE')
|
|
76
|
+
: undefined;
|
|
77
|
+
const icon = iconFromSwap ?? (iconFromChild ? resolveIcon(iconFromChild) : undefined);
|
|
78
|
+
|
|
79
|
+
// The split button's button slot is intended for an icon-only button, so emit
|
|
80
|
+
// `icon` (and `loading`) rather than a text `children` label.
|
|
81
|
+
// While loading, a spinner replaces the icon, so omit it
|
|
82
|
+
// (the leading icon is the component's default placeholder in that state).
|
|
83
|
+
const buttonProps = [
|
|
84
|
+
!loading && showIcon && icon ? `icon: '${icon}'` : undefined,
|
|
85
|
+
loading ? 'loading: true' : undefined,
|
|
86
|
+
]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(', ');
|
|
89
|
+
|
|
90
|
+
export default {
|
|
91
|
+
example: figma.code`<DsSplitButton size="${size}"${disabled ? ' disabled' : ''} slotProps={{ button: { ${buttonProps} }, select: { options: [], value: '' } }} />`,
|
|
92
|
+
imports: ["import { DsSplitButton } from '@drivenets/design-system'"],
|
|
93
|
+
id: 'ds-split-button',
|
|
94
|
+
metadata: { nestable: false },
|
|
95
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=34428-16204
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-status-badge-v2
|
|
3
|
+
// component=DsStatusBadgeV2
|
|
4
|
+
import figma from 'figma';
|
|
5
|
+
|
|
6
|
+
const instance = figma.selectedInstance;
|
|
7
|
+
|
|
8
|
+
const phase = instance.getEnum('status', {
|
|
9
|
+
'not-started': 'not-started',
|
|
10
|
+
temporary: 'temporary',
|
|
11
|
+
'in-review': 'in-review',
|
|
12
|
+
pending: 'pending',
|
|
13
|
+
active: 'active',
|
|
14
|
+
execution: 'execution',
|
|
15
|
+
'result-succeeded': 'result-succeeded',
|
|
16
|
+
'result-warning': 'result-warning',
|
|
17
|
+
'result-failed': 'result-failed',
|
|
18
|
+
deprecated: 'deprecated',
|
|
19
|
+
});
|
|
20
|
+
const variant = instance.getEnum('variant', { primary: 'primary', secondary: 'secondary' });
|
|
21
|
+
const size = instance.getEnum('Size', { medium: 'medium', small: 'small' });
|
|
22
|
+
const structure = instance.getEnum('structure', {
|
|
23
|
+
'icon+text': 'icon+text',
|
|
24
|
+
'icon-only': 'icon-only',
|
|
25
|
+
'text-only': 'text-only',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const labelNode = instance.findText('Active', { traverseInstances: true });
|
|
29
|
+
const label = labelNode.type === 'TEXT' ? labelNode.textContent : 'Active';
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
example:
|
|
33
|
+
structure === 'icon-only'
|
|
34
|
+
? figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" iconOnly />`
|
|
35
|
+
: structure === 'text-only'
|
|
36
|
+
? figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" icon={null} />`
|
|
37
|
+
: figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" />`,
|
|
38
|
+
imports: ["import { DsStatusBadgeV2 } from '@drivenets/design-system'"],
|
|
39
|
+
id: 'ds-status-badge-v2',
|
|
40
|
+
metadata: { nestable: true },
|
|
41
|
+
};
|
package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx
CHANGED
|
@@ -110,7 +110,7 @@ interface DsTableBodyProps<TData> {
|
|
|
110
110
|
rowSelection: RowSelectionState;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function DsTableBody<TData>({
|
|
113
|
+
export function DsTableBody<TData>({
|
|
114
114
|
ref,
|
|
115
115
|
rowVirtualizer,
|
|
116
116
|
rowsMapRef,
|
|
@@ -122,6 +122,11 @@ function DsTableBody<TData>({
|
|
|
122
122
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
123
123
|
const totalSize = rowVirtualizer.getTotalSize();
|
|
124
124
|
|
|
125
|
+
// Gate the empty state on real data count, not on rendered virtual rows.
|
|
126
|
+
// getVirtualItems() transiently returns [] while the virtualizer re-measures
|
|
127
|
+
// after a `data` swap; rendering nothing for that frame avoids an empty-state flash.
|
|
128
|
+
const hasNoData = rowsAndExpandedRowContent.length === 0;
|
|
129
|
+
|
|
125
130
|
return (
|
|
126
131
|
<TableBody ref={ref} className={styles.body}>
|
|
127
132
|
{virtualRows.length > 0 ? (
|
|
@@ -144,11 +149,11 @@ function DsTableBody<TData>({
|
|
|
144
149
|
/>
|
|
145
150
|
);
|
|
146
151
|
})
|
|
147
|
-
) : (
|
|
152
|
+
) : hasNoData ? (
|
|
148
153
|
<TableRow>
|
|
149
154
|
<TableCell className={styles.emptyState}>{emptyState || EMPTY_TABLE_STATE_TEXT}</TableCell>
|
|
150
155
|
</TableRow>
|
|
151
|
-
)}
|
|
156
|
+
) : null}
|
|
152
157
|
|
|
153
158
|
<tr aria-hidden className={styles.sentinel} style={{ top: `${totalSize.toString()}px` }}>
|
|
154
159
|
<td />
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@use '../../../../styles/typography';
|
|
2
|
+
@use '../../styles/variables' as vars;
|
|
3
|
+
|
|
4
|
+
.groupHeaderCell {
|
|
5
|
+
@include typography.body-sm-md;
|
|
6
|
+
color: var(--font-main);
|
|
7
|
+
height: var(--ds-table-header-height);
|
|
8
|
+
padding: 0;
|
|
9
|
+
border-bottom: vars.$border-width solid var(--color-dap-gray-400);
|
|
10
|
+
border-left: vars.$group-border-width solid var(--border);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.groupHeaderContent {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
gap: vars.$header-sort-gap;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
padding: 0 var(--standard);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.groupLabel {
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
text-overflow: ellipsis;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.collapseToggle {
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { flexRender } from '@tanstack/react-table';
|
|
2
|
+
import { DsButtonV3 } from '../../../ds-button-v3';
|
|
3
|
+
import { TableHead } from '../core-table';
|
|
4
|
+
import { useDsTableContext } from '../../context/ds-table-context';
|
|
5
|
+
import { getGroupColumnSizeStyle } from '../../grouping';
|
|
6
|
+
import { defaultDsTableLocale } from '../../ds-table.types';
|
|
7
|
+
import type { DsTableGroupHeaderCellProps } from './ds-table-group-header-cell.types';
|
|
8
|
+
import styles from './ds-table-group-header-cell.module.scss';
|
|
9
|
+
|
|
10
|
+
export const DsTableGroupHeaderCell = <TData, TValue>({
|
|
11
|
+
header,
|
|
12
|
+
}: DsTableGroupHeaderCellProps<TData, TValue>) => {
|
|
13
|
+
const { collapsedColumnGroups, onToggleColumnGroup, locale } = useDsTableContext<TData, TValue>();
|
|
14
|
+
|
|
15
|
+
const groupId = header.column.id;
|
|
16
|
+
const visibleLeafColumns = header.column.getLeafColumns().filter((leaf) => leaf.getIsVisible());
|
|
17
|
+
const groupStyle = getGroupColumnSizeStyle(visibleLeafColumns);
|
|
18
|
+
|
|
19
|
+
const isCollapsible = header.column.columnDef.meta?.group?.collapsible ?? false;
|
|
20
|
+
const isCollapsed = collapsedColumnGroups?.includes(groupId) ?? false;
|
|
21
|
+
|
|
22
|
+
const mergedLocale = { ...defaultDsTableLocale, ...locale };
|
|
23
|
+
const toggleLabel = isCollapsed ? mergedLocale.expandColumnGroup : mergedLocale.collapseColumnGroup;
|
|
24
|
+
|
|
25
|
+
const handleToggle = () => onToggleColumnGroup?.(groupId);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<TableHead className={styles.groupHeaderCell} style={groupStyle} colSpan={header.colSpan}>
|
|
29
|
+
<div className={styles.groupHeaderContent}>
|
|
30
|
+
<span className={styles.groupLabel}>
|
|
31
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
32
|
+
</span>
|
|
33
|
+
{isCollapsible && (
|
|
34
|
+
<DsButtonV3
|
|
35
|
+
variant="secondary"
|
|
36
|
+
size="tiny"
|
|
37
|
+
icon={isCollapsed ? 'chevron_right' : 'chevron_left'}
|
|
38
|
+
className={styles.collapseToggle}
|
|
39
|
+
aria-label={toggleLabel}
|
|
40
|
+
aria-expanded={!isCollapsed}
|
|
41
|
+
title={toggleLabel}
|
|
42
|
+
onClick={handleToggle}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</TableHead>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Header } from '@tanstack/react-table';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the grouping column header cell.
|
|
5
|
+
*/
|
|
6
|
+
export interface DsTableGroupHeaderCellProps<TData, TValue> {
|
|
7
|
+
/**
|
|
8
|
+
* The grouping header from `@tanstack/react-table` (a header whose column nests
|
|
9
|
+
* child columns).
|
|
10
|
+
*/
|
|
11
|
+
header: Header<TData, TValue>;
|
|
12
|
+
}
|
|
@@ -11,6 +11,8 @@ import { SELECT_COLUMN_ID } from '../../utils/constants';
|
|
|
11
11
|
import { DsStack } from '../../../ds-stack';
|
|
12
12
|
import { DsTableColumnFilterPopover } from '../../filters/components/column-filter-popover';
|
|
13
13
|
import type { ResolvedColumnFilter } from '../../filters/types/filter-adapter.types';
|
|
14
|
+
import { DsTableGroupHeaderCell } from '../ds-table-group-header-cell';
|
|
15
|
+
import { isFirstLeafColumnOfGroup } from '../../grouping';
|
|
14
16
|
|
|
15
17
|
const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
|
|
16
18
|
const { stickyHeader, bordered, virtualized } = useDsTableContext<TData, unknown>();
|
|
@@ -29,9 +31,24 @@ const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
|
|
|
29
31
|
)}
|
|
30
32
|
>
|
|
31
33
|
{headerGroup.headers.map((header) => {
|
|
34
|
+
if (header.isPlaceholder) {
|
|
35
|
+
return (
|
|
36
|
+
<TableHead
|
|
37
|
+
key={header.id}
|
|
38
|
+
className={styles.headerCell}
|
|
39
|
+
style={getColumnSizeStyle(header.getSize())}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (header.subHeaders.length > 0) {
|
|
45
|
+
return <DsTableGroupHeaderCell key={header.id} header={header} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
const headerStyle = getColumnSizeStyle(header.column.getSize());
|
|
33
49
|
const canSort = header.column.getCanSort();
|
|
34
50
|
const isSelectColumn = header.column.id === SELECT_COLUMN_ID;
|
|
51
|
+
const isGroupStart = isFirstLeafColumnOfGroup(header.column);
|
|
35
52
|
|
|
36
53
|
const metaFilter = header.column.columnDef.meta?.filter;
|
|
37
54
|
const resolvedFilter: ResolvedColumnFilter | undefined =
|
|
@@ -47,49 +64,48 @@ const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
|
|
|
47
64
|
isSelectColumn && styles.selectHeaderCell,
|
|
48
65
|
resolvedFilter && styles.filterableHeader,
|
|
49
66
|
hasActiveColumnFilter && styles.filterActive,
|
|
67
|
+
isGroupStart && styles.groupStart,
|
|
50
68
|
)}
|
|
51
69
|
onClick={header.column.getToggleSortingHandler()}
|
|
52
70
|
style={headerStyle}
|
|
53
71
|
>
|
|
54
|
-
{
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
72
|
+
<div className={styles.headerSortContainer}>
|
|
73
|
+
<DsStack direction="column" justifyContent="center" width="100%">
|
|
74
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
75
|
+
</DsStack>
|
|
76
|
+
{canSort && (
|
|
77
|
+
<div className={styles.pageButtonIconContainer}>
|
|
78
|
+
{{
|
|
79
|
+
asc: (
|
|
80
|
+
<DsIcon
|
|
81
|
+
icon="arrow_drop_up"
|
|
82
|
+
className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
desc: (
|
|
86
|
+
<DsIcon
|
|
87
|
+
icon="arrow_drop_down"
|
|
88
|
+
className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
}[header.column.getIsSorted() as string] ?? (
|
|
92
|
+
<div className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)} />
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{resolvedFilter && (
|
|
97
|
+
<div className={styles.filterTriggerContainer}>
|
|
98
|
+
<DsTableColumnFilterPopover
|
|
99
|
+
hasActiveFilter={resolvedFilter.hasActiveFilter}
|
|
100
|
+
onApply={resolvedFilter.onApply}
|
|
101
|
+
onClear={resolvedFilter.onClear}
|
|
102
|
+
onCancel={resolvedFilter.onCancel}
|
|
103
|
+
>
|
|
104
|
+
{resolvedFilter.render()}
|
|
105
|
+
</DsTableColumnFilterPopover>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
93
109
|
</TableHead>
|
|
94
110
|
);
|
|
95
111
|
})}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
&:hover {
|
|
12
|
+
&:not(.loadingRow):hover {
|
|
13
13
|
background-color: var(--background-secondary);
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
justify-content: center;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
.groupStart {
|
|
89
|
+
border-left: vars.$group-border-width solid var(--border);
|
|
90
|
+
}
|
|
91
|
+
|
|
88
92
|
.tableRow .editableCell {
|
|
89
93
|
padding: 1px;
|
|
90
94
|
|
|
@@ -10,6 +10,7 @@ import styles from './ds-table-row.module.scss';
|
|
|
10
10
|
import { useDsTableContext } from '../../context/ds-table-context';
|
|
11
11
|
import { mergeRefs } from '../../../../utils/merge-refs';
|
|
12
12
|
import { getColumnSizeStyle } from '../../utils/column-size';
|
|
13
|
+
import { isFirstLeafColumnOfGroup } from '../../grouping';
|
|
13
14
|
import { EXPANDER_COLUMN_ID, REORDER_COLUMN_ID, SELECT_COLUMN_ID } from '../../utils/constants';
|
|
14
15
|
|
|
15
16
|
interface DsRowDragHandleProps {
|
|
@@ -50,6 +51,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
|
|
|
50
51
|
primaryRowActions,
|
|
51
52
|
secondaryRowActions,
|
|
52
53
|
activeRowId,
|
|
54
|
+
loading,
|
|
53
55
|
} = useDsTableContext<TData, unknown>();
|
|
54
56
|
const isExpanded = row.getIsExpanded();
|
|
55
57
|
const isActive = activeRowId === row.id;
|
|
@@ -89,6 +91,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
|
|
|
89
91
|
onRowClick && styles.clickableRow,
|
|
90
92
|
!bordered && styles.rowNoBorder,
|
|
91
93
|
isExpanded && styles.expanded,
|
|
94
|
+
loading && styles.loadingRow,
|
|
92
95
|
)}
|
|
93
96
|
style={rowStyle}
|
|
94
97
|
onClick={() => onRowClick?.(row.original)}
|
|
@@ -118,6 +121,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
|
|
|
118
121
|
styles.tableCell,
|
|
119
122
|
cell.column.id === EXPANDER_COLUMN_ID && styles.expandableCell,
|
|
120
123
|
cell.column.id === SELECT_COLUMN_ID && styles.selectableCell,
|
|
124
|
+
isFirstLeafColumnOfGroup(cell.column) && styles.groupStart,
|
|
121
125
|
typeof cell.column.columnDef.editCell === 'function' && styles.editableCell,
|
|
122
126
|
)}
|
|
123
127
|
style={cellStyle}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
position: absolute;
|
|
8
8
|
width: 100%;
|
|
9
9
|
|
|
10
|
-
&:hover {
|
|
10
|
+
&:not(.loadingRow):hover {
|
|
11
11
|
background-color: var(--background-secondary);
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
justify-content: center;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
.groupStart {
|
|
70
|
+
border-left: vars.$group-border-width solid var(--border);
|
|
71
|
+
}
|
|
72
|
+
|
|
69
73
|
.expandedRowContent {
|
|
70
74
|
background-color: var(--background-secondary);
|
|
71
75
|
|