@drivenets/design-system 0.17.0 → 0.18.1
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 +24 -0
- package/dist/index.cjs +1797 -805
- package/dist/index.d.cts +117 -53
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +117 -53
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1799 -809
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +3 -3
- package/src/components/ds-alert-banner/ds-alert-banner-global.figma.ts +49 -0
- package/src/components/ds-alert-banner/ds-alert-banner.figma.ts +51 -0
- package/src/components/ds-autocomplete/ds-autocomplete.module.scss +15 -4
- package/src/components/ds-autocomplete/ds-autocomplete.tsx +10 -3
- package/src/components/ds-autocomplete/ds-autocomplete.types.ts +8 -0
- package/src/components/ds-button-v3/ds-button-v3.figma.ts +5 -3
- package/src/components/ds-button-v3/ds-button-v3.module.scss +84 -17
- package/src/components/ds-button-v3/ds-button-v3.tsx +2 -0
- package/src/components/ds-button-v3/ds-button-v3.types.ts +11 -2
- package/src/components/ds-button-v3/ds-button-v4.figma.ts +121 -0
- package/src/components/ds-comment-bubble/ds-comment-bubble.figma.ts +52 -0
- package/src/components/ds-comment-card/ds-comment-card.figma.ts +36 -0
- package/src/components/ds-comment-indicator/ds-comment-indicator.figma.ts +32 -0
- package/src/components/ds-comments-drawer/ds-comments-drawer.figma.ts +19 -0
- package/src/components/ds-select/ds-select.figma.ts +2 -1
- package/src/components/ds-select/ds-select.tsx +6 -1
- package/src/components/ds-select/ds-select.types.ts +2 -1
- package/src/components/ds-slider/ds-slider.figma.ts +29 -0
- package/src/components/ds-slider/ds-slider.module.scss +104 -0
- package/src/components/ds-slider/ds-slider.tsx +118 -0
- package/src/components/ds-slider/ds-slider.types.ts +102 -0
- package/src/components/ds-slider/ds-slider.utils.ts +14 -0
- package/src/components/ds-slider/index.ts +11 -0
- package/src/components/ds-smart-tabs/figma/ds-smart-tabs-tab.figma.ts +83 -0
- package/src/components/ds-smart-tabs/figma/ds-smart-tabs.figma.ts +23 -0
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +5 -1
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +15 -3
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +15 -1
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +29 -6
- package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.module.scss +23 -0
- package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.tsx +218 -0
- package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.types.ts +14 -0
- package/src/components/ds-table/components/ds-table-resize-handle/index.ts +1 -0
- package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.module.scss +18 -0
- package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.tsx +16 -0
- package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.types.ts +13 -0
- package/src/components/ds-table/components/ds-table-resize-overlay/index.ts +1 -0
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +1 -1
- package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +9 -2
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +2 -1
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +9 -2
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +2 -2
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +2 -1
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +2 -1
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +2 -1
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +2 -1
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +12 -3
- package/src/components/ds-table/context/ds-table-context.tsx +208 -0
- package/src/components/ds-table/ds-table-defaults.ts +42 -0
- package/src/components/ds-table/ds-table.module.scss +4 -1
- package/src/components/ds-table/ds-table.tsx +139 -214
- package/src/components/ds-table/ds-table.types.ts +53 -47
- package/src/components/ds-table/grouping/utils/column-group.ts +7 -5
- package/src/components/ds-table/hooks/use-cell-editor.ts +5 -1
- package/src/components/ds-table/hooks/use-column-resize.ts +385 -0
- package/src/components/ds-table/hooks/use-editing-state.ts +190 -0
- package/src/components/ds-table/index.ts +1 -0
- package/src/components/ds-table/styles/_variables.scss +2 -1
- package/src/components/ds-table/utils/column-size-meta.augmentation.ts +16 -0
- package/src/components/ds-table/utils/column-size.ts +213 -10
- package/src/components/ds-table/utils/column-size.unit.test.ts +213 -0
- package/src/components/ds-table/utils/constants.ts +28 -0
- package/src/components/ds-table/utils/frozen-body.ts +33 -0
- package/src/components/ds-table/utils/frozen-body.unit.test.ts +44 -0
- package/src/components/ds-table/utils/table-api.ts +42 -0
- package/src/components/ds-table/utils/table-api.unit.test.ts +82 -0
- package/src/components/ds-table/utils/table-columns.tsx +119 -0
- package/src/components/ds-table/utils/table-columns.unit.test.ts +199 -0
- package/src/components/ds-tag/ds-tag.figma.ts +51 -0
- package/src/components/ds-tag-filter/ds-tag-filter.figma.ts +16 -0
- package/src/components/ds-tag-filter/ds-tag-filter.module.scss +7 -35
- package/src/components/ds-tag-filter/ds-tag-filter.tsx +34 -57
- package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -1
- package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +13 -18
- package/src/components/ds-text-input/ds-input-field-v02.figma.ts +29 -4
- package/src/components/ds-text-input/ds-text-input.figma.ts +3 -2
- package/src/components/ds-text-input/ds-text-input.tsx +1 -0
- package/src/components/ds-text-input/ds-text-input.types.ts +1 -1
- package/src/components/ds-tree/ds-tree.figma.ts +54 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.figma.ts +99 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +1 -8
- package/src/index.ts +1 -0
- package/src/styles/_gradients.scss +19 -0
- package/src/styles/_root.scss +39 -15
- package/src/styles/_typography.scss +1 -0
- package/src/styles/shared/_input.scss +10 -0
- package/src/styles/styles.scss +1 -0
- package/src/components/ds-table/context/ds-table-context.ts +0 -112
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ColumnDef } from '@tanstack/react-table';
|
|
2
|
+
import { DsSkeletonText } from '../../ds-skeleton';
|
|
3
|
+
import { DsTableHeaderSelectableCell } from '../components/ds-table-header-selectable-cell';
|
|
4
|
+
import { DsTableRowExpandableCell } from '../components/ds-table-row-expandable-cell';
|
|
5
|
+
import { DsTableRowSelectableCell } from '../components/ds-table-row-selectable-cell';
|
|
6
|
+
import {
|
|
7
|
+
EXPANDER_COLUMN_ID,
|
|
8
|
+
EXPANDER_COLUMN_WIDTH,
|
|
9
|
+
REORDER_COLUMN_ID,
|
|
10
|
+
REORDER_COLUMN_WIDTH,
|
|
11
|
+
SELECT_COLUMN_ID,
|
|
12
|
+
SELECT_COLUMN_WIDTH,
|
|
13
|
+
SKELETON_ROW_COUNT,
|
|
14
|
+
} from './constants';
|
|
15
|
+
|
|
16
|
+
export type GetAugmentedColumnsOptions = {
|
|
17
|
+
selectable: boolean;
|
|
18
|
+
expandable: boolean;
|
|
19
|
+
reorderable: boolean;
|
|
20
|
+
virtualized: boolean;
|
|
21
|
+
showSelectAllCheckbox: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const EXPLICIT_SIZE_META = { hasExplicitSize: true } as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Clones leaf defs that authored `size` so `meta.hasExplicitSize` survives
|
|
28
|
+
* TanStack’s default-size merge. Group defs are cloned only to recurse;
|
|
29
|
+
* unsized leaves are returned as-is.
|
|
30
|
+
*/
|
|
31
|
+
const stampExplicitSize = <TData, TValue>(columns: ColumnDef<TData, TValue>[]): ColumnDef<TData, TValue>[] =>
|
|
32
|
+
columns.map((column) => {
|
|
33
|
+
if ('columns' in column && column.columns) {
|
|
34
|
+
return { ...column, columns: stampExplicitSize(column.columns) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (column.size === undefined) {
|
|
38
|
+
return column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { ...column, meta: { ...column.meta, ...EXPLICIT_SIZE_META } };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prepends the builtin select / expander / reorder columns when those features
|
|
46
|
+
* are on. Reorder is omitted for virtualized tables. Unshift order yields
|
|
47
|
+
* visual order: reorder, expander, select, then consumer columns.
|
|
48
|
+
*/
|
|
49
|
+
export const getAugmentedColumns = <TData, TValue>(
|
|
50
|
+
columns: ColumnDef<TData, TValue>[],
|
|
51
|
+
{ selectable, expandable, reorderable, virtualized, showSelectAllCheckbox }: GetAugmentedColumnsOptions,
|
|
52
|
+
): ColumnDef<TData, TValue>[] => {
|
|
53
|
+
const augmentedColumns: ColumnDef<TData, TValue>[] = stampExplicitSize(columns);
|
|
54
|
+
|
|
55
|
+
if (selectable) {
|
|
56
|
+
augmentedColumns.unshift({
|
|
57
|
+
id: SELECT_COLUMN_ID,
|
|
58
|
+
size: SELECT_COLUMN_WIDTH,
|
|
59
|
+
// Pin min/max to the fixed width: `enableResizing: false` blocks the
|
|
60
|
+
// handle but not the table-wide resize `minSize`, which would otherwise
|
|
61
|
+
// stretch this column.
|
|
62
|
+
minSize: SELECT_COLUMN_WIDTH,
|
|
63
|
+
maxSize: SELECT_COLUMN_WIDTH,
|
|
64
|
+
enableSorting: false,
|
|
65
|
+
enableResizing: false,
|
|
66
|
+
meta: EXPLICIT_SIZE_META,
|
|
67
|
+
header: ({ table }) => (showSelectAllCheckbox ? <DsTableHeaderSelectableCell table={table} /> : null),
|
|
68
|
+
cell: ({ row }) => <DsTableRowSelectableCell row={row} />,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (expandable) {
|
|
73
|
+
augmentedColumns.unshift({
|
|
74
|
+
id: EXPANDER_COLUMN_ID,
|
|
75
|
+
size: EXPANDER_COLUMN_WIDTH,
|
|
76
|
+
minSize: EXPANDER_COLUMN_WIDTH,
|
|
77
|
+
maxSize: EXPANDER_COLUMN_WIDTH,
|
|
78
|
+
enableSorting: false,
|
|
79
|
+
enableResizing: false,
|
|
80
|
+
meta: EXPLICIT_SIZE_META,
|
|
81
|
+
header: () => null,
|
|
82
|
+
cell: ({ row }) => (row.getCanExpand() ? <DsTableRowExpandableCell row={row} /> : null),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (reorderable && !virtualized) {
|
|
87
|
+
// Cell is rendered inline by DsTableRow when it encounters REORDER_COLUMN_ID,
|
|
88
|
+
// since the drag handle needs row-level useSortable state.
|
|
89
|
+
augmentedColumns.unshift({
|
|
90
|
+
id: REORDER_COLUMN_ID,
|
|
91
|
+
size: REORDER_COLUMN_WIDTH,
|
|
92
|
+
minSize: REORDER_COLUMN_WIDTH,
|
|
93
|
+
maxSize: REORDER_COLUMN_WIDTH,
|
|
94
|
+
enableSorting: false,
|
|
95
|
+
enableResizing: false,
|
|
96
|
+
meta: EXPLICIT_SIZE_META,
|
|
97
|
+
header: 'Order',
|
|
98
|
+
cell: () => null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return augmentedColumns;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Replaces each leaf `cell` with `loadingCell` (or a default text skeleton).
|
|
107
|
+
* Group columns are cloned with the same transform applied to their children.
|
|
108
|
+
*/
|
|
109
|
+
export const toSkeletonColumns = <TData, TValue>(
|
|
110
|
+
columns: ColumnDef<TData, TValue>[],
|
|
111
|
+
): ColumnDef<TData, TValue>[] =>
|
|
112
|
+
columns.map((column) =>
|
|
113
|
+
'columns' in column && column.columns
|
|
114
|
+
? { ...column, columns: toSkeletonColumns(column.columns) }
|
|
115
|
+
: { ...column, cell: column.loadingCell ?? (() => <DsSkeletonText width="60%" />) },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const createSkeletonRows = <TData extends { id: string }>(): TData[] =>
|
|
119
|
+
Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ({ id: `ds-table-skeleton-${String(i)}` }) as TData);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ColumnDef } from '@tanstack/react-table';
|
|
3
|
+
import { createSkeletonRows, getAugmentedColumns, toSkeletonColumns } from './table-columns';
|
|
4
|
+
import {
|
|
5
|
+
EXPANDER_COLUMN_ID,
|
|
6
|
+
EXPANDER_COLUMN_WIDTH,
|
|
7
|
+
REORDER_COLUMN_ID,
|
|
8
|
+
REORDER_COLUMN_WIDTH,
|
|
9
|
+
SELECT_COLUMN_ID,
|
|
10
|
+
SELECT_COLUMN_WIDTH,
|
|
11
|
+
SKELETON_ROW_COUNT,
|
|
12
|
+
} from './constants';
|
|
13
|
+
|
|
14
|
+
type Row = { id: string; name?: string };
|
|
15
|
+
|
|
16
|
+
const nameColumn: ColumnDef<Row> = { id: 'name', accessorKey: 'name' };
|
|
17
|
+
|
|
18
|
+
const noFeatures = {
|
|
19
|
+
selectable: false,
|
|
20
|
+
expandable: false,
|
|
21
|
+
reorderable: false,
|
|
22
|
+
virtualized: false,
|
|
23
|
+
showSelectAllCheckbox: true,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
const columnIds = (columns: ColumnDef<Row>[]) => columns.map((column) => column.id);
|
|
27
|
+
|
|
28
|
+
describe('getAugmentedColumns', () => {
|
|
29
|
+
it('returns consumer columns unchanged when no features are on', () => {
|
|
30
|
+
expect(columnIds(getAugmentedColumns([nameColumn], noFeatures))).toEqual(['name']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('prepends builtins in visual order: reorder, expander, select, then consumer columns', () => {
|
|
34
|
+
expect(
|
|
35
|
+
columnIds(
|
|
36
|
+
getAugmentedColumns([nameColumn], {
|
|
37
|
+
...noFeatures,
|
|
38
|
+
selectable: true,
|
|
39
|
+
expandable: true,
|
|
40
|
+
reorderable: true,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
).toEqual([REORDER_COLUMN_ID, EXPANDER_COLUMN_ID, SELECT_COLUMN_ID, 'name']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('omits select and expander when those flags are falsy', () => {
|
|
47
|
+
expect(columnIds(getAugmentedColumns([nameColumn], { ...noFeatures, reorderable: true }))).toEqual([
|
|
48
|
+
REORDER_COLUMN_ID,
|
|
49
|
+
'name',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('omits reorder when the table is virtualized', () => {
|
|
54
|
+
expect(
|
|
55
|
+
columnIds(
|
|
56
|
+
getAugmentedColumns([nameColumn], {
|
|
57
|
+
...noFeatures,
|
|
58
|
+
selectable: true,
|
|
59
|
+
expandable: true,
|
|
60
|
+
reorderable: true,
|
|
61
|
+
virtualized: true,
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
).toEqual([EXPANDER_COLUMN_ID, SELECT_COLUMN_ID, 'name']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('disables sorting and resizing on builtin columns', () => {
|
|
68
|
+
const columns = getAugmentedColumns([nameColumn], {
|
|
69
|
+
...noFeatures,
|
|
70
|
+
selectable: true,
|
|
71
|
+
expandable: true,
|
|
72
|
+
reorderable: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
for (const column of columns.slice(0, 3)) {
|
|
76
|
+
expect(column.enableSorting).toBe(false);
|
|
77
|
+
expect(column.enableResizing).toBe(false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('pins builtin column min/max to their fixed width so the resize minimum cannot stretch them', () => {
|
|
82
|
+
const columns = getAugmentedColumns([nameColumn], {
|
|
83
|
+
...noFeatures,
|
|
84
|
+
selectable: true,
|
|
85
|
+
expandable: true,
|
|
86
|
+
reorderable: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const boundsById = new Map(columns.map((column) => [column.id, column]));
|
|
90
|
+
|
|
91
|
+
for (const [id, width] of [
|
|
92
|
+
[SELECT_COLUMN_ID, SELECT_COLUMN_WIDTH],
|
|
93
|
+
[EXPANDER_COLUMN_ID, EXPANDER_COLUMN_WIDTH],
|
|
94
|
+
[REORDER_COLUMN_ID, REORDER_COLUMN_WIDTH],
|
|
95
|
+
] as const) {
|
|
96
|
+
const column = boundsById.get(id);
|
|
97
|
+
expect(column?.size).toBe(width);
|
|
98
|
+
expect(column?.minSize).toBe(width);
|
|
99
|
+
expect(column?.maxSize).toBe(width);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('stamps hasExplicitSize on authored leaves and builtins, not on unsized leaves', () => {
|
|
104
|
+
const columns = getAugmentedColumns(
|
|
105
|
+
[
|
|
106
|
+
{ id: 'name', accessorKey: 'name', size: 150 },
|
|
107
|
+
{ id: 'age', accessorKey: 'age' },
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
...noFeatures,
|
|
111
|
+
selectable: true,
|
|
112
|
+
expandable: true,
|
|
113
|
+
reorderable: true,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
117
|
+
|
|
118
|
+
expect(byId.get('name')?.meta?.hasExplicitSize).toBe(true);
|
|
119
|
+
expect(byId.get('age')?.meta?.hasExplicitSize).toBeUndefined();
|
|
120
|
+
expect(byId.get(SELECT_COLUMN_ID)?.meta?.hasExplicitSize).toBe(true);
|
|
121
|
+
expect(byId.get(EXPANDER_COLUMN_ID)?.meta?.hasExplicitSize).toBe(true);
|
|
122
|
+
expect(byId.get(REORDER_COLUMN_ID)?.meta?.hasExplicitSize).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('stamps hasExplicitSize on nested authored leaves and preserves group meta', () => {
|
|
126
|
+
const columns = getAugmentedColumns(
|
|
127
|
+
[
|
|
128
|
+
{
|
|
129
|
+
id: 'identity',
|
|
130
|
+
header: 'Identity',
|
|
131
|
+
meta: { group: { collapsible: true } },
|
|
132
|
+
columns: [{ id: 'firstName', size: 150 }, { id: 'lastName' }],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
noFeatures,
|
|
136
|
+
);
|
|
137
|
+
const group = columns[0];
|
|
138
|
+
const nested = group && 'columns' in group ? group.columns : undefined;
|
|
139
|
+
|
|
140
|
+
expect(group?.meta?.group).toEqual({ collapsible: true });
|
|
141
|
+
expect(nested?.[0]?.meta?.hasExplicitSize).toBe(true);
|
|
142
|
+
expect(nested?.[1]?.meta?.hasExplicitSize).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not mutate the caller’s column defs', () => {
|
|
146
|
+
const leaf: ColumnDef<Row> = { id: 'name', accessorKey: 'name', size: 150 };
|
|
147
|
+
|
|
148
|
+
getAugmentedColumns([leaf], noFeatures);
|
|
149
|
+
|
|
150
|
+
expect(leaf.meta).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('toSkeletonColumns', () => {
|
|
155
|
+
it('replaces a leaf cell with loadingCell when provided', () => {
|
|
156
|
+
const loadingCell = () => 'loading';
|
|
157
|
+
const result = toSkeletonColumns<Row, unknown>([{ id: 'name', cell: () => 'name', loadingCell }])[0];
|
|
158
|
+
|
|
159
|
+
expect(result?.cell).toBe(loadingCell);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('replaces a leaf cell with a default skeleton when loadingCell is omitted', () => {
|
|
163
|
+
const originalCell = () => 'name';
|
|
164
|
+
const result = toSkeletonColumns<Row, unknown>([{ id: 'name', cell: originalCell }])[0];
|
|
165
|
+
|
|
166
|
+
expect(result?.cell).not.toBe(originalCell);
|
|
167
|
+
expect(result?.cell).toBeTypeOf('function');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('recurses into group columns', () => {
|
|
171
|
+
const loadingCell = () => 'loading';
|
|
172
|
+
const leafCell = () => 'leaf';
|
|
173
|
+
const group = toSkeletonColumns<Row, unknown>([
|
|
174
|
+
{
|
|
175
|
+
id: 'group',
|
|
176
|
+
columns: [
|
|
177
|
+
{ id: 'a', cell: leafCell },
|
|
178
|
+
{ id: 'b', cell: leafCell, loadingCell },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
])[0];
|
|
182
|
+
|
|
183
|
+
const nested = group && 'columns' in group ? group.columns : undefined;
|
|
184
|
+
|
|
185
|
+
expect(nested?.[0]?.cell).not.toBe(leafCell);
|
|
186
|
+
expect(nested?.[1]?.cell).toBe(loadingCell);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('createSkeletonRows', () => {
|
|
191
|
+
it('returns placeholder rows with stable skeleton ids', () => {
|
|
192
|
+
const rows = createSkeletonRows<Row>();
|
|
193
|
+
|
|
194
|
+
expect(rows).toHaveLength(SKELETON_ROW_COUNT);
|
|
195
|
+
expect(rows.map((row) => row.id)).toEqual(
|
|
196
|
+
Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => `ds-table-skeleton-${String(i)}`),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=36566-107407
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-tag
|
|
3
|
+
// component=DsTag
|
|
4
|
+
//
|
|
5
|
+
// Figma "Type" maps to code `variant`, "Size" to `size`, "State" to the `selected`/`disabled`
|
|
6
|
+
// booleans (hover/focus are interaction states with no code prop). Figma's `Type=include-exclude`
|
|
7
|
+
// is a single variant backed by an internal, non-exposed include/exclude sub-toggle, so it can
|
|
8
|
+
// only be surfaced as one code value — it maps to `variant="include"`; switch to `variant="exclude"`
|
|
9
|
+
// for the exclude case. `label` is content, pulled from the tag text when present, otherwise a
|
|
10
|
+
// placeholder; `value` (required only for `key-value`) is emitted as a placeholder.
|
|
11
|
+
import figma from 'figma';
|
|
12
|
+
|
|
13
|
+
const instance = figma.selectedInstance;
|
|
14
|
+
|
|
15
|
+
const variant =
|
|
16
|
+
instance.getEnum('Type', {
|
|
17
|
+
default: 'default',
|
|
18
|
+
'include-exclude': 'include',
|
|
19
|
+
'key-value': 'key-value',
|
|
20
|
+
}) ?? 'default';
|
|
21
|
+
|
|
22
|
+
const size = instance.getEnum('Size', { medium: 'medium', small: 'small' }) ?? 'medium';
|
|
23
|
+
|
|
24
|
+
const state = instance.getEnum('State', {
|
|
25
|
+
default: 'default',
|
|
26
|
+
hover: 'hover',
|
|
27
|
+
focus: 'focus',
|
|
28
|
+
selected: 'selected',
|
|
29
|
+
disabled: 'disabled',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const labelNode = instance.findText('Tag-name', { traverseInstances: true });
|
|
33
|
+
const label = labelNode.type === 'TEXT' ? labelNode.textContent : 'Label';
|
|
34
|
+
|
|
35
|
+
const attrs = [
|
|
36
|
+
`label="${label}"`,
|
|
37
|
+
variant !== 'default' ? `variant="${variant}"` : '',
|
|
38
|
+
variant === 'key-value' ? 'value="Value"' : '',
|
|
39
|
+
size === 'small' ? 'size="small"' : '',
|
|
40
|
+
state === 'selected' ? 'selected' : '',
|
|
41
|
+
state === 'disabled' ? 'disabled' : '',
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' ');
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
example: figma.code`<DsTag ${attrs} />`,
|
|
48
|
+
imports: ["import { DsTag } from '@drivenets/design-system';"],
|
|
49
|
+
id: 'ds-tag',
|
|
50
|
+
metadata: { nestable: true },
|
|
51
|
+
} satisfies figma.Template;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=40298-1117
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-tag-filter
|
|
3
|
+
// component=DsTagFilter
|
|
4
|
+
//
|
|
5
|
+
// Figma's `Type` variant (less/all) maps to the component's internal `expanded` state (driven by
|
|
6
|
+
// useTagOverflowCalculation, surfaced only via `onExpand`) — there is no settable code prop, so it
|
|
7
|
+
// is intentionally not represented. `items` is content, not derivable from Figma, so a placeholder
|
|
8
|
+
// array is emitted for the developer to fill in.
|
|
9
|
+
import figma from 'figma';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
example: figma.code`<DsTagFilter items={[{ id: '1', label: 'Status: Active' }, { id: '2', label: 'Version: 1.0.0' }]} />`,
|
|
13
|
+
imports: ["import { DsTagFilter } from '@drivenets/design-system';"],
|
|
14
|
+
id: 'ds-tag-filter',
|
|
15
|
+
metadata: { nestable: true },
|
|
16
|
+
} satisfies figma.Template;
|
|
@@ -1,61 +1,33 @@
|
|
|
1
1
|
.container {
|
|
2
2
|
display: flex;
|
|
3
|
-
|
|
3
|
+
align-items: flex-start;
|
|
4
4
|
gap: var(--xs);
|
|
5
5
|
padding: var(--xs) var(--standard);
|
|
6
6
|
background: var(--background-secondary-hover);
|
|
7
7
|
border: 1px solid var(--outline-weak);
|
|
8
8
|
border-radius: var(--3xs);
|
|
9
9
|
overflow: hidden;
|
|
10
|
-
|
|
11
|
-
&.expanded {
|
|
12
|
-
max-height: none;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.header {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
width: 100%;
|
|
20
|
-
gap: var(--xs);
|
|
21
|
-
flex-shrink: 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.headerActions {
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
gap: var(--xs);
|
|
28
|
-
flex-shrink: 0;
|
|
29
|
-
margin-left: auto;
|
|
30
10
|
}
|
|
31
11
|
|
|
32
12
|
.tagsArea {
|
|
33
13
|
display: flex;
|
|
34
14
|
align-items: center;
|
|
35
|
-
|
|
15
|
+
flex: 1;
|
|
16
|
+
min-width: 0;
|
|
36
17
|
gap: var(--xs);
|
|
37
18
|
flex-wrap: wrap;
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
.
|
|
41
|
-
color: var(--font-secondary);
|
|
42
|
-
white-space: nowrap;
|
|
43
|
-
flex-shrink: 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.itemCheckbox {
|
|
21
|
+
.actions {
|
|
47
22
|
display: flex;
|
|
48
23
|
align-items: center;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.ghostButtonContent {
|
|
52
|
-
// TODO: remove once migrated 1.2 button to the ghost with transparent bg
|
|
53
|
-
background: transparent !important;
|
|
24
|
+
gap: var(--xs);
|
|
25
|
+
flex-shrink: 0;
|
|
54
26
|
}
|
|
55
27
|
|
|
56
28
|
.actionButton {
|
|
57
|
-
padding: 0;
|
|
58
29
|
flex-shrink: 0;
|
|
30
|
+
white-space: nowrap;
|
|
59
31
|
}
|
|
60
32
|
|
|
61
33
|
.measurementContainer {
|
|
@@ -4,17 +4,12 @@ import classNames from 'classnames';
|
|
|
4
4
|
import styles from './ds-tag-filter.module.scss';
|
|
5
5
|
import type { DsTagFilterProps, TagFilterItem } from './ds-tag-filter.types';
|
|
6
6
|
import { useTagOverflowCalculation } from './hooks/use-tag-overflow-calculation';
|
|
7
|
-
import { DsTypography } from '../ds-typography';
|
|
8
7
|
import { DsTag, type DsTagProps } from '../ds-tag';
|
|
9
|
-
import {
|
|
8
|
+
import { DsButtonV3 } from '../ds-button-v3';
|
|
10
9
|
import { DsIcon } from '../ds-icon';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Design system TagFilter component
|
|
14
|
-
*
|
|
15
|
-
* A component for displaying active filters as tags with overflow handling.
|
|
16
|
-
* Non-tag elements (label, expand/collapse, clear) sit in a header row.
|
|
17
|
-
* Tags occupy the full width below, wrapping as needed.
|
|
18
13
|
*/
|
|
19
14
|
const DsTagFilter = ({
|
|
20
15
|
items,
|
|
@@ -27,22 +22,17 @@ const DsTagFilter = ({
|
|
|
27
22
|
onExpand,
|
|
28
23
|
}: DsTagFilterProps) => {
|
|
29
24
|
const [expanded, setExpanded] = useState(false);
|
|
30
|
-
const
|
|
25
|
+
const tagsAreaRef = useRef<HTMLDivElement>(null);
|
|
31
26
|
const measurementRef = useRef<HTMLDivElement>(null);
|
|
32
27
|
|
|
33
28
|
const { visibleTagCount, hasOverflow } = useTagOverflowCalculation({
|
|
34
|
-
|
|
29
|
+
tagsAreaRef,
|
|
35
30
|
measurementRef,
|
|
36
31
|
totalItems: items.length,
|
|
37
32
|
expanded,
|
|
38
33
|
});
|
|
39
34
|
|
|
40
|
-
const {
|
|
41
|
-
label = 'Filtered by:',
|
|
42
|
-
clearButton = 'Clear all filters',
|
|
43
|
-
showMore = 'Show more',
|
|
44
|
-
showLess = 'Show less',
|
|
45
|
-
} = locale;
|
|
35
|
+
const { clearButton = 'Clear all filters', showMore = 'Show more', showLess = 'Show less' } = locale;
|
|
46
36
|
|
|
47
37
|
const visibleTags = expanded ? items : items.slice(0, visibleTagCount);
|
|
48
38
|
const hiddenCount = items.length - visibleTagCount;
|
|
@@ -79,54 +69,41 @@ const DsTagFilter = ({
|
|
|
79
69
|
</div>
|
|
80
70
|
);
|
|
81
71
|
|
|
72
|
+
const toggleLabel = expanded ? showLess : showMore;
|
|
73
|
+
|
|
82
74
|
return (
|
|
83
75
|
<>
|
|
84
|
-
<div
|
|
85
|
-
ref={
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
>
|
|
89
|
-
<div className={styles.header}>
|
|
90
|
-
{label && (
|
|
91
|
-
<DsTypography variant="body-sm-reg" className={styles.label}>
|
|
92
|
-
{label}
|
|
93
|
-
</DsTypography>
|
|
94
|
-
)}
|
|
76
|
+
<div className={classNames(styles.container, className)} style={style}>
|
|
77
|
+
<div ref={tagsAreaRef} className={styles.tagsArea} aria-live="polite">
|
|
78
|
+
{visibleTags.map((item) => renderTag(item))}
|
|
79
|
+
</div>
|
|
95
80
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</DsButton>
|
|
110
|
-
)}
|
|
81
|
+
<div className={styles.actions}>
|
|
82
|
+
{hasOverflow && (
|
|
83
|
+
<DsButtonV3
|
|
84
|
+
variant="tertiary"
|
|
85
|
+
size="small"
|
|
86
|
+
className={styles.actionButton}
|
|
87
|
+
aria-expanded={expanded}
|
|
88
|
+
onClick={handleExpandToggle}
|
|
89
|
+
>
|
|
90
|
+
{`${toggleLabel} (${String(hiddenCount)})`}
|
|
91
|
+
<DsIcon icon={expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'} size="tiny" aria-hidden />
|
|
92
|
+
</DsButtonV3>
|
|
93
|
+
)}
|
|
111
94
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{clearButton}
|
|
124
|
-
</DsButton>
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
95
|
+
{onClearAll && (
|
|
96
|
+
<DsButtonV3
|
|
97
|
+
variant="tertiary"
|
|
98
|
+
size="small"
|
|
99
|
+
icon="close"
|
|
100
|
+
className={styles.actionButton}
|
|
101
|
+
onClick={onClearAll}
|
|
102
|
+
>
|
|
103
|
+
{clearButton}
|
|
104
|
+
</DsButtonV3>
|
|
105
|
+
)}
|
|
127
106
|
</div>
|
|
128
|
-
|
|
129
|
-
<div className={styles.tagsArea}>{visibleTags.map((item) => renderTag(item))}</div>
|
|
130
107
|
</div>
|
|
131
108
|
{createPortal(measurementContainer, document.body)}
|
|
132
109
|
</>
|
|
@@ -40,7 +40,8 @@ export interface DsTagFilterProps {
|
|
|
40
40
|
*/
|
|
41
41
|
locale?: {
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* @deprecated No longer rendered. Retained for backwards compatibility;
|
|
44
|
+
* Will be removed in the future.
|
|
44
45
|
*/
|
|
45
46
|
label?: string;
|
|
46
47
|
/**
|
|
@@ -2,7 +2,7 @@ import { type RefObject, useCallback, useLayoutEffect, useState } from 'react';
|
|
|
2
2
|
import { fitTagsInRow, getContainerAvailableWidth, getElementMeasurements } from '../utils';
|
|
3
3
|
|
|
4
4
|
interface UseTagOverflowCalculationOptions {
|
|
5
|
-
|
|
5
|
+
tagsAreaRef: RefObject<HTMLDivElement | null>;
|
|
6
6
|
measurementRef: RefObject<HTMLDivElement | null>;
|
|
7
7
|
totalItems: number;
|
|
8
8
|
expanded: boolean;
|
|
@@ -14,13 +14,10 @@ interface UseTagOverflowCalculationResult {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Custom hook to calculate how many tags fit
|
|
18
|
-
*
|
|
19
|
-
* Tags occupy the full container width. Non-tag elements (label, clear button,
|
|
20
|
-
* expand control) live in a separate header row and don't affect the calculation.
|
|
17
|
+
* Custom hook to calculate how many tags fit on the first row of the tags-area.
|
|
21
18
|
*/
|
|
22
19
|
export const useTagOverflowCalculation = ({
|
|
23
|
-
|
|
20
|
+
tagsAreaRef,
|
|
24
21
|
measurementRef,
|
|
25
22
|
totalItems,
|
|
26
23
|
expanded,
|
|
@@ -31,11 +28,11 @@ export const useTagOverflowCalculation = ({
|
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
const calculateLayout = useCallback(() => {
|
|
34
|
-
if (!
|
|
31
|
+
if (!tagsAreaRef.current || !measurementRef.current) {
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
const
|
|
35
|
+
const tagsArea = tagsAreaRef.current;
|
|
39
36
|
const measurementContainer = measurementRef.current;
|
|
40
37
|
|
|
41
38
|
const { tagWidths, gap } = getElementMeasurements(measurementContainer);
|
|
@@ -45,13 +42,13 @@ export const useTagOverflowCalculation = ({
|
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
const
|
|
45
|
+
const availableWidth = getContainerAvailableWidth(tagsArea);
|
|
49
46
|
|
|
50
|
-
const { count } = fitTagsInRow(tagWidths,
|
|
47
|
+
const { count } = fitTagsInRow(tagWidths, availableWidth, gap);
|
|
51
48
|
const hasOverflow = count < tagWidths.length;
|
|
52
49
|
|
|
53
50
|
setState({ visibleTagCount: count, hasOverflow });
|
|
54
|
-
}, [
|
|
51
|
+
}, [tagsAreaRef, measurementRef]);
|
|
55
52
|
|
|
56
53
|
useLayoutEffect(() => {
|
|
57
54
|
const rafId = requestAnimationFrame(() => {
|
|
@@ -64,21 +61,19 @@ export const useTagOverflowCalculation = ({
|
|
|
64
61
|
});
|
|
65
62
|
});
|
|
66
63
|
|
|
67
|
-
if (
|
|
68
|
-
resizeObserver.observe(
|
|
64
|
+
if (tagsAreaRef.current) {
|
|
65
|
+
resizeObserver.observe(tagsAreaRef.current);
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
return () => {
|
|
72
69
|
cancelAnimationFrame(rafId);
|
|
73
70
|
resizeObserver.disconnect();
|
|
74
71
|
};
|
|
75
|
-
}, [
|
|
72
|
+
}, [tagsAreaRef, measurementRef, totalItems, expanded, calculateLayout]);
|
|
76
73
|
|
|
74
|
+
// While expanded, keep hasOverflow true so a width edge case can't hide the collapse control.
|
|
77
75
|
if (expanded) {
|
|
78
|
-
return {
|
|
79
|
-
visibleTagCount: totalItems,
|
|
80
|
-
hasOverflow: true,
|
|
81
|
-
};
|
|
76
|
+
return { ...state, hasOverflow: true };
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
return state;
|