@drivenets/design-system 0.18.0 → 0.19.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.cjs +15850 -10821
  3. package/dist/index.d.cts +785 -239
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +785 -239
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +15835 -10822
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +13 -13
  11. package/src/components/ds-bot-button/ds-bot-button.figma.ts +38 -0
  12. package/src/components/ds-bot-button/ds-bot-button.module.scss +41 -0
  13. package/src/components/ds-bot-button/ds-bot-button.tsx +48 -0
  14. package/src/components/ds-bot-button/ds-bot-button.types.ts +33 -0
  15. package/src/components/ds-bot-button/index.ts +2 -0
  16. package/src/components/ds-bot-button/net-gen-logo.tsx +46 -0
  17. package/src/components/ds-bulk-actions/components/action-button.tsx +42 -0
  18. package/src/components/ds-bulk-actions/components/menu-entries.tsx +76 -0
  19. package/src/components/ds-bulk-actions/components/overflow.tsx +75 -0
  20. package/src/components/ds-bulk-actions/ds-bulk-actions.figma.ts +28 -0
  21. package/src/components/ds-bulk-actions/ds-bulk-actions.module.scss +199 -0
  22. package/src/components/ds-bulk-actions/ds-bulk-actions.tsx +220 -0
  23. package/src/components/ds-bulk-actions/ds-bulk-actions.types.ts +170 -0
  24. package/src/components/ds-bulk-actions/ds-bulk-actions.unit.test.ts +195 -0
  25. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.ts +91 -0
  26. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.unit.test.ts +134 -0
  27. package/src/components/ds-bulk-actions/hooks/compute-bulk-actions-overflow.ts +85 -0
  28. package/src/components/ds-bulk-actions/hooks/use-bulk-actions-overflow.ts +153 -0
  29. package/src/components/ds-bulk-actions/index.ts +18 -0
  30. package/src/components/ds-button-v3/ds-button-v3.module.scss +2 -2
  31. package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +1 -1
  32. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  33. package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
  34. package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
  35. package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
  36. package/src/components/ds-empty-state/index.ts +3 -0
  37. package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
  38. package/src/components/ds-form-control/ds-form-control.tsx +18 -10
  39. package/src/components/ds-illustration/ds-illustration.figma.ts +34 -0
  40. package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
  41. package/src/components/ds-illustration/ds-illustration.tsx +29 -0
  42. package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
  43. package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
  44. package/src/components/ds-illustration/illustrations/device.tsx +133 -0
  45. package/src/components/ds-illustration/illustrations/done.tsx +107 -0
  46. package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
  47. package/src/components/ds-illustration/illustrations/error.tsx +86 -0
  48. package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
  49. package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
  50. package/src/components/ds-illustration/illustrations/index.ts +39 -0
  51. package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
  52. package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
  53. package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
  54. package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
  55. package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
  56. package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
  57. package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
  58. package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
  59. package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
  60. package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
  61. package/src/components/ds-illustration/illustrations/site.tsx +80 -0
  62. package/src/components/ds-illustration/index.ts +3 -0
  63. package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
  64. package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
  65. package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
  66. package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
  67. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
  68. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
  69. package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
  70. package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
  71. package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
  72. package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
  73. package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
  74. package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
  75. package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
  76. package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
  77. package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
  78. package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
  79. package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
  80. package/src/components/ds-main-menu/index.ts +9 -0
  81. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
  82. package/src/components/ds-popover/ds-popover.tsx +2 -1
  83. package/src/components/ds-popover/ds-popover.types.ts +7 -0
  84. package/src/components/ds-site-node/ds-site-node.tsx +2 -2
  85. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
  86. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
  87. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
  88. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
  89. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
  90. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
  91. package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
  92. package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
  93. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
  94. package/src/components/ds-table/context/ds-table-context.tsx +8 -0
  95. package/src/components/ds-table/ds-table.module.scss +26 -16
  96. package/src/components/ds-table/ds-table.tsx +46 -14
  97. package/src/components/ds-table/ds-table.types.ts +33 -3
  98. package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
  99. package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
  100. package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
  101. package/src/components/ds-table/styles/_variables.scss +0 -2
  102. package/src/components/ds-table/utils/column-size.ts +274 -1
  103. package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
  104. package/src/components/ds-table/utils/constants.ts +13 -5
  105. package/src/components/ds-table/utils/table-columns.tsx +27 -13
  106. package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -0
  107. package/src/components/ds-tag-filter/ds-tag-filter.module.scss +7 -35
  108. package/src/components/ds-tag-filter/ds-tag-filter.tsx +34 -57
  109. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -1
  110. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +13 -18
  111. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  112. package/src/components/ds-toggle/ds-toggle.tsx +15 -13
  113. package/src/components/ds-tooltip/ds-tooltip.tsx +6 -2
  114. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  115. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  116. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  117. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  118. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  119. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  120. package/src/index.ts +6 -0
  121. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  122. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  123. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  124. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  125. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  126. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -13,8 +13,13 @@ import {
13
13
  getResizableColumnStyle,
14
14
  getResizableHeaderStyle,
15
15
  getResizeOriginSize,
16
+ getUtilityColumnSizing,
17
+ growFillLeavesToContainer,
16
18
  isExplicitColumnWidth,
17
19
  omitBuiltinColumnSizing,
20
+ resolveUtilityColumnWidth,
21
+ shiftColumnTrack,
22
+ withUtilityColumnSizing,
18
23
  } from './column-size';
