@drivenets/design-system 0.18.1 → 0.20.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 (216) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.cjs +17038 -10114
  3. package/dist/index.d.cts +1355 -444
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1354 -443
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17073 -10172
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +41 -41
  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-checkbox/ds-checkbox.figma.ts +3 -0
  33. package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
  34. package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
  35. package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
  36. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
  37. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
  38. package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
  39. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
  40. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
  41. package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
  42. package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
  43. package/src/components/ds-code-input/ds-code-input.tsx +401 -0
  44. package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
  45. package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
  46. package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
  47. package/src/components/ds-code-input/index.ts +2 -0
  48. package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
  49. package/src/components/ds-code-input/use-editor-host.ts +87 -0
  50. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  51. package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
  52. package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
  53. package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
  54. package/src/components/ds-empty-state/index.ts +3 -0
  55. package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
  56. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
  57. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
  58. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
  59. package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
  60. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
  61. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
  62. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
  63. package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
  64. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
  65. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
  66. package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
  67. package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
  68. package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
  69. package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
  70. package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
  71. package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
  72. package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
  73. package/src/components/ds-filters-bar/components/index.ts +11 -0
  74. package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
  75. package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
  76. package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
  77. package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
  78. package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
  79. package/src/components/ds-filters-bar/index.ts +1 -0
  80. package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
  81. package/src/components/ds-form-control/ds-form-control.tsx +61 -19
  82. package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
  83. package/src/components/ds-icon/custom-icons/index.ts +2 -0
  84. package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
  85. package/src/components/ds-illustration/ds-illustration.figma.ts +34 -0
  86. package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
  87. package/src/components/ds-illustration/ds-illustration.tsx +29 -0
  88. package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
  89. package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
  90. package/src/components/ds-illustration/illustrations/device.tsx +133 -0
  91. package/src/components/ds-illustration/illustrations/done.tsx +107 -0
  92. package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
  93. package/src/components/ds-illustration/illustrations/error.tsx +86 -0
  94. package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
  95. package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
  96. package/src/components/ds-illustration/illustrations/index.ts +39 -0
  97. package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
  98. package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
  99. package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
  100. package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
  101. package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
  102. package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
  103. package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
  104. package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
  105. package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
  106. package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
  107. package/src/components/ds-illustration/illustrations/site.tsx +80 -0
  108. package/src/components/ds-illustration/index.ts +3 -0
  109. package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
  110. package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
  111. package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
  112. package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
  113. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
  114. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
  115. package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
  116. package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
  117. package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
  118. package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
  119. package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
  120. package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
  121. package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
  122. package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
  123. package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
  124. package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
  125. package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
  126. package/src/components/ds-main-menu/index.ts +9 -0
  127. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
  128. package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
  129. package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
  130. package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
  131. package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
  132. package/src/components/ds-pin-toggle/index.ts +2 -0
  133. package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
  134. package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
  135. package/src/components/ds-popover/ds-popover.context.ts +18 -0
  136. package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
  137. package/src/components/ds-popover/ds-popover.tsx +164 -41
  138. package/src/components/ds-popover/ds-popover.types.ts +65 -0
  139. package/src/components/ds-popover/ds-popover.utils.ts +60 -0
  140. package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
  141. package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
  142. package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
  143. package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
  144. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
  145. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
  146. package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
  147. package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
  148. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
  149. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
  150. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
  151. package/src/components/ds-saved-filters/components/index.ts +2 -0
  152. package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
  153. package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
  154. package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
  155. package/src/components/ds-saved-filters/hooks/index.ts +1 -0
  156. package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
  157. package/src/components/ds-saved-filters/index.ts +2 -0
  158. package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
  159. package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
  160. package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
  161. package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
  162. package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
  163. package/src/components/ds-site-node/ds-site-node.tsx +2 -2
  164. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
  165. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
  166. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
  167. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
  168. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
  169. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
  170. package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
  171. package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
  172. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
  173. package/src/components/ds-table/context/ds-table-context.tsx +8 -0
  174. package/src/components/ds-table/ds-table.module.scss +26 -16
  175. package/src/components/ds-table/ds-table.tsx +46 -14
  176. package/src/components/ds-table/ds-table.types.ts +33 -3
  177. package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
  178. package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
  179. package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
  180. package/src/components/ds-table/styles/_variables.scss +0 -2
  181. package/src/components/ds-table/utils/column-size.ts +274 -1
  182. package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
  183. package/src/components/ds-table/utils/constants.ts +13 -5
  184. package/src/components/ds-table/utils/table-columns.tsx +27 -13
  185. package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -0
  186. package/src/components/ds-tag/ds-tag.figma.ts +23 -3
  187. package/src/components/ds-tag/ds-tag.module.scss +71 -7
  188. package/src/components/ds-tag/ds-tag.tsx +115 -14
  189. package/src/components/ds-tag/ds-tag.types.ts +70 -3
  190. package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
  191. package/src/components/ds-tag/index.ts +2 -2
  192. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
  193. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  194. package/src/components/ds-toggle/ds-toggle.tsx +15 -9
  195. package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
  196. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
  197. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
  198. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
  199. package/src/components/ds-toggle-filter-data/index.ts +1 -0
  200. package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
  201. package/src/components/ds-tooltip/ds-tooltip.tsx +7 -3
  202. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  203. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  204. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  205. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  206. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  207. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  208. package/src/components/ds-tree/ds-tree.tsx +10 -10
  209. package/src/components/ds-tree/ds-tree.types.ts +40 -7
  210. package/src/index.ts +8 -0
  211. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  212. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  213. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  214. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  215. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  216. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -0,0 +1,195 @@
