@drivenets/design-system 0.1.0 → 0.2.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 (170) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/components/ds-avatar/ds-avatar.cjs +98 -0
  3. package/dist/components/ds-avatar/ds-avatar.d.cts +12 -0
  4. package/dist/components/ds-avatar/ds-avatar.d.cts.map +1 -0
  5. package/dist/components/ds-avatar/ds-avatar.d.ts +12 -0
  6. package/dist/components/ds-avatar/ds-avatar.d.ts.map +1 -0
  7. package/dist/components/ds-avatar/ds-avatar.js +97 -0
  8. package/dist/components/ds-avatar/ds-avatar.js.map +1 -0
  9. package/dist/components/ds-avatar/ds-avatar.module.cjs +17 -0
  10. package/dist/components/ds-avatar/ds-avatar.module.js +17 -0
  11. package/dist/components/ds-avatar/ds-avatar.module.js.map +1 -0
  12. package/dist/components/ds-avatar/ds-avatar.types.d.cts +44 -0
  13. package/dist/components/ds-avatar/ds-avatar.types.d.cts.map +1 -0
  14. package/dist/components/ds-avatar/ds-avatar.types.d.ts +44 -0
  15. package/dist/components/ds-avatar/ds-avatar.types.d.ts.map +1 -0
  16. package/dist/components/ds-avatar/index.cjs +1 -0
  17. package/dist/components/ds-avatar/index.d.ts +2 -0
  18. package/dist/components/ds-avatar/index.js +1 -0
  19. package/dist/components/ds-avatar-group/ds-avatar-group.cjs +93 -0
  20. package/dist/components/ds-avatar-group/ds-avatar-group.d.cts +12 -0
  21. package/dist/components/ds-avatar-group/ds-avatar-group.d.cts.map +1 -0
  22. package/dist/components/ds-avatar-group/ds-avatar-group.d.ts +12 -0
  23. package/dist/components/ds-avatar-group/ds-avatar-group.d.ts.map +1 -0
  24. package/dist/components/ds-avatar-group/ds-avatar-group.js +92 -0
  25. package/dist/components/ds-avatar-group/ds-avatar-group.js.map +1 -0
  26. package/dist/components/ds-avatar-group/ds-avatar-group.module.cjs +19 -0
  27. package/dist/components/ds-avatar-group/ds-avatar-group.module.js +19 -0
  28. package/dist/components/ds-avatar-group/ds-avatar-group.module.js.map +1 -0
  29. package/dist/components/ds-avatar-group/ds-avatar-group.types.d.cts +31 -0
  30. package/dist/components/ds-avatar-group/ds-avatar-group.types.d.cts.map +1 -0
  31. package/dist/components/ds-avatar-group/ds-avatar-group.types.d.ts +32 -0
  32. package/dist/components/ds-avatar-group/ds-avatar-group.types.d.ts.map +1 -0
  33. package/dist/components/ds-avatar-group/index.cjs +1 -0
  34. package/dist/components/ds-avatar-group/index.d.ts +2 -0
  35. package/dist/components/ds-avatar-group/index.js +1 -0
  36. package/dist/components/ds-button/ds-button.d.ts +2 -2
  37. package/dist/components/ds-button/ds-button.d.ts.map +1 -1
  38. package/dist/components/ds-chip/ds-chip.cjs +2 -1
  39. package/dist/components/ds-chip/ds-chip.d.cts +2 -1
  40. package/dist/components/ds-chip/ds-chip.d.cts.map +1 -1
  41. package/dist/components/ds-chip/ds-chip.d.ts +2 -1
  42. package/dist/components/ds-chip/ds-chip.d.ts.map +1 -1
  43. package/dist/components/ds-chip/ds-chip.js +2 -1
  44. package/dist/components/ds-chip/ds-chip.js.map +1 -1
  45. package/dist/components/ds-chip/ds-chip.types.d.cts +13 -0
  46. package/dist/components/ds-chip/ds-chip.types.d.cts.map +1 -1
  47. package/dist/components/ds-chip/ds-chip.types.d.ts +13 -0
  48. package/dist/components/ds-chip/ds-chip.types.d.ts.map +1 -1
  49. package/dist/components/ds-chip-group/ds-chip-group.cjs +2 -2
  50. package/dist/components/ds-chip-group/ds-chip-group.d.cts +2 -2
  51. package/dist/components/ds-chip-group/ds-chip-group.d.ts +2 -2
  52. package/dist/components/ds-chip-group/ds-chip-group.js +2 -2
  53. package/dist/components/ds-chip-group/ds-chip-group.js.map +1 -1
  54. package/dist/components/ds-chip-group/ds-chip-group.types.d.cts +9 -0
  55. package/dist/components/ds-chip-group/ds-chip-group.types.d.cts.map +1 -1
  56. package/dist/components/ds-chip-group/ds-chip-group.types.d.ts +9 -0
  57. package/dist/components/ds-chip-group/ds-chip-group.types.d.ts.map +1 -1
  58. package/dist/components/ds-confirmation/ds-confirmation.d.cts +8 -8
  59. package/dist/components/ds-confirmation/ds-confirmation.d.ts +8 -8
  60. package/dist/components/ds-date-input/ds-date-input.d.cts +2 -2
  61. package/dist/components/ds-date-input/ds-date-input.d.cts.map +1 -1
  62. package/dist/components/ds-date-input/ds-date-input.d.ts +2 -2
  63. package/dist/components/ds-date-input/ds-date-input.js +2 -2
  64. package/dist/components/ds-date-input/ds-date-input.js.map +1 -1
  65. package/dist/components/ds-drawer/ds-drawer.d.cts +9 -9
  66. package/dist/components/ds-drawer/ds-drawer.d.ts +9 -9
  67. package/dist/components/ds-drawer/ds-drawer.js +2 -2
  68. package/dist/components/ds-dropdown-menu/ds-dropdown-menu.js +3 -3
  69. package/dist/components/ds-dropdown-menu/ds-dropdown-menu.js.map +1 -1
  70. package/dist/components/ds-expandable-text-input/ds-expandable-text-input.d.cts +2 -2
  71. package/dist/components/ds-expandable-text-input/ds-expandable-text-input.d.ts +2 -2
  72. package/dist/components/ds-file-upload/components/file-upload-item/file-upload-item.cjs +2 -2
  73. package/dist/components/ds-file-upload/components/file-upload-item/file-upload-item.js +2 -2
  74. package/dist/components/ds-form-control/ds-form-control.d.cts +8 -8
  75. package/dist/components/ds-modal/ds-modal.d.cts +8 -8
  76. package/dist/components/ds-panel/ds-panel.d.cts +2 -2
  77. package/dist/components/ds-panel/ds-panel.d.ts +2 -2
  78. package/dist/components/ds-select/ds-select.d.cts +2 -2
  79. package/dist/components/ds-select/ds-select.d.ts +2 -2
  80. package/dist/components/ds-smart-tabs/ds-smart-tabs.d.cts +2 -2
  81. package/dist/components/ds-smart-tabs/ds-smart-tabs.d.ts +2 -2
  82. package/dist/components/ds-smart-tabs/ds-smart-tabs.d.ts.map +1 -1
  83. package/dist/components/ds-stepper/components/ds-step-content.d.cts +2 -2
  84. package/dist/components/ds-stepper/components/ds-step-content.d.ts +2 -2
  85. package/dist/components/ds-stepper/components/ds-step-next-button.d.cts +2 -2
  86. package/dist/components/ds-stepper/components/ds-step-next-button.d.ts +2 -2
  87. package/dist/components/ds-stepper/components/ds-step.d.cts +2 -2
  88. package/dist/components/ds-stepper/components/ds-step.d.ts +2 -2
  89. package/dist/components/ds-stepper/ds-stepper.d.cts +2 -2
  90. package/dist/components/ds-stepper/ds-stepper.d.ts +2 -2
  91. package/dist/components/ds-table/ds-table.d.cts +2 -2
  92. package/dist/components/ds-table/ds-table.d.ts +2 -2
  93. package/dist/components/ds-tag/ds-tag.cjs +254 -0
  94. package/dist/components/ds-tag/ds-tag.d.cts +25 -0
  95. package/dist/components/ds-tag/ds-tag.d.cts.map +1 -0
  96. package/dist/components/ds-tag/ds-tag.d.ts +25 -0
  97. package/dist/components/ds-tag/ds-tag.d.ts.map +1 -0
  98. package/dist/components/ds-tag/ds-tag.js +253 -0
  99. package/dist/components/ds-tag/ds-tag.js.map +1 -0
  100. package/dist/components/ds-tag/ds-tag.module.cjs +17 -0
  101. package/dist/components/ds-tag/ds-tag.module.js +17 -0
  102. package/dist/components/ds-tag/ds-tag.module.js.map +1 -0
  103. package/dist/components/ds-tag/ds-tag.types.d.cts +68 -0
  104. package/dist/components/ds-tag/ds-tag.types.d.cts.map +1 -0
  105. package/dist/components/ds-tag/ds-tag.types.d.ts +68 -0
  106. package/dist/components/ds-tag/ds-tag.types.d.ts.map +1 -0
  107. package/dist/components/ds-tag/index.cjs +1 -0
  108. package/dist/components/ds-tag/index.d.ts +2 -0
  109. package/dist/components/ds-tag/index.js +1 -0
  110. package/dist/components/ds-tag-filter/ds-tag-filter.cjs +325 -0
  111. package/dist/components/ds-tag-filter/ds-tag-filter.d.cts +24 -0
  112. package/dist/components/ds-tag-filter/ds-tag-filter.d.cts.map +1 -0
  113. package/dist/components/ds-tag-filter/ds-tag-filter.d.ts +24 -0
  114. package/dist/components/ds-tag-filter/ds-tag-filter.d.ts.map +1 -0
  115. package/dist/components/ds-tag-filter/ds-tag-filter.js +324 -0
  116. package/dist/components/ds-tag-filter/ds-tag-filter.js.map +1 -0
  117. package/dist/components/ds-tag-filter/ds-tag-filter.module.cjs +17 -0
  118. package/dist/components/ds-tag-filter/ds-tag-filter.module.js +17 -0
  119. package/dist/components/ds-tag-filter/ds-tag-filter.module.js.map +1 -0
  120. package/dist/components/ds-tag-filter/ds-tag-filter.types.d.cts +71 -0
  121. package/dist/components/ds-tag-filter/ds-tag-filter.types.d.cts.map +1 -0
  122. package/dist/components/ds-tag-filter/ds-tag-filter.types.d.ts +72 -0
  123. package/dist/components/ds-tag-filter/ds-tag-filter.types.d.ts.map +1 -0
  124. package/dist/components/ds-tag-filter/hooks/use-tag-overflow-calculation.cjs +137 -0
  125. package/dist/components/ds-tag-filter/hooks/use-tag-overflow-calculation.js +137 -0
  126. package/dist/components/ds-tag-filter/hooks/use-tag-overflow-calculation.js.map +1 -0
  127. package/dist/components/ds-tag-filter/index.cjs +1 -0
  128. package/dist/components/ds-tag-filter/index.d.ts +2 -0
  129. package/dist/components/ds-tag-filter/index.js +1 -0
  130. package/dist/components/ds-tag-filter/utils.cjs +54 -0
  131. package/dist/components/ds-tag-filter/utils.js +52 -0
  132. package/dist/components/ds-tag-filter/utils.js.map +1 -0
  133. package/dist/components/ds-text-input/ds-text-input.js +2 -2
  134. package/dist/components/ds-toggle/ds-toggle.d.cts +2 -2
  135. package/dist/components/ds-toggle/ds-toggle.d.ts +2 -2
  136. package/dist/components/ds-vertical-tabs/ds-vertical-tabs.d.cts +5 -5
  137. package/dist/components/ds-vertical-tabs/ds-vertical-tabs.d.ts +5 -5
  138. package/dist/index.cjs +14 -2
  139. package/dist/index.d.cts +9 -1
  140. package/dist/index.d.ts +13 -1
  141. package/dist/index.js +11 -3
  142. package/dist/index.min.css +1 -1
  143. package/package.json +6 -2
  144. package/src/components/ds-avatar/_mixins.scss +45 -0
  145. package/src/components/ds-avatar/ds-avatar.module.scss +52 -0
  146. package/src/components/ds-avatar/ds-avatar.tsx +42 -0
  147. package/src/components/ds-avatar/ds-avatar.types.ts +41 -0
  148. package/src/components/ds-avatar/index.ts +2 -0
  149. package/src/components/ds-avatar-group/ds-avatar-group.module.scss +70 -0
  150. package/src/components/ds-avatar-group/ds-avatar-group.tsx +51 -0
  151. package/src/components/ds-avatar-group/ds-avatar-group.types.ts +27 -0
  152. package/src/components/ds-avatar-group/index.ts +2 -0
  153. package/src/components/ds-chip/ds-chip.tsx +2 -1
  154. package/src/components/ds-chip/ds-chip.types.tsx +13 -0
  155. package/src/components/ds-chip/index.tsx +8 -0
  156. package/src/components/ds-chip-group/ds-chip-group.tsx +2 -2
  157. package/src/components/ds-chip-group/ds-chip-group.types.ts +8 -0
  158. package/src/components/ds-chip-group/index.ts +1 -0
  159. package/src/components/ds-tag/ds-tag.module.scss +112 -0
  160. package/src/components/ds-tag/ds-tag.tsx +124 -0
  161. package/src/components/ds-tag/ds-tag.types.tsx +67 -0
  162. package/src/components/ds-tag/index.tsx +2 -0
  163. package/src/components/ds-tag-filter/ds-tag-filter.module.scss +72 -0
  164. package/src/components/ds-tag-filter/ds-tag-filter.tsx +178 -0
  165. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +68 -0
  166. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +135 -0
  167. package/src/components/ds-tag-filter/index.ts +2 -0
  168. package/src/components/ds-tag-filter/utils.test.ts +401 -0
  169. package/src/components/ds-tag-filter/utils.ts +65 -0
  170. package/src/index.ts +4 -0
