@arbor-education/design-system.components 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/articleCard/ArticleCard.d.ts +30 -0
  3. package/dist/components/articleCard/ArticleCard.d.ts.map +1 -0
  4. package/dist/components/articleCard/ArticleCard.js +24 -0
  5. package/dist/components/articleCard/ArticleCard.js.map +1 -0
  6. package/dist/components/articleCard/ArticleCard.stories.d.ts +18 -0
  7. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -0
  8. package/dist/components/articleCard/ArticleCard.stories.js +112 -0
  9. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -0
  10. package/dist/components/articleCard/ArticleCard.test.d.ts +2 -0
  11. package/dist/components/articleCard/ArticleCard.test.d.ts.map +1 -0
  12. package/dist/components/articleCard/ArticleCard.test.js +49 -0
  13. package/dist/components/articleCard/ArticleCard.test.js.map +1 -0
  14. package/dist/components/card/Card.d.ts +41 -12
  15. package/dist/components/card/Card.d.ts.map +1 -1
  16. package/dist/components/card/Card.js +46 -17
  17. package/dist/components/card/Card.js.map +1 -1
  18. package/dist/components/card/Card.stories.d.ts +9 -84
  19. package/dist/components/card/Card.stories.d.ts.map +1 -1
  20. package/dist/components/card/Card.stories.js +15 -73
  21. package/dist/components/card/Card.stories.js.map +1 -1
  22. package/dist/components/card/Card.test.js +50 -152
  23. package/dist/components/card/Card.test.js.map +1 -1
  24. package/dist/components/formField/inputs/number/NumberInput.d.ts.map +1 -1
  25. package/dist/components/formField/inputs/number/NumberInput.js +14 -2
  26. package/dist/components/formField/inputs/number/NumberInput.js.map +1 -1
  27. package/dist/components/formField/inputs/number/NumberInput.test.js +21 -0
  28. package/dist/components/formField/inputs/number/NumberInput.test.js.map +1 -1
  29. package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
  30. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
  31. package/dist/components/icoText/IcoText.d.ts +37 -0
  32. package/dist/components/icoText/IcoText.d.ts.map +1 -0
  33. package/dist/components/icoText/IcoText.js +29 -0
  34. package/dist/components/icoText/IcoText.js.map +1 -0
  35. package/dist/components/icoText/IcoText.stories.d.ts +34 -0
  36. package/dist/components/icoText/IcoText.stories.d.ts.map +1 -0
  37. package/dist/components/icoText/IcoText.stories.js +24 -0
  38. package/dist/components/icoText/IcoText.stories.js.map +1 -0
  39. package/dist/components/icoText/IcoText.test.d.ts +2 -0
  40. package/dist/components/icoText/IcoText.test.d.ts.map +1 -0
  41. package/dist/components/icoText/IcoText.test.js +27 -0
  42. package/dist/components/icoText/IcoText.test.js.map +1 -0
  43. package/dist/components/kpiCard/KPICard.d.ts +13 -0
  44. package/dist/components/kpiCard/KPICard.d.ts.map +1 -0
  45. package/dist/components/kpiCard/KPICard.js +8 -0
  46. package/dist/components/kpiCard/KPICard.js.map +1 -0
  47. package/dist/components/kpiCard/KPICard.stories.d.ts +9 -0
  48. package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -0
  49. package/dist/components/kpiCard/KPICard.stories.js +18 -0
  50. package/dist/components/kpiCard/KPICard.stories.js.map +1 -0
  51. package/dist/components/kpiCard/KPICard.test.d.ts +2 -0
  52. package/dist/components/kpiCard/KPICard.test.d.ts.map +1 -0
  53. package/dist/components/kpiCard/KPICard.test.js +37 -0
  54. package/dist/components/kpiCard/KPICard.test.js.map +1 -0
  55. package/dist/components/kvpList/KVPList.d.ts +34 -0
  56. package/dist/components/kvpList/KVPList.d.ts.map +1 -0
  57. package/dist/components/kvpList/KVPList.js +20 -0
  58. package/dist/components/kvpList/KVPList.js.map +1 -0
  59. package/dist/components/kvpList/KVPList.stories.d.ts +27 -0
  60. package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -0
  61. package/dist/components/kvpList/KVPList.stories.js +18 -0
  62. package/dist/components/kvpList/KVPList.stories.js.map +1 -0
  63. package/dist/components/kvpList/KVPList.test.d.ts +2 -0
  64. package/dist/components/kvpList/KVPList.test.d.ts.map +1 -0
  65. package/dist/components/kvpList/KVPList.test.js +29 -0
  66. package/dist/components/kvpList/KVPList.test.js.map +1 -0
  67. package/dist/components/singleUser/SingleUser.d.ts +1 -1
  68. package/dist/components/table/Table.d.ts +1 -0
  69. package/dist/components/table/Table.d.ts.map +1 -1
  70. package/dist/components/table/Table.js +4 -2
  71. package/dist/components/table/Table.js.map +1 -1
  72. package/dist/components/table/Table.stories.d.ts +1 -0
  73. package/dist/components/table/Table.stories.d.ts.map +1 -1
  74. package/dist/components/table/Table.stories.js +88 -0
  75. package/dist/components/table/Table.stories.js.map +1 -1
  76. package/dist/components/table/Table.test.js +184 -0
  77. package/dist/components/table/Table.test.js.map +1 -1
  78. package/dist/components/table/cellEditors/NumberCellEditor.d.ts +13 -0
  79. package/dist/components/table/cellEditors/NumberCellEditor.d.ts.map +1 -0
  80. package/dist/components/table/cellEditors/NumberCellEditor.js +35 -0
  81. package/dist/components/table/cellEditors/NumberCellEditor.js.map +1 -0
  82. package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
  83. package/dist/index.css +205 -22
  84. package/dist/index.css.map +1 -1
  85. package/dist/index.d.ts +14 -4
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +13 -3
  88. package/dist/index.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/components/articleCard/ArticleCard.stories.tsx +132 -0
  91. package/src/components/articleCard/ArticleCard.test.tsx +121 -0
  92. package/src/components/articleCard/ArticleCard.tsx +100 -0
  93. package/src/components/articleCard/articleCard.scss +39 -0
  94. package/src/components/card/Card.stories.tsx +35 -79
  95. package/src/components/card/Card.test.tsx +72 -190
  96. package/src/components/card/Card.tsx +117 -58
  97. package/src/components/card/card.scss +18 -31
  98. package/src/components/formField/inputs/number/NumberInput.test.tsx +28 -0
  99. package/src/components/formField/inputs/number/NumberInput.tsx +15 -0
  100. package/src/components/icoText/IcoText.stories.tsx +47 -0
  101. package/src/components/icoText/IcoText.test.tsx +41 -0
  102. package/src/components/icoText/IcoText.tsx +93 -0
  103. package/src/components/icoText/icoText.scss +34 -0
  104. package/src/components/kpiCard/KPICard.stories.tsx +47 -0
  105. package/src/components/kpiCard/KPICard.test.tsx +60 -0
  106. package/src/components/kpiCard/KPICard.tsx +45 -0
  107. package/src/components/kpiCard/kpiCard.scss +35 -0
  108. package/src/components/kvpList/KVPList.stories.tsx +51 -0
  109. package/src/components/kvpList/KVPList.test.tsx +66 -0
  110. package/src/components/kvpList/KVPList.tsx +109 -0
  111. package/src/components/kvpList/kvpList.scss +64 -0
  112. package/src/components/table/Table.stories.tsx +93 -0
  113. package/src/components/table/Table.test.tsx +255 -0
  114. package/src/components/table/Table.tsx +6 -0
  115. package/src/components/table/cellEditors/NumberCellEditor.tsx +83 -0
  116. package/src/components/table/cellEditors/numberCellEditor.scss +11 -0
  117. package/src/components/table/table.scss +11 -0
  118. package/src/index.scss +5 -0
  119. package/src/index.ts +14 -4
  120. package/src/tokens.scss +6 -0
  121. package/tokens/json/Arbor.json +30 -0