1
+ /* eslint-disable vitest/expect-expect */
2
+ import { describe, expect, expectTypeOf, it } from 'vitest';
3
+ import type {
4
+ DsBulkActionsItemProps,
5
+ DsBulkActionsMenuEntry,
6
+ DsBulkActionsProps,
7
+ } from './ds-bulk-actions.types';
8
+ import { computeBulkActionsOverflow } from './hooks/compute-bulk-actions-overflow';
9
+
10
+ type ClickHandler = () => void;
11
+
12
+ const chrome = {
13
+ badgeWidth: 60,
14
+ dismissWidth: 40,
15
+ labelWidth: 120,
16
+ gap: 4,
17
+ moreWidth: 70,
18
+ };
19
+
20
+ describe('DsBulkActionsItemProps discriminated union', () => {
21
+ it('allows click items with icon, label, and onClick', () => {
22
+ expectTypeOf<{ icon: 'edit'; label: string; onClick: ClickHandler }>().toExtend<DsBulkActionsItemProps>();
23
+ expectTypeOf<{
24
+ icon: 'edit';
25
+ label: string;
26
+ onClick: ClickHandler;
27
+ disabled: true;
28
+ width: 120;
29
+ }>().toExtend<DsBulkActionsItemProps>();
30
+ expectTypeOf<{
31
+ icon: 'edit';
32
+ label: string;
33
+ onClick: ClickHandler;
34
+ width: 'fixed';
35
+ }>().toExtend<DsBulkActionsItemProps>();
36
+ expectTypeOf<{
37
+ icon: 'edit';
38
+ label: string;
39
+ onClick: ClickHandler;
40
+ width: 'fit-content';
41
+ }>().toExtend<DsBulkActionsItemProps>();
42
+ });
43
+
44
+ it('allows menu items with icon, label, and menu', () => {
45
+ expectTypeOf<{
46
+ icon: 'share';
47
+ label: string;
48
+ menu: DsBulkActionsMenuEntry[];
49
+ }>().toExtend<DsBulkActionsItemProps>();
50
+ expectTypeOf<{
51
+ icon: 'share';
52
+ label: string;
53
+ disabled: true;
54
+ menu: Array<
55
+ | {
56
+ value: string;
57
+ label: string;
58
+ icon: 'mail';
59
+ onSelect: ClickHandler;
60
+ disabled: false;
61
+ variant: 'error';
62
+ }
63
+ | {
64
+ value: string;
65
+ label: string;
66
+ menu: Array<{ value: string; label: string; onSelect: ClickHandler }>;
67
+ }
68
+ >;
69
+ }>().toExtend<DsBulkActionsItemProps>();
70
+ });
71
+
72
+ it('rejects mixing onClick with menu', () => {
73
+ expectTypeOf<{
74
+ icon: 'edit';
75
+ label: string;
76
+ onClick: ClickHandler;
77
+ menu: DsBulkActionsMenuEntry[];
78
+ }>().not.toExtend<DsBulkActionsItemProps>();
79
+ });
80
+
81
+ it('rejects children composition', () => {
82
+ expectTypeOf<{ children: string }>().not.toExtend<DsBulkActionsItemProps>();
83
+ expectTypeOf<{
84
+ icon: 'edit';
85
+ label: string;
86
+ onClick: ClickHandler;
87
+ children: string;
88
+ }>().not.toExtend<DsBulkActionsItemProps>();
89
+ expectTypeOf<{
90
+ icon: 'share';
91
+ label: string;
92
+ menu: DsBulkActionsMenuEntry[];
93
+ children: string;
94
+ }>().not.toExtend<DsBulkActionsItemProps>();
95
+ });
96
+
97
+ it('requires icon, label, and either onClick or menu', () => {
98
+ expectTypeOf<{ icon: 'edit'; label: string }>().not.toExtend<DsBulkActionsItemProps>();
99
+ expectTypeOf<{ icon: 'edit'; onClick: ClickHandler }>().not.toExtend<DsBulkActionsItemProps>();
100
+ expectTypeOf<{ label: string; onClick: ClickHandler }>().not.toExtend<DsBulkActionsItemProps>();
101
+ expectTypeOf<{ icon: 'share'; menu: DsBulkActionsMenuEntry[] }>().not.toExtend<DsBulkActionsItemProps>();
102
+ });
103
+ });
104
+
105
+ describe('DsBulkActionsMenuEntry', () => {
106
+ it('allows a leaf with onSelect or a branch with nested menu, not both', () => {
107
+ expectTypeOf<{
108
+ value: string;
109
+ label: string;
110
+ icon: 'mail';
111
+ onSelect: ClickHandler;
112
+ disabled: true;
113
+ variant: 'error';
114
+ }>().toExtend<DsBulkActionsMenuEntry>();
115
+ expectTypeOf<{
116
+ value: string;
117
+ label: string;
118
+ menu: Array<{ value: string; label: string; onSelect: ClickHandler }>;
119
+ }>().toExtend<DsBulkActionsMenuEntry>();
120
+ expectTypeOf<{ value: string; label: string }>().toExtend<DsBulkActionsMenuEntry>();
121
+ expectTypeOf<{
122
+ value: string;
123
+ label: string;
124
+ onSelect: ClickHandler;
125
+ menu: DsBulkActionsMenuEntry[];
126
+ }>().not.toExtend<DsBulkActionsMenuEntry>();
127
+ });
128
+ });
129
+
130
+ describe('DsBulkActionsProps', () => {
131
+ it('accepts children action slots, menuPlacement, and locale.moreActionsLabel', () => {
132
+ expectTypeOf<{
133
+ selectedCount: number;
134
+ onClearSelection: ClickHandler;
135
+ menuPlacement: 'top';
136
+ locale: { moreActionsLabel: string };
137
+ }>().toExtend<DsBulkActionsProps>();
138
+
139
+ expectTypeOf<DsBulkActionsProps>().toHaveProperty('children');
140
+ expectTypeOf<DsBulkActionsProps>().toHaveProperty('menuPlacement');
141
+ expectTypeOf<DsBulkActionsProps>().not.toHaveProperty('actions');
142
+ });
143
+ });
144
+
145
+ describe('computeBulkActionsOverflow', () => {
146
+ it('keeps every item visible when the bar is wide enough', () => {
147
+ expect(
148
+ computeBulkActionsOverflow({
149
+ ...chrome,
150
+ containerWidth: 600,
151
+ itemWidths: [70, 70, 70, 70],
152
+ }),
153
+ ).toEqual({ visibleCount: 4, showMore: false, summaryLabelHidden: false });
154
+ });
155
+
156
+ it('collapses rightmost items into More and keeps the summary label', () => {
157
+ expect(
158
+ computeBulkActionsOverflow({
159
+ ...chrome,
160
+ containerWidth: 450,
161
+ itemWidths: [70, 70, 70, 70, 70],
162
+ }),
163
+ ).toEqual({ visibleCount: 2, showMore: true, summaryLabelHidden: false });
164
+ });
165
+
166
+ it('hides the summary label only after collapsing to one visible action', () => {
167
+ expect(
168
+ computeBulkActionsOverflow({
169
+ ...chrome,
170
+ containerWidth: 250,
171
+ itemWidths: [70, 70, 70, 70, 70],
172
+ }),
173
+ ).toEqual({ visibleCount: 1, showMore: true, summaryLabelHidden: true });
174
+ });
175
+
176
+ it('does not render More when a single item remains', () => {
177
+ expect(
178
+ computeBulkActionsOverflow({
179
+ ...chrome,
180
+ containerWidth: 200,
181
+ itemWidths: [70],
182
+ }),
183
+ ).toEqual({ visibleCount: 1, showMore: false, summaryLabelHidden: true });
184
+ });
185
+
186
+ it('returns no actions when the bar has none', () => {
187
+ expect(
188
+ computeBulkActionsOverflow({
189
+ ...chrome,
190
+ containerWidth: 400,
191
+ itemWidths: [],
192
+ }),
193
+ ).toEqual({ visibleCount: 0, showMore: false, summaryLabelHidden: false });
194
+ });
195
+ });
@@ -0,0 +1,91 @@
1
+ import {
2
+ Children,
3
+ Fragment,
4
+ isValidElement,
5
+ type CSSProperties,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from 'react';
9
+ import type {
10
+ BulkActionsItemWidthMode,
11
+ DsBulkActionsItemProps,
12
+ DsBulkActionsItemWidth,
13
+ DsBulkActionsMenuEntry,
14
+ } from './ds-bulk-actions.types';
15
+
16
+ const ITEM_WIDTH_CUSTOM_VAR = '--ds-bulk-actions-item-width';
17
+
18
+ export interface ItemWidthLayout {
19
+ mode: BulkActionsItemWidthMode | 'custom';
20
+ buttonStyle?: CSSProperties;
21
+ }
22
+
23
+ export const resolveItemWidth = (width?: DsBulkActionsItemWidth): ItemWidthLayout => {
24
+ if (width === undefined || width === 'fixed') {
25
+ return { mode: 'fixed' };
26
+ }
27
+
28
+ if (width === 'fit-content') {
29
+ return { mode: 'fit-content' };
30
+ }
31
+
32
+ return {
33
+ mode: 'custom',
34
+ buttonStyle: { [ITEM_WIDTH_CUSTOM_VAR]: `${String(width)}px` } as CSSProperties,
35
+ };
36
+ };
37
+
38
+ export const isMenuItemProps = (
39
+ props: DsBulkActionsItemProps,
40
+ ): props is Extract<DsBulkActionsItemProps, { menu: DsBulkActionsMenuEntry[] }> =>
41
+ 'menu' in props && props.menu !== undefined;
42
+
43
+ export const isSubmenuEntry = (
44
+ entry: DsBulkActionsMenuEntry,
45
+ ): entry is Extract<DsBulkActionsMenuEntry, { menu: DsBulkActionsMenuEntry[] }> =>
46
+ 'menu' in entry && entry.menu !== undefined;
47
+
48
+ export const isItemElement = (child: ReactNode): child is ReactElement<DsBulkActionsItemProps> =>
49
+ isValidElement(child) &&
50
+ typeof child.type === 'function' &&
51
+ 'displayName' in child.type &&
52
+ child.type.displayName === 'DsBulkActions.Item';
53
+
54
+ export const getBulkActionItems = (children: ReactNode): ReactElement<DsBulkActionsItemProps>[] => {
55
+ const items: ReactElement<DsBulkActionsItemProps>[] = [];
56
+
57
+ Children.forEach(children, (child) => {
58
+ if (isItemElement(child)) {
59
+ items.push(child);
60
+ return;
61
+ }
62
+
63
+ if (isValidElement(child) && child.type === Fragment) {
64
+ items.push(...getBulkActionItems((child.props as { children?: ReactNode }).children));
65
+ }
66
+ });
67
+
68
+ return items;
69
+ };
70
+
71
+ export const itemToOverflowEntry = (item: DsBulkActionsItemProps, index: number): DsBulkActionsMenuEntry => {
72
+ const { icon, label, disabled } = item;
73
+
74
+ if (isMenuItemProps(item)) {
75
+ return {
76
+ value: `overflow-${String(index)}`,
77
+ label,
78
+ icon,
79
+ disabled,
80
+ menu: item.menu,
81
+ };
82
+ }
83
+
84
+ return {
85
+ value: `overflow-${String(index)}`,
86
+ label,
87
+ icon,
88
+ disabled,
89
+ onSelect: item.onClick,
90
+ };
91
+ };
@@ -0,0 +1,134 @@
1
+ import { createElement, Fragment, type FunctionComponent } from 'react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import type { DsBulkActionsItemProps } from './ds-bulk-actions.types';
4
+ import {
5
+ getBulkActionItems,
6
+ isItemElement,
7
+ isSubmenuEntry,
8
+ itemToOverflowEntry,
9
+ resolveItemWidth,
10
+ } from './ds-bulk-actions.utils';
11
+
12
+ const StubItem: FunctionComponent<DsBulkActionsItemProps> = () => null;
13
+ StubItem.displayName = 'DsBulkActions.Item';
14
+
15
+ const createItem = (props: DsBulkActionsItemProps, key?: string) =>
16
+ createElement(StubItem, { key, ...props });
17
+
18
+ const clickItem: DsBulkActionsItemProps = { icon: 'edit', label: 'Edit', onClick: () => {} };
19
+
20
+ describe('resolveItemWidth', () => {
21
+ it('uses the fixed column when width is omitted', () => {
22
+ expect(resolveItemWidth()).toEqual({ mode: 'fixed' });
23
+ });
24
+
25
+ it('uses the fixed column when width is fixed', () => {
26
+ expect(resolveItemWidth('fixed')).toEqual({ mode: 'fixed' });
27
+ });
28
+
29
+ it('hugs the label when width is fit-content', () => {
30
+ expect(resolveItemWidth('fit-content')).toEqual({ mode: 'fit-content' });
31
+ });
32
+
33
+ it('sets a custom pixel width via CSS variable', () => {
34
+ expect(resolveItemWidth(120)).toEqual({
35
+ mode: 'custom',
36
+ buttonStyle: { '--ds-bulk-actions-item-width': '120px' },
37
+ });
38
+ });
39
+ });
40
+
41
+ describe('itemToOverflowEntry', () => {
42
+ it('maps a click item to a leaf entry that keeps onSelect', () => {
43
+ const onClick = vi.fn();
44
+ const item: DsBulkActionsItemProps = {
45
+ icon: 'edit',
46
+ label: 'Edit',
47
+ disabled: true,
48
+ onClick,
49
+ };
50
+
51
+ expect(itemToOverflowEntry(item, 2)).toEqual({
52
+ value: 'overflow-2',
53
+ label: 'Edit',
54
+ icon: 'edit',
55
+ disabled: true,
56
+ onSelect: onClick,
57
+ });
58
+ });
59
+
60
+ it('maps a menu item to a branch entry that keeps the nested menu', () => {
61
+ const menu = [{ value: 'mail', label: 'Mail', icon: 'mail' as const }];
62
+ const item: DsBulkActionsItemProps = {
63
+ icon: 'share',
64
+ label: 'Share',
65
+ menu,
66
+ };
67
+
68
+ expect(itemToOverflowEntry(item, 0)).toEqual({
69
+ value: 'overflow-0',
70
+ label: 'Share',
71
+ icon: 'share',
72
+ disabled: undefined,
73
+ menu,
74
+ });
75
+ });
76
+ });
77
+
78
+ describe('isItemElement', () => {
79
+ it('matches an element whose type carries the Item displayName', () => {
80
+ expect(isItemElement(createItem(clickItem))).toBe(true);
81
+ });
82
+
83
+ it('rejects other elements, strings, and nullish children', () => {
84
+ expect(isItemElement(createElement('div'))).toBe(false);
85
+ expect(isItemElement('Notify')).toBe(false);
86
+ expect(isItemElement(null)).toBe(false);
87
+ expect(isItemElement(undefined)).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe('getBulkActionItems', () => {
92
+ it('collects only Item elements and drops other children', () => {
93
+ const first = createItem(clickItem, 'a');
94
+ const second = createItem({ icon: 'delete_outline', label: 'Delete', onClick: () => {} }, 'b');
95
+
96
+ const items = getBulkActionItems([first, 'text', createElement('span'), second, null]);
97
+
98
+ expect(items).toEqual([first, second]);
99
+ });
100
+
101
+ it('flattens Item elements nested inside fragments', () => {
102
+ const first = createItem(clickItem, 'a');
103
+ const second = createItem({ icon: 'share', label: 'Share', onClick: () => {} }, 'b');
104
+ const fragment = createElement(Fragment, null, second);
105
+
106
+ expect(getBulkActionItems([first, fragment])).toEqual([first, second]);
107
+ });
108
+
109
+ it('returns an empty array when there are no Item children', () => {
110
+ expect(getBulkActionItems(createElement('div'))).toEqual([]);
111
+ });
112
+ });
113
+
114
+ describe('isSubmenuEntry', () => {
115
+ it('identifies a branch entry with a nested menu', () => {
116
+ expect(
117
+ isSubmenuEntry({
118
+ value: 'share',
119
+ label: 'Share',
120
+ menu: [{ value: 'mail', label: 'Mail' }],
121
+ }),
122
+ ).toBe(true);
123
+ });
124
+
125
+ it('rejects a leaf entry with onSelect', () => {
126
+ expect(
127
+ isSubmenuEntry({
128
+ value: 'edit',
129
+ label: 'Edit',
130
+ onSelect: () => undefined,
131
+ }),
132
+ ).toBe(false);
133
+ });
134
+ });
@@ -0,0 +1,85 @@
1
+ const FIT_EPSILON_PX = 0.5;
2
+ const MIN_VISIBLE_ACTIONS = 1;
3
+ const STACK_GAP_COUNT_WITH_LABEL = 3;
4
+ const STACK_GAP_COUNT_WITHOUT_LABEL = 2;
5
+
6
+ export interface BulkActionsOverflowMetrics {
7
+ containerWidth: number;
8
+ badgeWidth: number;
9
+ dismissWidth: number;
10
+ labelWidth: number;
11
+ itemWidths: number[];
12
+ moreWidth: number;
13
+ gap: number;
14
+ }
15
+
16
+ export interface BulkActionsOverflowState {
17
+ visibleCount: number;
18
+ showMore: boolean;
19
+ summaryLabelHidden: boolean;
20
+ }
21
+
22
+ const sum = (values: number[]): number => values.reduce((total, value) => total + value, 0);
23
+
24
+ const chromeWidth = ({
25
+ badgeWidth,
26
+ dismissWidth,
27
+ labelWidth,
28
+ gap,
29
+ hideLabel,
30
+ }: Pick<BulkActionsOverflowMetrics, 'badgeWidth' | 'dismissWidth' | 'labelWidth' | 'gap'> & {
31
+ hideLabel: boolean;
32
+ }): number => {
33
+ const gapCount = hideLabel ? STACK_GAP_COUNT_WITHOUT_LABEL : STACK_GAP_COUNT_WITH_LABEL;
34
+ const labelPart = hideLabel ? 0 : labelWidth;
35
+
36
+ return badgeWidth + dismissWidth + labelPart + gapCount * gap;
37
+ };
38
+
39
+ const fits = (usedWidth: number, containerWidth: number): boolean =>
40
+ usedWidth <= containerWidth + FIT_EPSILON_PX;
41
+
42
+ /**
43
+ * First-N visible / rightmost overflow, then hide the summary label as a last resort.
44
+ * Dismiss and the count badge are never collapsed.
45
+ */
46
+ export const computeBulkActionsOverflow = ({
47
+ containerWidth,
48
+ badgeWidth,
49
+ dismissWidth,
50
+ labelWidth,
51
+ itemWidths,
52
+ moreWidth,
53
+ gap,
54
+ }: BulkActionsOverflowMetrics): BulkActionsOverflowState => {
55
+ const itemCount = itemWidths.length;
56
+
57
+ if (itemCount === 0) {
58
+ return { visibleCount: 0, showMore: false, summaryLabelHidden: false };
59
+ }
60
+
61
+ const chrome = {
62
+ badgeWidth,
63
+ dismissWidth,
64
+ labelWidth,
65
+ gap,
66
+ };
67
+
68
+ if (fits(chromeWidth({ ...chrome, hideLabel: false }) + sum(itemWidths), containerWidth)) {
69
+ return { visibleCount: itemCount, showMore: false, summaryLabelHidden: false };
70
+ }
71
+
72
+ for (let visibleCount = itemCount - 1; visibleCount >= MIN_VISIBLE_ACTIONS; visibleCount--) {
73
+ const actionsWidth = sum(itemWidths.slice(0, visibleCount)) + moreWidth;
74
+
75
+ if (fits(chromeWidth({ ...chrome, hideLabel: false }) + actionsWidth, containerWidth)) {
76
+ return { visibleCount, showMore: true, summaryLabelHidden: false };
77
+ }
78
+ }
79
+
80
+ return {
81
+ visibleCount: MIN_VISIBLE_ACTIONS,
82
+ showMore: itemCount > MIN_VISIBLE_ACTIONS,
83
+ summaryLabelHidden: true,
84
+ };
85
+ };
@@ -0,0 +1,153 @@
1
+ import { type CSSProperties, type RefObject, useLayoutEffect, useState } from 'react';
2
+ import { computeBulkActionsOverflow, type BulkActionsOverflowState } from './compute-bulk-actions-overflow';
3
+
4
+ const allVisible = (itemCount: number): BulkActionsOverflowState => ({
5
+ visibleCount: itemCount,
6
+ showMore: false,
7
+ summaryLabelHidden: false,
8
+ });
9
+
10
+ const sameOverflowState = (prev: BulkActionsOverflowState, next: BulkActionsOverflowState): boolean =>
11
+ prev.visibleCount === next.visibleCount &&
12
+ prev.showMore === next.showMore &&
13
+ prev.summaryLabelHidden === next.summaryLabelHidden;
14
+
15
+ const isConstrainedCssLength = (value: CSSProperties['width']): boolean => {
16
+ if (value === undefined) {
17
+ return false;
18
+ }
19
+
20
+ return (
21
+ value !== 'auto' &&
22
+ value !== 'fit-content' &&
23
+ value !== 'max-content' &&
24
+ value !== 'min-content' &&
25
+ value !== 'none'
26
+ );
27
+ };
28
+
29
+ // A bare `width` set via className cannot be detected here: the default
30
+ // `width: fit-content` also resolves to a pixel value, so computed `width` is
31
+ // ambiguous. Consumers must bound the toolbar via `style.width`, `style.maxWidth`,
32
+ // or a CSS `max-width` (detected below) to enable overflow.
33
+ const hasExplicitWidthConstraint = (element: HTMLElement, style?: CSSProperties): boolean => {
34
+ if (isConstrainedCssLength(style?.width) || isConstrainedCssLength(style?.maxWidth)) {
35
+ return true;
36
+ }
37
+
38
+ return getComputedStyle(element).maxWidth !== 'none';
39
+ };
40
+
41
+ interface UseBulkActionsOverflowOptions {
42
+ active: boolean;
43
+ itemCount: number;
44
+ style?: CSSProperties;
45
+ containerRef: RefObject<HTMLElement | null>;
46
+ stackRef: RefObject<HTMLElement | null>;
47
+ badgeRef: RefObject<HTMLElement | null>;
48
+ dismissRef: RefObject<HTMLElement | null>;
49
+ measureRef: RefObject<HTMLElement | null>;
50
+ }
51
+
52
+ export const useBulkActionsOverflow = ({
53
+ active,
54
+ itemCount,
55
+ style,
56
+ containerRef,
57
+ stackRef,
58
+ badgeRef,
59
+ dismissRef,
60
+ measureRef,
61
+ }: UseBulkActionsOverflowOptions): BulkActionsOverflowState => {
62
+ const [state, setState] = useState<BulkActionsOverflowState>(() => allVisible(itemCount));
63
+ const styleWidth = style?.width;
64
+ const styleMaxWidth = style?.maxWidth;
65
+
66
+ useLayoutEffect(() => {
67
+ if (!active) {
68
+ return;
69
+ }
70
+
71
+ const container = containerRef.current;
72
+
73
+ if (!container) {
74
+ return;
75
+ }
76
+
77
+ const constraintStyle = { width: styleWidth, maxWidth: styleMaxWidth };
78
+ let observerRafId = 0;
79
+
80
+ const measureLayout = () => {
81
+ if (!hasExplicitWidthConstraint(container, constraintStyle) || itemCount === 0) {
82
+ setState((prev) => {
83
+ const next = allVisible(itemCount);
84
+ return sameOverflowState(prev, next) ? prev : next;
85
+ });
86
+ return;
87
+ }
88
+
89
+ const stack = stackRef.current;
90
+ const badge = badgeRef.current;
91
+ const dismiss = dismissRef.current;
92
+ const measure = measureRef.current;
93
+
94
+ if (!stack || !badge || !dismiss || !measure) {
95
+ return;
96
+ }
97
+
98
+ const itemWidths = Array.from(
99
+ measure.querySelectorAll<HTMLElement>('[data-overflow-measure="item"]'),
100
+ ).map((el) => el.getBoundingClientRect().width);
101
+ const moreEl = measure.querySelector<HTMLElement>('[data-overflow-measure="more"]');
102
+ const labelEl = measure.querySelector<HTMLElement>('[data-overflow-measure="label"]');
103
+
104
+ if (!moreEl || !labelEl || itemWidths.length !== itemCount) {
105
+ return;
106
+ }
107
+
108
+ const gap = parseFloat(getComputedStyle(stack).columnGap || getComputedStyle(stack).gap) || 0;
109
+
110
+ const next = computeBulkActionsOverflow({
111
+ containerWidth: container.getBoundingClientRect().width,
112
+ badgeWidth: badge.getBoundingClientRect().width,
113
+ dismissWidth: dismiss.getBoundingClientRect().width,
114
+ labelWidth: labelEl.getBoundingClientRect().width,
115
+ itemWidths,
116
+ moreWidth: moreEl.getBoundingClientRect().width,
117
+ gap,
118
+ });
119
+
120
+ setState((prev) => (sameOverflowState(prev, next) ? prev : next));
121
+ };
122
+
123
+ measureLayout();
124
+
125
+ const observer = new ResizeObserver(() => {
126
+ cancelAnimationFrame(observerRafId);
127
+ observerRafId = requestAnimationFrame(measureLayout);
128
+ });
129
+
130
+ observer.observe(container);
131
+
132
+ if (measureRef.current) {
133
+ observer.observe(measureRef.current);
134
+ }
135
+
136
+ return () => {
137
+ cancelAnimationFrame(observerRafId);
138
+ observer.disconnect();
139
+ };
140
+ }, [
141
+ active,
142
+ badgeRef,
143
+ containerRef,
144
+ dismissRef,
145
+ itemCount,
146
+ measureRef,
147
+ stackRef,
148
+ styleMaxWidth,
149
+ styleWidth,
150
+ ]);
151
+
152
+ return state;
153
+ };
@@ -0,0 +1,18 @@
1
+ export { default as DsBulkActions } from './ds-bulk-actions';
2
+ export type {
3
+ DsBulkActionsItemProps,
4
+ DsBulkActionsItemWidth,
5
+ DsBulkActionsLocale,
6
+ DsBulkActionsMenuEntry,
7
+ DsBulkActionsProps,
8
+ BulkActionsItemWidthMode,
9
+ BulkActionsMenuItemVariant,
10
+ BulkActionsMenuPlacement,
11
+ BulkActionsPlacement,
12
+ } from './ds-bulk-actions.types';
13
+ export {
14
+ bulkActionsItemWidthModes,
15
+ bulkActionsMenuItemVariants,
16
+ bulkActionsMenuPlacements,
17
+ bulkActionsPlacements,
18
+ } from './ds-bulk-actions.types';
@@ -164,7 +164,7 @@ $focus-ring-width: 2px;
164
164
 
165
165
  .root[data-color='default'][data-variant='secondary'] {
166
166
  background-color: var(--background-action-secondary);
167
- color: var(--font-main);
167
+ color: var(--font-action);
168
168
  border-color: var(--border);
169
169
 
170
170
  &:hover:not(:disabled):not(:active):not([data-selected='true']) {
@@ -202,7 +202,7 @@ $focus-ring-width: 2px;
202
202
 
203
203
  .root[data-color='default'][data-variant='tertiary'] {
204
204
  background-color: transparent;
205
- color: var(--font-main);
205
+ color: var(--font-action);
206
206
  border-color: transparent;
207
207
 
208
208
  &:hover:not(:disabled):not(:active):not([data-selected='true']) {
@@ -32,7 +32,7 @@ const contentHeader =
32
32
 
33
33
  const contentBody =
34
34
  type === 'empty'
35
- ? '{/* EmptyStateCard + DsButtonV3 (no data / clear filters) */}'
35
+ ? '{/* DsTable + DsEmptyState (no data / no matches) */}'
36
36
  : '{/* DsTable with data rows */}';
37
37
 
38
38
  export default {