@@ -0,0 +1,401 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { fitTagsInRow, getContainerAvailableWidth, getElementMeasurements } from './utils';
3
+
4
+ /**
5
+ * Tests here heavily test implementation details.
6
+ * Required to ensure proper calculations.
7
+ */
8
+
9
+ describe('Tag Filter Utils', () => {
10
+ describe('fitTagsInRow', () => {
11
+ it('should return count 0 for empty tag array', () => {
12
+ const result = fitTagsInRow([], 100, 8);
13
+
14
+ expect(result.count).toBe(0);
15
+ expect(result.usedWidth).toBe(0);
16
+ });
17
+
18
+ it('should fit all tags when they all fit in available width', () => {
19
+ const tagWidths = [50, 60, 70]; // total: 180 + 2 gaps (16) = 196
20
+ const availableWidth = 200;
21
+ const gap = 8;
22
+
23
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
24
+
25
+ expect(result.count).toBe(3);
26
+ expect(result.usedWidth).toBe(196); // 50 + 8 + 60 + 8 + 70 = 196
27
+ });
28
+
29
+ it('should return partial count when not all tags fit', () => {
30
+ const tagWidths = [50, 60, 70, 80];
31
+ const availableWidth = 150;
32
+ const gap = 8;
33
+
34
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
35
+
36
+ // 50 = 50, 50 + 8 + 60 = 118, 118 + 8 + 70 = 196 (exceeds 150)
37
+ expect(result.count).toBe(2);
38
+ expect(result.usedWidth).toBe(118); // 50 + 8 + 60 = 118
39
+ });
40
+
41
+ it('should not add gap before the first tag', () => {
42
+ const tagWidths = [100];
43
+ const availableWidth = 100;
44
+ const gap = 8;
45
+
46
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
47
+
48
+ expect(result.count).toBe(1);
49
+ expect(result.usedWidth).toBe(100); // No gap added for first tag
50
+ });
51
+
52
+ it('should handle exact fit edge case', () => {
53
+ const tagWidths = [40, 52]; // 40 + 8 + 52 = 100
54
+ const availableWidth = 100;
55
+ const gap = 8;
56
+
57
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
58
+
59
+ expect(result.count).toBe(2);
60
+ expect(result.usedWidth).toBe(100);
61
+ });
62
+
63
+ it('should return 0 when first tag exceeds available width', () => {
64
+ const tagWidths = [150, 50, 60];
65
+ const availableWidth = 100;
66
+ const gap = 8;
67
+
68
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
69
+
70
+ expect(result.count).toBe(0);
71
+ expect(result.usedWidth).toBe(0);
72
+ });
73
+
74
+ it('should handle zero gap correctly', () => {
75
+ const tagWidths = [30, 30, 30];
76
+ const availableWidth = 90;
77
+ const gap = 0;
78
+
79
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
80
+
81
+ expect(result.count).toBe(3);
82
+ expect(result.usedWidth).toBe(90);
83
+ });
84
+
85
+ it('should account for gap in width calculation', () => {
86
+ const tagWidths = [45, 45]; // 45 + 8 + 45 = 98
87
+ const availableWidth = 95;
88
+ const gap = 8;
89
+
90
+ const result = fitTagsInRow(tagWidths, availableWidth, gap);
91
+
92
+ // First tag: 45, second would be 45 + 8 = 53, total = 98 > 95
93
+ expect(result.count).toBe(1);
94
+ expect(result.usedWidth).toBe(45);
95
+ });
96
+ });
97
+
98
+ describe('getContainerAvailableWidth', () => {
99
+ let mockContainer: HTMLDivElement;
100
+
101
+ beforeEach(() => {
102
+ mockContainer = document.createElement('div');
103
+ });
104
+
105
+ afterEach(() => {
106
+ vi.restoreAllMocks();
107
+ });
108
+
109
+ it('should calculate available width without padding', () => {
110
+ vi.spyOn(mockContainer, 'getBoundingClientRect').mockReturnValue({
111
+ width: 500,
112
+ height: 100,
113
+ top: 0,
114
+ left: 0,
115
+ bottom: 100,
116
+ right: 500,
117
+ x: 0,
118
+ y: 0,
119
+ toJSON: () => ({}),
120
+ });
121
+
122
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
123
+ paddingLeft: '0px',
124
+ paddingRight: '0px',
125
+ } as CSSStyleDeclaration);
126
+
127
+ const result = getContainerAvailableWidth(mockContainer);
128
+
129
+ expect(result).toBe(500);
130
+ });
131
+
132
+ it('should subtract padding from container width', () => {
133
+ vi.spyOn(mockContainer, 'getBoundingClientRect').mockReturnValue({
134
+ width: 500,
135
+ height: 100,
136
+ top: 0,
137
+ left: 0,
138
+ bottom: 100,
139
+ right: 500,
140
+ x: 0,
141
+ y: 0,
142
+ toJSON: () => ({}),
143
+ });
144
+
145
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
146
+ paddingLeft: '20px',
147
+ paddingRight: '30px',
148
+ } as CSSStyleDeclaration);
149
+
150
+ const result = getContainerAvailableWidth(mockContainer);
151
+
152
+ expect(result).toBe(450); // 500 - 20 - 30 = 450
153
+ });
154
+
155
+ it('should handle string padding values with decimals', () => {
156
+ vi.spyOn(mockContainer, 'getBoundingClientRect').mockReturnValue({
157
+ width: 500,
158
+ height: 100,
159
+ top: 0,
160
+ left: 0,
161
+ bottom: 100,
162
+ right: 500,
163
+ x: 0,
164
+ y: 0,
165
+ toJSON: () => ({}),
166
+ });
167
+
168
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
169
+ paddingLeft: '15.5px',
170
+ paddingRight: '24.5px',
171
+ } as CSSStyleDeclaration);
172
+
173
+ const result = getContainerAvailableWidth(mockContainer);
174
+
175
+ expect(result).toBe(460); // 500 - 15.5 - 24.5 = 460
176
+ });
177
+
178
+ it('should default to 0 for invalid padding values', () => {
179
+ vi.spyOn(mockContainer, 'getBoundingClientRect').mockReturnValue({
180
+ width: 500,
181
+ height: 100,
182
+ top: 0,
183
+ left: 0,
184
+ bottom: 100,
185
+ right: 500,
186
+ x: 0,
187
+ y: 0,
188
+ toJSON: () => ({}),
189
+ });
190
+
191
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
192
+ paddingLeft: '',
193
+ paddingRight: '',
194
+ } as CSSStyleDeclaration);
195
+
196
+ const result = getContainerAvailableWidth(mockContainer);
197
+
198
+ expect(result).toBe(500); // No padding subtracted
199
+ });
200
+ });
201
+
202
+ describe('getElementMeasurements', () => {
203
+ let mockContainer: HTMLDivElement;
204
+
205
+ beforeEach(() => {
206
+ mockContainer = document.createElement('div');
207
+ document.body.appendChild(mockContainer);
208
+ });
209
+
210
+ afterEach(() => {
211
+ document.body.removeChild(mockContainer);
212
+ vi.restoreAllMocks();
213
+ });
214
+
215
+ it('should measure all elements with default gap', () => {
216
+ // Create mock elements
217
+ const tag1 = document.createElement('div');
218
+ tag1.setAttribute('data-measure-tag', '');
219
+ const tag2 = document.createElement('div');
220
+ tag2.setAttribute('data-measure-tag', '');
221
+ const label = document.createElement('div');
222
+ label.setAttribute('data-measure-label', '');
223
+ const clearButton = document.createElement('div');
224
+ clearButton.setAttribute('data-measure-clear', '');
225
+ const expandTag = document.createElement('div');
226
+ expandTag.setAttribute('data-measure-expand', '');
227
+
228
+ mockContainer.appendChild(tag1);
229
+ mockContainer.appendChild(tag2);
230
+ mockContainer.appendChild(label);
231
+ mockContainer.appendChild(clearButton);
232
+ mockContainer.appendChild(expandTag);
233
+
234
+ // Mock getBoundingClientRect for each element
235
+ vi.spyOn(tag1, 'getBoundingClientRect').mockReturnValue({
236
+ width: 80,
237
+ height: 30,
238
+ top: 0,
239
+ left: 0,
240
+ bottom: 30,
241
+ right: 80,
242
+ x: 0,
243
+ y: 0,
244
+ toJSON: () => ({}),
245
+ });
246
+
247
+ vi.spyOn(tag2, 'getBoundingClientRect').mockReturnValue({
248
+ width: 90,
249
+ height: 30,
250
+ top: 0,
251
+ left: 0,
252
+ bottom: 30,
253
+ right: 90,
254
+ x: 0,
255
+ y: 0,
256
+ toJSON: () => ({}),
257
+ });
258
+
259
+ vi.spyOn(label, 'getBoundingClientRect').mockReturnValue({
260
+ width: 60,
261
+ height: 30,
262
+ top: 0,
263
+ left: 0,
264
+ bottom: 30,
265
+ right: 60,
266
+ x: 0,
267
+ y: 0,
268
+ toJSON: () => ({}),
269
+ });
270
+
271
+ vi.spyOn(clearButton, 'getBoundingClientRect').mockReturnValue({
272
+ width: 40,
273
+ height: 30,
274
+ top: 0,
275
+ left: 0,
276
+ bottom: 30,
277
+ right: 40,
278
+ x: 0,
279
+ y: 0,
280
+ toJSON: () => ({}),
281
+ });
282
+
283
+ vi.spyOn(expandTag, 'getBoundingClientRect').mockReturnValue({
284
+ width: 100,
285
+ height: 30,
286
+ top: 0,
287
+ left: 0,
288
+ bottom: 30,
289
+ right: 100,
290
+ x: 0,
291
+ y: 0,
292
+ toJSON: () => ({}),
293
+ });
294
+
295
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
296
+ gap: '8px',
297
+ } as CSSStyleDeclaration);
298
+
299
+ const result = getElementMeasurements(mockContainer);
300
+
301
+ expect(result.tagWidths).toEqual([80, 90]);
302
+ expect(result.labelWidth).toBe(68); // 60 + 8
303
+ expect(result.clearButtonWidth).toBe(48); // 40 + 8
304
+ expect(result.expandTagWidth).toBe(108); // 100 + 8
305
+ expect(result.gap).toBe(8);
306
+ });
307
+
308
+ it('should handle missing optional elements', () => {
309
+ const tag1 = document.createElement('div');
310
+ tag1.setAttribute('data-measure-tag', '');
311
+ mockContainer.appendChild(tag1);
312
+
313
+ vi.spyOn(tag1, 'getBoundingClientRect').mockReturnValue({
314
+ width: 80,
315
+ height: 30,
316
+ top: 0,
317
+ left: 0,
318
+ bottom: 30,
319
+ right: 80,
320
+ x: 0,
321
+ y: 0,
322
+ toJSON: () => ({}),
323
+ });
324
+
325
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
326
+ gap: '12px',
327
+ } as CSSStyleDeclaration);
328
+
329
+ const result = getElementMeasurements(mockContainer);
330
+
331
+ expect(result.tagWidths).toEqual([80]);
332
+ expect(result.labelWidth).toBe(0); // Missing label
333
+ expect(result.clearButtonWidth).toBe(0); // Missing clear button
334
+ expect(result.expandTagWidth).toBe(100); // Fallback width
335
+ expect(result.gap).toBe(12);
336
+ });
337
+
338
+ it('should use fallback gap when gap is invalid', () => {
339
+ const tag1 = document.createElement('div');
340
+ tag1.setAttribute('data-measure-tag', '');
341
+ mockContainer.appendChild(tag1);
342
+
343
+ vi.spyOn(tag1, 'getBoundingClientRect').mockReturnValue({
344
+ width: 80,
345
+ height: 30,
346
+ top: 0,
347
+ left: 0,
348
+ bottom: 30,
349
+ right: 80,
350
+ x: 0,
351
+ y: 0,
352
+ toJSON: () => ({}),
353
+ });
354
+
355
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
356
+ gap: '',
357
+ } as CSSStyleDeclaration);
358
+
359
+ const result = getElementMeasurements(mockContainer);
360
+
361
+ expect(result.gap).toBe(8); // Fallback to 8
362
+ });
363
+
364
+ it('should handle no tags present', () => {
365
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
366
+ gap: '10px',
367
+ } as CSSStyleDeclaration);
368
+
369
+ const result = getElementMeasurements(mockContainer);
370
+
371
+ expect(result.tagWidths).toEqual([]);
372
+ expect(result.gap).toBe(10);
373
+ });
374
+
375
+ it('should use fallback for expand tag when width is 0', () => {
376
+ const expandTag = document.createElement('div');
377
+ expandTag.setAttribute('data-measure-expand', '');
378
+ mockContainer.appendChild(expandTag);
379
+
380
+ vi.spyOn(expandTag, 'getBoundingClientRect').mockReturnValue({
381
+ width: 0,
382
+ height: 30,
383
+ top: 0,
384
+ left: 0,
385
+ bottom: 30,
386
+ right: 0,
387
+ x: 0,
388
+ y: 0,
389
+ toJSON: () => ({}),
390
+ });
391
+
392
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({
393
+ gap: '8px',
394
+ } as CSSStyleDeclaration);
395
+
396
+ const result = getElementMeasurements(mockContainer);
397
+
398
+ expect(result.expandTagWidth).toBe(8); // 0 + 8 (not using fallback since element exists)
399
+ });
400
+ });
401
+ });
@@ -0,0 +1,65 @@
1
+ export interface FitTagsResult {
2
+ count: number;
3
+ usedWidth: number;
4
+ }
5
+
6
+ export interface ElementMeasurements {
7
+ tagWidths: number[];
8
+ labelWidth: number;
9
+ clearButtonWidth: number;
10
+ expandTagWidth: number;
11
+ gap: number;
12
+ }
13
+
14
+ /**
15
+ * Calculate how many tags fit within the available width.
16
+ * Pure function for easy testing.
17
+ */
18
+ export function fitTagsInRow(tagWidths: number[], availableWidth: number, gap: number): FitTagsResult {
19
+ let usedWidth = 0;
20
+ let count = 0;
21
+
22
+ for (const tagWidth of tagWidths) {
23
+ const widthWithGap = tagWidth + (count > 0 ? gap : 0);
24
+ if (usedWidth + widthWithGap <= availableWidth) {
25
+ usedWidth += widthWithGap;
26
+ count++;
27
+ } else {
28
+ break;
29
+ }
30
+ }
31
+
32
+ return { count, usedWidth };
33
+ }
34
+
35
+ /**
36
+ * Calculate the available width inside a container, accounting for padding.
37
+ */
38
+ export function getContainerAvailableWidth(container: HTMLDivElement): number {
39
+ const containerRect = container.getBoundingClientRect();
40
+ const containerStyle = getComputedStyle(container);
41
+ const paddingLeft = parseFloat(containerStyle.paddingLeft) || 0;
42
+ const paddingRight = parseFloat(containerStyle.paddingRight) || 0;
43
+ return containerRect.width - paddingLeft - paddingRight;
44
+ }
45
+
46
+ /**
47
+ * Measure all relevant elements (tags, label, buttons) inside the measurement container.
48
+ */
49
+ export function getElementMeasurements(measurementContainer: HTMLDivElement): ElementMeasurements {
50
+ const tags = Array.from(measurementContainer.querySelectorAll('[data-measure-tag]'));
51
+ const label = measurementContainer.querySelector('[data-measure-label]');
52
+ const clearButton = measurementContainer.querySelector('[data-measure-clear]');
53
+ const expandTag = measurementContainer.querySelector('[data-measure-expand]');
54
+
55
+ const computedStyle = getComputedStyle(measurementContainer);
56
+ const gap = parseFloat(computedStyle.gap) || 8;
57
+
58
+ const tagWidths = tags.map((tag) => tag.getBoundingClientRect().width);
59
+ const labelWidth = label ? label.getBoundingClientRect().width + gap : 0;
60
+ const clearButtonWidth = clearButton ? clearButton.getBoundingClientRect().width + gap : 0;
61
+ // Fallback width for expand tag (~100px for "+99 filters" text) if measurement fails
62
+ const expandTagWidth = expandTag ? expandTag.getBoundingClientRect().width + gap : 100;
63
+
64
+ return { tagWidths, labelWidth, clearButtonWidth, expandTagWidth, gap };
65
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './components/ds-alert-banner';
2
+ export * from './components/ds-avatar';
3
+ export * from './components/ds-avatar-group';
2
4
  export * from './components/ds-breadcrumb';
3
5
  export * from './components/ds-button';
4
6
  export * from './components/ds-checkbox';
@@ -29,6 +31,8 @@ export * from './components/ds-status-badge';
29
31
  export * from './components/ds-stepper';
30
32
  export * from './components/ds-system-status';
31
33
  export * from './components/ds-table';
34
+ export * from './components/ds-tag';
35
+ export * from './components/ds-tag-filter';
32
36
  export * from './components/ds-text-input';
33
37
  export * from './components/ds-textarea';
34
38
  export * from './components/ds-toast';