@@ -0,0 +1,109 @@
1
+ import classNames from 'classnames';
2
+
3
+ export type KVPListProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ children?: React.ReactNode;
5
+ };
6
+
7
+ export type KVPListRowProps = React.HTMLAttributes<HTMLDListElement> & {
8
+ children?: React.ReactNode;
9
+ orientation?: 'horizontal' | 'vertical';
10
+ };
11
+
12
+ export type KVPListTermProps = React.HTMLAttributes<HTMLElement> & {
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ export type KVPListDefinitionProps = React.HTMLAttributes<HTMLElement> & {
17
+ children?: React.ReactNode;
18
+ isRow?: boolean;
19
+ isPercentage?: boolean;
20
+ prominence?: 'neutral' | 'strong';
21
+ };
22
+
23
+ const KVPListRoot = ({
24
+ children,
25
+ className,
26
+ role,
27
+ 'aria-label': ariaLabel,
28
+ 'aria-labelledby': ariaLabelledBy,
29
+ ...rest
30
+ }: KVPListProps): React.JSX.Element => (
31
+ <div
32
+ className={classNames('ds-kvp-list', className)}
33
+ role={role ?? (ariaLabel || ariaLabelledBy ? 'group' : undefined)}
34
+ aria-label={ariaLabel}
35
+ aria-labelledby={ariaLabelledBy}
36
+ {...rest}
37
+ >
38
+ {children}
39
+ </div>
40
+ );
41
+
42
+ const KVPListRow = ({
43
+ children,
44
+ className,
45
+ orientation = 'vertical',
46
+ ...rest
47
+ }: KVPListRowProps): React.JSX.Element => (
48
+ <dl
49
+ className={classNames(
50
+ 'ds-kvp-list__row',
51
+ `ds-kvp-list__row--${orientation}`,
52
+ className,
53
+ )}
54
+ {...rest}
55
+ >
56
+ {children}
57
+ </dl>
58
+ );
59
+
60
+ const KVPListTerm = ({
61
+ children,
62
+ className,
63
+ ...rest
64
+ }: KVPListTermProps): React.JSX.Element => (
65
+ <dt className={classNames('ds-kvp-list__term', className)} {...rest}>
66
+ {children}
67
+ </dt>
68
+ );
69
+
70
+ const KVPListDefinition = ({
71
+ children,
72
+ className,
73
+ isRow = false,
74
+ isPercentage = false,
75
+ prominence = 'neutral',
76
+ ...rest
77
+ }: KVPListDefinitionProps): React.JSX.Element => (
78
+ <dd
79
+ className={classNames(
80
+ 'ds-kvp-list__description',
81
+ `ds-kvp-list__description--${prominence}`,
82
+ {
83
+ 'ds-kvp-list__description--row': isRow,
84
+ },
85
+ className,
86
+ )}
87
+ {...rest}
88
+ >
89
+ {children}
90
+ {isPercentage
91
+ ? (
92
+ <span className="ds-kvp-list__suffix">
93
+ %
94
+ </span>
95
+ )
96
+ : null}
97
+ </dd>
98
+ );
99
+
100
+ KVPListRoot.displayName = 'KVPList';
101
+ KVPListRow.displayName = 'KVPList.Row';
102
+ KVPListTerm.displayName = 'KVPList.Term';
103
+ KVPListDefinition.displayName = 'KVPList.Definition';
104
+
105
+ export const KVPList = Object.assign(KVPListRoot, {
106
+ Row: KVPListRow,
107
+ Term: KVPListTerm,
108
+ Definition: KVPListDefinition,
109
+ });
@@ -0,0 +1,64 @@
1
+ .ds-kvp-list {
2
+ display: flex;
3
+ width: 100%;
4
+ flex-direction: column;
5
+ gap: var(--spacing-medium);
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ .ds-kvp-list__term,
10
+ .ds-kvp-list__description {
11
+ margin: 0;
12
+ flex: 1;
13
+ }
14
+
15
+ .ds-kvp-list__term,
16
+ .ds-kvp-list__description--neutral {
17
+ color: var(--kvp-list-color-text-subtle);
18
+ font-family: var(--type-body-p-family);
19
+ font-size: var(--type-body-p-size);
20
+ font-weight: var(--type-body-p-weight);
21
+ line-height: var(--type-body-line-height);
22
+ }
23
+
24
+ .ds-kvp-list__description--strong {
25
+ color: var(--kvp-list-color-text-strong);
26
+ font-family: var(--type-headings-h2-family);
27
+ font-size: var(--type-headings-h2-size);
28
+ font-weight: var(--type-headings-h2-weight);
29
+ line-height: var(--type-headings-h2-line-height);
30
+ }
31
+
32
+ .ds-kvp-list__description--row {
33
+ flex-basis: 100%;
34
+ width: 100%;
35
+ }
36
+
37
+ .ds-kvp-list__suffix {
38
+ margin-left: var(--spacing-xsmall);
39
+ font: inherit;
40
+ }
41
+
42
+ .ds-kvp-list__description {
43
+ text-align: right;
44
+ }
45
+
46
+ .ds-kvp-list__row {
47
+ display: flex;
48
+ width: 100%;
49
+ margin: 0;
50
+ gap: var(--spacing-small);
51
+
52
+ &--vertical {
53
+ flex-direction: column;
54
+ }
55
+
56
+ &--horizontal {
57
+ flex-wrap: wrap;
58
+ align-items: center;
59
+
60
+ .ds-kvp-list__description:not(.ds-kvp-list__description--row) {
61
+ flex: 0 0 auto;
62
+ }
63
+ }
64
+ }
@@ -52,6 +52,7 @@ interface RowData {
52
52
  email: { value: string } & CellColors;
53
53
  role: { value: string } & CellColors;
54
54
  status: { value: string } & CellColors;
55
+ marks: number;
55
56
  active: boolean;
56
57
  time: string;
57
58
  dateOfBirth: Date;
@@ -64,6 +65,7 @@ const sampleData: RowData[] = [
64
65
  email: { value: 'alice.johnson@example.com' },
65
66
  role: { value: 'Developer' },
66
67
  status: { value: 'Active' },
68
+ marks: 84,
67
69
  active: true,
68
70
  time: '2026-01-28 12:00:00',
69
71
  dateOfBirth: new Date(1990, 0, 1),
@@ -74,6 +76,7 @@ const sampleData: RowData[] = [
74
76
  email: { value: 'bob.smith@example.com' },
75
77
  role: { value: 'Designer' },
76
78
  status: { value: 'Active' },
79
+ marks: 76,
77
80
  active: false,
78
81
  time: '2026-01-28 13:00:00',
79
82
  dateOfBirth: new Date(1991, 1, 2),
@@ -84,6 +87,7 @@ const sampleData: RowData[] = [
84
87
  email: { value: 'charlie.brown@example.com' },
85
88
  role: { value: 'Manager' },
86
89
  status: { value: 'Inactive' },
90
+ marks: 92,
87
91
  active: true,
88
92
  time: '2026-01-28 14:00:00',
89
93
  dateOfBirth: new Date(1992, 2, 3),
@@ -94,6 +98,7 @@ const sampleData: RowData[] = [
94
98
  email: { value: 'diana.prince@example.com' },
95
99
  role: { value: 'Developer' },
96
100
  status: { value: 'Active' },
101
+ marks: 88,
97
102
  active: false,
98
103
  time: '2026-01-28 15:00:00',
99
104
  dateOfBirth: new Date(1993, 3, 4),
@@ -104,6 +109,7 @@ const sampleData: RowData[] = [
104
109
  email: { value: 'ethan.hunt@example.com' },
105
110
  role: { value: 'Analyst' },
106
111
  status: { value: 'Active' },
112
+ marks: 69,
107
113
  active: false,
108
114
  time: '2026-01-28 16:00:00',
109
115
  dateOfBirth: new Date(1994, 4, 5),
@@ -154,6 +160,17 @@ const sampleColumnDefs: (ColDef | ColGroupDef)[] = [
154
160
  valueFormatter: Table.DefaultValueFormatter,
155
161
  },
156
162
  { field: 'status', valueFormatter: Table.DefaultValueFormatter },
163
+ {
164
+ field: 'marks',
165
+ cellDataType: 'number',
166
+ cellEditor: 'dsNumberCellEditor',
167
+ cellEditorParams: {
168
+ min: 0,
169
+ max: 100,
170
+ step: 1,
171
+ disableSpinners: false,
172
+ },
173
+ },
157
174
  {
158
175
  field: 'active',
159
176
  filter: { component: BooleanFilter, doesFilterPass: doesBooleanFilterPass },
@@ -168,6 +185,60 @@ const sampleColumnDefs: (ColDef | ColGroupDef)[] = [
168
185
  },
169
186
  ];
170
187
 
188
+ const verticalHeaderTextColumnDefs: (ColDef | ColGroupDef)[] = [
189
+ {
190
+ headerName: 'Details',
191
+ children: [
192
+ {
193
+ field: 'name',
194
+ headerName: 'Preferred Learner Name',
195
+ filter: 'agSetColumnFilter',
196
+ },
197
+ {
198
+ field: 'email',
199
+ headerName: 'Primary Contact Email Address',
200
+ filter: 'agSetColumnFilter',
201
+ },
202
+ {
203
+ field: 'role',
204
+ headerName: 'Role',
205
+ filter: 'agSetColumnFilter',
206
+ },
207
+ {
208
+ field: 'dateOfBirth',
209
+ headerName: 'Date of Birth',
210
+ filter: 'agDateColumnFilter',
211
+ filterParams: { buttons: ['clear', 'apply'] },
212
+ valueFormatter: params =>
213
+ params.value instanceof Date
214
+ ? params.value.toLocaleDateString()
215
+ : params.value,
216
+ },
217
+ ],
218
+ valueFormatter: Table.DefaultValueFormatter,
219
+ },
220
+ {
221
+ field: 'status',
222
+ headerName: 'Current Enrollment Status',
223
+ },
224
+ {
225
+ field: 'marks',
226
+ headerName: 'Overall Score (%)',
227
+ cellDataType: 'number',
228
+ },
229
+ {
230
+ field: 'active',
231
+ headerName: 'Active?',
232
+ cellDataType: 'boolean',
233
+ editable: false,
234
+ },
235
+ {
236
+ field: 'time',
237
+ headerName: 'Most Recent Activity Timestamp',
238
+ editable: false,
239
+ },
240
+ ];
241
+
171
242
  const footerContent = [
172
243
  <Button key={0} variant="primary">
173
244
  Button 1
@@ -201,6 +272,28 @@ export const Default: Story = {
201
272
  },
202
273
  };
203
274
 
275
+ export const WithVerticalHeaderText: Story = {
276
+ parameters: {
277
+ docs: {
278
+ description: {
279
+ story:
280
+ 'Use the `verticalHeaderText` prop when columns are narrow and header labels need to remain readable.',
281
+ },
282
+ },
283
+ },
284
+ args: {
285
+ rowData: sampleData,
286
+ columnDefs: verticalHeaderTextColumnDefs,
287
+ defaultColDef: {
288
+ ...defaultColDef,
289
+ wrapHeaderText: true,
290
+ autoHeaderHeight: true,
291
+ },
292
+ domLayout: 'autoHeight',
293
+ verticalHeaderText: true,
294
+ },
295
+ };
296
+
204
297
  export const WithFooter: Story = {
205
298
  args: {
206
299
  rowData: sampleData,
@@ -36,6 +36,20 @@ describe('Table', () => {
36
36
  expect(screen.getByTestId('test-table')).toHaveClass('ds-table__container');
37
37
  });
38
38
 
39
+ describe('verticalHeaderText', () => {
40
+ test('applies vertical header text class when enabled', () => {
41
+ const { container } = render(<Table verticalHeaderText />);
42
+
43
+ expect(container.querySelector('.ds-table')).toHaveClass('ds-table--vertical-header-text');
44
+ });
45
+
46
+ test('does not apply vertical header text class by default', () => {
47
+ const { container } = render(<Table />);
48
+
49
+ expect(container.querySelector('.ds-table')).not.toHaveClass('ds-table--vertical-header-text');
50
+ });
51
+ });
52
+
39
53
  describe('TableHeader', () => {
40
54
  test('renders header when headerContent is provided', () => {
41
55
  render(<Table headerContent={<div>Header content</div>} />);
@@ -1406,6 +1420,247 @@ describe('Table', () => {
1406
1420
 
1407
1421
  expect(onCellValueChanged).toHaveBeenLastCalledWith(expect.objectContaining({ oldValue: 30, newValue: 35 }));
1408
1422
  });
1423
+
1424
+ test('supports editing number fields with the number cell editor spinners', async () => {
1425
+ const onCellValueChanged = vi.fn();
1426
+ const columnDefs = [{
1427
+ field: 'age',
1428
+ headerName: 'Age',
1429
+ editable: true,
1430
+ cellDataType: 'number',
1431
+ cellEditor: 'dsNumberCellEditor',
1432
+ cellEditorParams: {
1433
+ min: 0,
1434
+ max: 100,
1435
+ step: 5,
1436
+ disableSpinners: false,
1437
+ },
1438
+ }];
1439
+ const rowData = [
1440
+ { id: 1, name: 'John Doe', age: 30 },
1441
+ { id: 2, name: 'Jane Smith', age: 25 },
1442
+ ];
1443
+
1444
+ render(
1445
+ <Table
1446
+ columnDefs={columnDefs}
1447
+ rowData={rowData}
1448
+ onCellValueChanged={onCellValueChanged}
1449
+ />,
1450
+ );
1451
+
1452
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1453
+ await waitFor(() => expect(screen.getByText('30')).toBeInTheDocument());
1454
+
1455
+ const cell = screen.getByText('30');
1456
+ await userEvent.dblClick(cell);
1457
+
1458
+ const editorInput = await screen.findByRole('textbox');
1459
+ expect(editorInput).toHaveFocus();
1460
+
1461
+ await userEvent.click(screen.getByLabelText('Plus button'));
1462
+ await userEvent.keyboard('{Enter}');
1463
+
1464
+ await waitFor(() => {
1465
+ expect(onCellValueChanged).toHaveBeenCalled();
1466
+ });
1467
+
1468
+ expect(onCellValueChanged).toHaveBeenLastCalledWith(expect.objectContaining({ oldValue: 30, newValue: 35 }));
1469
+ });
1470
+
1471
+ test('clamps out-of-range number editor values when committing with enter', async () => {
1472
+ const onCellValueChanged = vi.fn();
1473
+ const columnDefs = [{
1474
+ field: 'age',
1475
+ headerName: 'Age',
1476
+ editable: true,
1477
+ cellDataType: 'number',
1478
+ cellEditor: 'dsNumberCellEditor',
1479
+ cellEditorParams: {
1480
+ min: 0,
1481
+ max: 100,
1482
+ step: 1,
1483
+ disableSpinners: true,
1484
+ },
1485
+ }];
1486
+ const rowData = [
1487
+ { id: 1, name: 'John Doe', age: 30 },
1488
+ { id: 2, name: 'Jane Smith', age: 25 },
1489
+ ];
1490
+
1491
+ render(
1492
+ <Table
1493
+ columnDefs={columnDefs}
1494
+ rowData={rowData}
1495
+ onCellValueChanged={onCellValueChanged}
1496
+ />,
1497
+ );
1498
+
1499
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1500
+ await waitFor(() => expect(screen.getByText('30')).toBeInTheDocument());
1501
+
1502
+ const cell = screen.getByText('30');
1503
+ await userEvent.dblClick(cell);
1504
+
1505
+ const editorInput = await screen.findByRole('textbox');
1506
+ await userEvent.clear(editorInput);
1507
+ await userEvent.type(editorInput, '150');
1508
+ await userEvent.keyboard('{Enter}');
1509
+
1510
+ await waitFor(() => {
1511
+ expect(onCellValueChanged).toHaveBeenCalled();
1512
+ });
1513
+
1514
+ const lastCellChangeEvent = onCellValueChanged.mock.calls.at(-1)?.[0];
1515
+ expect(lastCellChangeEvent?.oldValue).toBe(30);
1516
+ expect(lastCellChangeEvent?.newValue).toBe(100);
1517
+ });
1518
+
1519
+ test('clamps below-min number editor values when committing with enter', async () => {
1520
+ const onCellValueChanged = vi.fn();
1521
+ const columnDefs = [{
1522
+ field: 'age',
1523
+ headerName: 'Age',
1524
+ editable: true,
1525
+ cellDataType: 'number',
1526
+ cellEditor: 'dsNumberCellEditor',
1527
+ cellEditorParams: {
1528
+ min: 0,
1529
+ max: 100,
1530
+ step: 1,
1531
+ disableSpinners: true,
1532
+ },
1533
+ }];
1534
+ const rowData = [
1535
+ { id: 1, name: 'John Doe', age: 30 },
1536
+ { id: 2, name: 'Jane Smith', age: 25 },
1537
+ ];
1538
+
1539
+ render(
1540
+ <Table
1541
+ columnDefs={columnDefs}
1542
+ rowData={rowData}
1543
+ onCellValueChanged={onCellValueChanged}
1544
+ />,
1545
+ );
1546
+
1547
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1548
+ await waitFor(() => expect(screen.getByText('30')).toBeInTheDocument());
1549
+
1550
+ const cell = screen.getByText('30');
1551
+ await userEvent.dblClick(cell);
1552
+
1553
+ const editorInput = await screen.findByRole('textbox');
1554
+ await userEvent.clear(editorInput);
1555
+ await userEvent.type(editorInput, '-5');
1556
+ await userEvent.keyboard('{Enter}');
1557
+
1558
+ await waitFor(() => {
1559
+ expect(onCellValueChanged).toHaveBeenCalled();
1560
+ });
1561
+
1562
+ const lastCellChangeEvent = onCellValueChanged.mock.calls.at(-1)?.[0];
1563
+ expect(lastCellChangeEvent?.oldValue).toBe(30);
1564
+ expect(lastCellChangeEvent?.newValue).toBe(0);
1565
+ });
1566
+
1567
+ test('does not clamp in-progress number editor typing before commit', async () => {
1568
+ const onCellValueChanged = vi.fn();
1569
+ const columnDefs = [{
1570
+ field: 'age',
1571
+ headerName: 'Age',
1572
+ editable: true,
1573
+ cellDataType: 'number',
1574
+ cellEditor: 'dsNumberCellEditor',
1575
+ cellEditorParams: {
1576
+ min: 50,
1577
+ max: 100,
1578
+ step: 1,
1579
+ disableSpinners: true,
1580
+ },
1581
+ }];
1582
+ const rowData = [
1583
+ { id: 1, name: 'John Doe', age: 60 },
1584
+ { id: 2, name: 'Jane Smith', age: 25 },
1585
+ ];
1586
+
1587
+ render(
1588
+ <Table
1589
+ columnDefs={columnDefs}
1590
+ rowData={rowData}
1591
+ onCellValueChanged={onCellValueChanged}
1592
+ />,
1593
+ );
1594
+
1595
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1596
+ await waitFor(() => expect(screen.getByText('60')).toBeInTheDocument());
1597
+
1598
+ const cell = screen.getByText('60');
1599
+ await userEvent.dblClick(cell);
1600
+
1601
+ const editorInput = await screen.findByRole('textbox');
1602
+ await userEvent.clear(editorInput);
1603
+ await userEvent.type(editorInput, '6');
1604
+ expect(editorInput).toHaveValue('6');
1605
+ await userEvent.type(editorInput, '5');
1606
+ await userEvent.keyboard('{Enter}');
1607
+
1608
+ await waitFor(() => {
1609
+ expect(onCellValueChanged).toHaveBeenCalled();
1610
+ });
1611
+
1612
+ const lastCellChangeEvent = onCellValueChanged.mock.calls.at(-1)?.[0];
1613
+ expect(lastCellChangeEvent?.oldValue).toBe(60);
1614
+ expect(lastCellChangeEvent?.newValue).toBe(65);
1615
+ });
1616
+
1617
+ test('supports clearing number editor values and committing null', async () => {
1618
+ const onCellValueChanged = vi.fn();
1619
+ const columnDefs = [{
1620
+ field: 'age',
1621
+ headerName: 'Age',
1622
+ editable: true,
1623
+ cellDataType: 'number',
1624
+ cellEditor: 'dsNumberCellEditor',
1625
+ cellEditorParams: {
1626
+ min: 0,
1627
+ max: 100,
1628
+ step: 1,
1629
+ disableSpinners: true,
1630
+ },
1631
+ }];
1632
+ const rowData = [
1633
+ { id: 1, name: 'John Doe', age: 30 },
1634
+ { id: 2, name: 'Jane Smith', age: 25 },
1635
+ ];
1636
+
1637
+ render(
1638
+ <Table
1639
+ columnDefs={columnDefs}
1640
+ rowData={rowData}
1641
+ onCellValueChanged={onCellValueChanged}
1642
+ />,
1643
+ );
1644
+
1645
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1646
+ await waitFor(() => expect(screen.getByText('30')).toBeInTheDocument());
1647
+
1648
+ const cell = screen.getByText('30');
1649
+ await userEvent.dblClick(cell);
1650
+
1651
+ const editorInput = await screen.findByRole('textbox');
1652
+ expect(screen.queryByLabelText('Plus button')).not.toBeInTheDocument();
1653
+ expect(screen.queryByLabelText('Minus button')).not.toBeInTheDocument();
1654
+
1655
+ await userEvent.clear(editorInput);
1656
+ await userEvent.keyboard('{Enter}');
1657
+
1658
+ await waitFor(() => {
1659
+ expect(onCellValueChanged).toHaveBeenCalled();
1660
+ });
1661
+
1662
+ expect(onCellValueChanged).toHaveBeenLastCalledWith(expect.objectContaining({ oldValue: 30, newValue: null }));
1663
+ });
1409
1664
  });
1410
1665
 
1411
1666
  /**
@@ -16,6 +16,7 @@ import { useTableSettings, type UseTableSettingsParams } from './useTableSetting
16
16
  import { setAgGridLicenseKey } from 'Utils/setAgGridLicenseKey';
17
17
  import { toggleRowSelectionInCurrentRange } from './toggleRowSelectionInCurrentRange';
18
18
  import { defaultValueFormatter, DSDefaultColDef, shouldSuppressFocus } from './DSDefaultColDef';
19
+ import { NumberCellEditor } from './cellEditors/NumberCellEditor';
19
20
  import { ButtonCellRenderer } from './cellRenderers/ButtonCellRenderer';
20
21
  import { InlineTextCellRenderer } from './cellRenderers/InlineTextCellRenderer';
21
22
  import { SelectDropdownCellRenderer } from './cellRenderers/SelectDropdownCellRenderer';
@@ -45,6 +46,7 @@ type TableProps<TData = any> = {
45
46
  'enableSimultaneousRangeAndRowSelection'?: boolean;
46
47
  'disableDragSelect'?: boolean;
47
48
  'tableTheme'?: string;
49
+ 'verticalHeaderText'?: boolean;
48
50
  } & UseTableSettingsParams & AgGridReactProps<TData>;
49
51
 
50
52
  setAgGridLicenseKey();
@@ -76,6 +78,8 @@ export const Table = (props: TableProps) => {
76
78
  components = {},
77
79
  tableTheme,
78
80
  onCellFocused,
81
+ verticalHeaderText = false,
82
+ className: propsClassName,
79
83
  ...rest
80
84
  } = props;
81
85
 
@@ -151,6 +155,7 @@ export const Table = (props: TableProps) => {
151
155
  setGridApi(api);
152
156
  onGridReady?.(event);
153
157
  }}
158
+ className={classNames('ds-table', { 'ds-table--vertical-header-text': verticalHeaderText }, propsClassName)}
154
159
  enableFilterHandlers
155
160
  suppressPaginationPanel
156
161
  onCellSelectionChanged={(event) => {
@@ -185,6 +190,7 @@ export const Table = (props: TableProps) => {
185
190
  dsSelectDropdownCellRenderer: SelectDropdownCellRenderer,
186
191
  dsBooleanFilter: BooleanFilter,
187
192
  dsTimeFilter: TimeFilter,
193
+ dsNumberCellEditor: NumberCellEditor,
188
194
  dsDateCellEditor: DateCellEditor,
189
195
  dsCheckboxCellRenderer: CheckboxCellRenderer,
190
196
  dsBooleanCellRenderer: BooleanCellRenderer,