19
24
  import {
20
25
  EXPANDER_COLUMN_ID,
@@ -179,6 +184,115 @@ describe('collectMissingLeafSizing', () => {
179
184
  it('returns an empty map when every measured id already has a sizing entry', () => {
180
185
  expect(collectMissingLeafSizing({ a: 100 }, { a: 90 })).toEqual({});
181
186
  });
187
+
188
+ it('reconciles independent rounding so seeded widths match the measured total', () => {
189
+ const next = collectMissingLeafSizing({ a: 500.6, b: 500.6 }, {});
190
+
191
+ expect(next).toEqual({ a: 501, b: 500 });
192
+ });
193
+
194
+ it('absorbs rounding drift into missing ids and leaves existing widths unchanged', () => {
195
+ expect(collectMissingLeafSizing({ a: 500.6, b: 500.6 }, { b: 501 }, 1001)).toEqual({ a: 500 });
196
+ });
197
+
198
+ it('does not snap seeded widths to the container when measured columns overflow it', () => {
199
+ expect(collectMissingLeafSizing({ a: 1000, b: 1000 }, {}, 1116)).toEqual({ a: 1000, b: 1000 });
200
+ });
201
+
202
+ it('snaps a 1px measured overflow to the container width', () => {
203
+ const next = collectMissingLeafSizing({ a: 558.5, b: 558.5 }, {}, 1116);
204
+ const total = Object.values(next).reduce((sum, width) => sum + width, 0);
205
+
206
+ expect(total).toBe(1116);
207
+ });
208
+
209
+ it('distributes container underflow across fill leaves by their measured share', () => {
210
+ const next = collectMissingLeafSizing({ a: 200, b: 200 }, {}, 1200, ['a', 'b']);
211
+
212
+ expect(next).toEqual({ a: 600, b: 600 });
213
+ });
214
+
215
+ it('grows only fill leaves when the container is wider than the measured total', () => {
216
+ const next = collectMissingLeafSizing({ a: 200, b: 200, c: 200 }, {}, 1200, ['b', 'c']);
217
+
218
+ expect(next).toEqual({ a: 200, b: 500, c: 500 });
219
+ });
220
+ });
221
+
222
+ describe('growFillLeavesToContainer', () => {
223
+ it('moves leftover space onto uncapped fill leaves after a sibling hits maxSize', () => {
224
+ const next = growFillLeavesToContainer(
225
+ { a: 280, b: 360, c: 360 },
226
+ {},
227
+ ['a', 'b', 'c'],
228
+ 1200,
229
+ ['a', 'b', 'c'],
230
+ [
231
+ { id: 'a', columnDef: { maxSize: 280 } },
232
+ { id: 'b', columnDef: {} },
233
+ { id: 'c', columnDef: {} },
234
+ ],
235
+ );
236
+
237
+ expect(next.a).toBe(280);
238
+ expect(next.b).toBeGreaterThan(360);
239
+ expect(next.c).toBeGreaterThan(360);
240
+ expect((next.a ?? 0) + (next.b ?? 0) + (next.c ?? 0)).toBe(1200);
241
+ });
242
+
243
+ it('does not grow an explicit leaf when filling leftover space', () => {
244
+ const next = growFillLeavesToContainer(
245
+ { a: 280, b: 360, c: 360 },
246
+ {},
247
+ ['a', 'b', 'c'],
248
+ 1200,
249
+ ['b', 'c'],
250
+ [
251
+ { id: 'a', columnDef: { maxSize: 280 } },
252
+ { id: 'b', columnDef: {} },
253
+ { id: 'c', columnDef: {} },
254
+ ],
255
+ );
256
+
257
+ expect(next).toEqual({ a: 280, b: 460, c: 460 });
258
+ });
259
+ });
260
+
261
+ describe('shiftColumnTrack', () => {
262
+ const bounds = [
263
+ { id: 'a', columnDef: {} },
264
+ { id: 'b', columnDef: {} },
265
+ ];
266
+
267
+ it('returns the same map when delta is 0', () => {
268
+ const sizing = { a: 200, b: 200 };
269
+
270
+ expect(shiftColumnTrack(sizing, ['a', 'b'], 0, bounds)).toBe(sizing);
271
+ });
272
+
273
+ it('shrinks from the last leaf when the spacer appears', () => {
274
+ const next = shiftColumnTrack({ a: 500, b: 500 }, ['a', 'b'], -10, bounds);
275
+
276
+ expect(next.a).toBe(500);
277
+ expect(next.b).toBe(490);
278
+ expect((next.a ?? 0) + (next.b ?? 0)).toBe(990);
279
+ });
280
+
281
+ it('grows leaves when the spacer disappears', () => {
282
+ const next = shiftColumnTrack({ a: 500, b: 490 }, ['a', 'b'], 10, bounds);
283
+
284
+ expect((next.a ?? 0) + (next.b ?? 0)).toBe(1000);
285
+ });
286
+
287
+ it('does not shrink a leaf below minSize', () => {
288
+ const next = shiftColumnTrack({ a: 200, b: 52 }, ['a', 'b'], -10, [
289
+ { id: 'a', columnDef: {} },
290
+ { id: 'b', columnDef: { minSize: 52 } },
291
+ ]);
292
+
293
+ expect(next.b).toBe(52);
294
+ expect(next.a).toBe(190);
295
+ });
182
296
  });
183
297
 
184
298
  describe('omitBuiltinColumnSizing', () => {
@@ -211,3 +325,46 @@ describe('omitBuiltinColumnSizing', () => {
211
325
  expect(next).not.toBe(sizing);
212
326
  });
213
327
  });
328
+
329
+ describe('resolveUtilityColumnWidth', () => {
330
+ it('returns the fallback when the width is omitted, non-finite, or not positive', () => {
331
+ expect(resolveUtilityColumnWidth(undefined, 36)).toBe(36);
332
+ expect(resolveUtilityColumnWidth(0, 36)).toBe(36);
333
+ expect(resolveUtilityColumnWidth(-8, 36)).toBe(36);
334
+ expect(resolveUtilityColumnWidth(Number.NaN, 36)).toBe(36);
335
+ expect(resolveUtilityColumnWidth(Number.POSITIVE_INFINITY, 36)).toBe(36);
336
+ });
337
+
338
+ it('returns a positive finite width as-is', () => {
339
+ expect(resolveUtilityColumnWidth(48, 36)).toBe(48);
340
+ expect(resolveUtilityColumnWidth(1, 36)).toBe(1);
341
+ });
342
+ });
343
+
344
+ describe('withUtilityColumnSizing', () => {
345
+ it('strips inbound utility ids and overlays the authored widths', () => {
346
+ expect(
347
+ withUtilityColumnSizing(
348
+ { [SELECT_COLUMN_ID]: 80, firstName: 200 },
349
+ { [SELECT_COLUMN_ID]: 48, [EXPANDER_COLUMN_ID]: 40 },
350
+ ),
351
+ ).toEqual({ firstName: 200, [SELECT_COLUMN_ID]: 48, [EXPANDER_COLUMN_ID]: 40 });
352
+ });
353
+ });
354
+
355
+ describe('getUtilityColumnSizing', () => {
356
+ it('collects size entries for injected utility leaves only', () => {
357
+ expect(
358
+ getUtilityColumnSizing([
359
+ { id: REORDER_COLUMN_ID, size: 80 },
360
+ { id: EXPANDER_COLUMN_ID, size: 40 },
361
+ { id: SELECT_COLUMN_ID, size: 48 },
362
+ { id: 'firstName', size: 200 },
363
+ ]),
364
+ ).toEqual({
365
+ [REORDER_COLUMN_ID]: 80,
366
+ [EXPANDER_COLUMN_ID]: 40,
367
+ [SELECT_COLUMN_ID]: 48,
368
+ });
369
+ });
370
+ });
@@ -1,8 +1,3 @@
1
- /**
2
- * Default text for empty table state
3
- */
4
- export const EMPTY_TABLE_STATE_TEXT = 'No results.';
5
-
6
1
  /**
7
2
  * Column id used for the synthetic expander column injected when `expandable` is set.
8
3
  */
@@ -55,6 +50,19 @@ export const SKELETON_ROW_COUNT = 5;
55
50
  */
56
51
  export const RESIZE_MIN_COLUMN_WIDTH = 52;
57
52
 
53
+ /**
54
+ * Pixel width of the full-height resize overlay divider. Must match
55
+ * `$resize-divider-width` in `styles/_variables.scss`.
56
+ */
57
+ export const RESIZE_DIVIDER_WIDTH = 2;
58
+
59
+ /**
60
+ * Rest width of the header **Scrollbar spacer** when the body overflows
61
+ * vertically. Must match `$scrollbar-default-size` in `styles/_scrollbars.scss`.
62
+ * Hover (12px) is intentionally not tracked.
63
+ */
64
+ export const SCROLLBAR_SPACER_WIDTH = 10;
65
+
58
66
  /**
59
67
  * Width change (in px) applied per Arrow key press when a resize handle is
60
68
  * focused.
@@ -3,6 +3,7 @@ import { DsSkeletonText } from '../../ds-skeleton';
3
3
  import { DsTableHeaderSelectableCell } from '../components/ds-table-header-selectable-cell';
4
4
  import { DsTableRowExpandableCell } from '../components/ds-table-row-expandable-cell';
5
5
  import { DsTableRowSelectableCell } from '../components/ds-table-row-selectable-cell';
6
+ import { resolveUtilityColumnWidth } from './column-size';
6
7
  import {
7
8
  EXPANDER_COLUMN_ID,
8
9
  EXPANDER_COLUMN_WIDTH,
@@ -19,6 +20,9 @@ export type GetAugmentedColumnsOptions = {
19
20
  reorderable: boolean;
20
21
  virtualized: boolean;
21
22
  showSelectAllCheckbox: boolean;
23
+ selectableColumnWidth?: number;
24
+ expandableColumnWidth?: number;
25
+ reorderableColumnWidth?: number;
22
26
  };
23
27
 
24
28
  const EXPLICIT_SIZE_META = { hasExplicitSize: true } as const;
@@ -41,6 +45,13 @@ const stampExplicitSize = <TData, TValue>(columns: ColumnDef<TData, TValue>[]):
41
45
  return { ...column, meta: { ...column.meta, ...EXPLICIT_SIZE_META } };
42
46
  });
43
47
 
48
+ /** Pin min/max so the table-wide resize `minSize` cannot stretch this column. */
49
+ const pinUtilityColumnSize = (width: number) => ({
50
+ size: width,
51
+ minSize: width,
52
+ maxSize: width,
53
+ });
54
+
44
55
  /**
45
56
  * Prepends the builtin select / expander / reorder columns when those features
46
57
  * are on. Reorder is omitted for virtualized tables. Unshift order yields
@@ -48,19 +59,24 @@ const stampExplicitSize = <TData, TValue>(columns: ColumnDef<TData, TValue>[]):
48
59
  */
49
60
  export const getAugmentedColumns = <TData, TValue>(
50
61
  columns: ColumnDef<TData, TValue>[],
51
- { selectable, expandable, reorderable, virtualized, showSelectAllCheckbox }: GetAugmentedColumnsOptions,
62
+ {
63
+ selectable,
64
+ expandable,
65
+ reorderable,
66
+ virtualized,
67
+ showSelectAllCheckbox,
68
+ selectableColumnWidth,
69
+ expandableColumnWidth,
70
+ reorderableColumnWidth,
71
+ }: GetAugmentedColumnsOptions,
52
72
  ): ColumnDef<TData, TValue>[] => {
53
73
  const augmentedColumns: ColumnDef<TData, TValue>[] = stampExplicitSize(columns);
54
74
 
55
75
  if (selectable) {
76
+ const width = resolveUtilityColumnWidth(selectableColumnWidth, SELECT_COLUMN_WIDTH);
56
77
  augmentedColumns.unshift({
57
78
  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,
79
+ ...pinUtilityColumnSize(width),
64
80
  enableSorting: false,
65
81
  enableResizing: false,
66
82
  meta: EXPLICIT_SIZE_META,
@@ -70,11 +86,10 @@ export const getAugmentedColumns = <TData, TValue>(
70
86
  }
71
87
 
72
88
  if (expandable) {
89
+ const width = resolveUtilityColumnWidth(expandableColumnWidth, EXPANDER_COLUMN_WIDTH);
73
90
  augmentedColumns.unshift({
74
91
  id: EXPANDER_COLUMN_ID,
75
- size: EXPANDER_COLUMN_WIDTH,
76
- minSize: EXPANDER_COLUMN_WIDTH,
77
- maxSize: EXPANDER_COLUMN_WIDTH,
92
+ ...pinUtilityColumnSize(width),
78
93
  enableSorting: false,
79
94
  enableResizing: false,
80
95
  meta: EXPLICIT_SIZE_META,
@@ -84,13 +99,12 @@ export const getAugmentedColumns = <TData, TValue>(
84
99
  }
85
100
 
86
101
  if (reorderable && !virtualized) {
102
+ const width = resolveUtilityColumnWidth(reorderableColumnWidth, REORDER_COLUMN_WIDTH);
87
103
  // Cell is rendered inline by DsTableRow when it encounters REORDER_COLUMN_ID,
88
104
  // since the drag handle needs row-level useSortable state.
89
105
  augmentedColumns.unshift({
90
106
  id: REORDER_COLUMN_ID,
91
- size: REORDER_COLUMN_WIDTH,
92
- minSize: REORDER_COLUMN_WIDTH,
93
- maxSize: REORDER_COLUMN_WIDTH,
107
+ ...pinUtilityColumnSize(width),
94
108
  enableSorting: false,
95
109
  enableResizing: false,
96
110
  meta: EXPLICIT_SIZE_META,
@@ -100,6 +100,50 @@ describe('getAugmentedColumns', () => {
100
100
  }
101
101
  });
102
102
 
103
+ it('pins custom utility column widths and ignores them when the feature is off', () => {
104
+ const columns = getAugmentedColumns([nameColumn], {
105
+ ...noFeatures,
106
+ selectable: true,
107
+ expandable: true,
108
+ reorderable: true,
109
+ selectableColumnWidth: 48,
110
+ expandableColumnWidth: 40,
111
+ reorderableColumnWidth: 80,
112
+ });
113
+ const boundsById = new Map(columns.map((column) => [column.id, column]));
114
+
115
+ expect(boundsById.get(SELECT_COLUMN_ID)?.size).toBe(48);
116
+ expect(boundsById.get(SELECT_COLUMN_ID)?.minSize).toBe(48);
117
+ expect(boundsById.get(SELECT_COLUMN_ID)?.maxSize).toBe(48);
118
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.size).toBe(40);
119
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.minSize).toBe(40);
120
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.maxSize).toBe(40);
121
+ expect(boundsById.get(REORDER_COLUMN_ID)?.size).toBe(80);
122
+ expect(boundsById.get(REORDER_COLUMN_ID)?.minSize).toBe(80);
123
+ expect(boundsById.get(REORDER_COLUMN_ID)?.maxSize).toBe(80);
124
+
125
+ expect(
126
+ columnIds(getAugmentedColumns([nameColumn], { ...noFeatures, selectableColumnWidth: 48 })),
127
+ ).toEqual(['name']);
128
+ });
129
+
130
+ it('falls back to the default width when the custom width is invalid', () => {
131
+ const columns = getAugmentedColumns([nameColumn], {
132
+ ...noFeatures,
133
+ selectable: true,
134
+ expandable: true,
135
+ reorderable: true,
136
+ selectableColumnWidth: 0,
137
+ expandableColumnWidth: Number.NaN,
138
+ reorderableColumnWidth: -10,
139
+ });
140
+ const boundsById = new Map(columns.map((column) => [column.id, column]));
141
+
142
+ expect(boundsById.get(SELECT_COLUMN_ID)?.size).toBe(SELECT_COLUMN_WIDTH);
143
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.size).toBe(EXPANDER_COLUMN_WIDTH);
144
+ expect(boundsById.get(REORDER_COLUMN_ID)?.size).toBe(REORDER_COLUMN_WIDTH);
145
+ });
146
+
103
147
  it('stamps hasExplicitSize on authored leaves and builtins, not on unsized leaves', () => {
104
148
  const columns = getAugmentedColumns(
105
149
  [
@@ -1,61 +1,33 @@
1
1
  .container {
2
2
  display: flex;
3
- flex-direction: column;
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
- width: 100%;
15
+ flex: 1;
16
+ min-width: 0;
36
17
  gap: var(--xs);
37
18
  flex-wrap: wrap;
38
19
  }
39
20
 
40
- .label {
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 { DsButton } from '../ds-button';
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 containerRef = useRef<HTMLDivElement>(null);
25
+ const tagsAreaRef = useRef<HTMLDivElement>(null);
31
26
  const measurementRef = useRef<HTMLDivElement>(null);
32
27
 
33
28
  const { visibleTagCount, hasOverflow } = useTagOverflowCalculation({
34
- containerRef,
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={containerRef}
86
- className={classNames(styles.container, expanded && styles.expanded, className)}
87
- style={style}
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
- <div className={styles.headerActions}>
97
- {hasOverflow && (
98
- <DsButton
99
- design="v1.2"
100
- buttonType="tertiary"
101
- variant="ghost"
102
- className={styles.actionButton}
103
- contentClassName={styles.ghostButtonContent}
104
- size="small"
105
- onClick={handleExpandToggle}
106
- >
107
- <DsIcon icon={expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'} />
108
- {expanded ? showLess : `${showMore} (${String(hiddenCount)})`}
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
- {onClearAll && (
113
- <DsButton
114
- design="v1.2"
115
- buttonType="tertiary"
116
- variant="ghost"
117
- className={styles.actionButton}
118
- contentClassName={styles.ghostButtonContent}
119
- size="small"
120
- onClick={onClearAll}
121
- >
122
- <DsIcon icon="close" />
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
- * Heading label shown above the tag list (e.g., "Filters").
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
- containerRef: RefObject<HTMLDivElement | null>;
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 in the available container width.
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
- containerRef,
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 (!containerRef.current || !measurementRef.current) {
31
+ if (!tagsAreaRef.current || !measurementRef.current) {
35
32
  return;
36
33
  }
37
34
 
38
- const container = containerRef.current;
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 containerWidth = getContainerAvailableWidth(container);
45
+ const availableWidth = getContainerAvailableWidth(tagsArea);
49
46
 
50
- const { count } = fitTagsInRow(tagWidths, containerWidth, gap);
47
+ const { count } = fitTagsInRow(tagWidths, availableWidth, gap);
51
48
  const hasOverflow = count < tagWidths.length;
52
49
 
53
50
  setState({ visibleTagCount: count, hasOverflow });
54
- }, [containerRef, measurementRef]);
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 (containerRef.current) {
68
- resizeObserver.observe(containerRef.current);
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
- }, [containerRef, measurementRef, totalItems, expanded, calculateLayout]);
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;
@@ -144,6 +144,24 @@
144
144
  pointer-events: none;
145
145
  }
146
146
 
147
+ // Overlay the native input over the control so `getByRole('checkbox')` clicks
148
+ // land on a real interactive surface. Ark applies `visuallyHiddenStyle` inline
149
+ // (1x1, clipped, off-screen) which Playwright refuses to click; `!important`
150
+ // is required to beat inline-style specificity.
151
+ .hiddenInput {
152
+ position: absolute !important;
153
+ inset-block-start: 0 !important;
154
+ inset-inline-start: 0 !important;
155
+ width: calc(var(--switch-track-width) + var(--thumb-gap) * 2) !important;
156
+ height: calc(var(--thumb-size) + var(--thumb-gap) * 2) !important;
157
+ margin: 0 !important;
158
+ padding: 0 !important;
159
+ clip: auto !important;
160
+ overflow: visible !important;
161
+ opacity: 0 !important;
162
+ cursor: pointer !important;
163
+ }
164
+
147
165
  .iconCheck {
148
166
  left: var(--icon-spacing);
149
167
  }