@arbor-education/design-system.components 0.14.0 → 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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/components/formField/inputs/number/NumberInput.d.ts.map +1 -1
  3. package/dist/components/formField/inputs/number/NumberInput.js +14 -2
  4. package/dist/components/formField/inputs/number/NumberInput.js.map +1 -1
  5. package/dist/components/formField/inputs/number/NumberInput.test.js +21 -0
  6. package/dist/components/formField/inputs/number/NumberInput.test.js.map +1 -1
  7. package/dist/components/table/Table.d.ts +1 -0
  8. package/dist/components/table/Table.d.ts.map +1 -1
  9. package/dist/components/table/Table.js +4 -2
  10. package/dist/components/table/Table.js.map +1 -1
  11. package/dist/components/table/Table.stories.d.ts +1 -0
  12. package/dist/components/table/Table.stories.d.ts.map +1 -1
  13. package/dist/components/table/Table.stories.js +88 -0
  14. package/dist/components/table/Table.stories.js.map +1 -1
  15. package/dist/components/table/Table.test.js +184 -0
  16. package/dist/components/table/Table.test.js.map +1 -1
  17. package/dist/components/table/cellEditors/NumberCellEditor.d.ts +13 -0
  18. package/dist/components/table/cellEditors/NumberCellEditor.d.ts.map +1 -0
  19. package/dist/components/table/cellEditors/NumberCellEditor.js +35 -0
  20. package/dist/components/table/cellEditors/NumberCellEditor.js.map +1 -0
  21. package/dist/index.css +19 -0
  22. package/dist/index.css.map +1 -1
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/formField/inputs/number/NumberInput.test.tsx +28 -0
  29. package/src/components/formField/inputs/number/NumberInput.tsx +15 -0
  30. package/src/components/table/Table.stories.tsx +93 -0
  31. package/src/components/table/Table.test.tsx +255 -0
  32. package/src/components/table/Table.tsx +6 -0
  33. package/src/components/table/cellEditors/NumberCellEditor.tsx +83 -0
  34. package/src/components/table/cellEditors/numberCellEditor.scss +11 -0
  35. package/src/components/table/table.scss +11 -0
  36. package/src/index.scss +1 -0
  37. package/src/index.ts +3 -2
@@ -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,
@@ -0,0 +1,83 @@
1
+ import { NumberInput } from 'Components/formField/inputs/number/NumberInput';
2
+ import {
3
+ type ChangeEvent,
4
+ type KeyboardEvent,
5
+ } from 'react';
6
+
7
+ export type NumberCellEditorProps = {
8
+ value: number | null | undefined;
9
+ onValueChange: (value: number | null) => void;
10
+ min: number;
11
+ max: number;
12
+ step: number;
13
+ disableSpinners: boolean;
14
+ };
15
+
16
+ const clampValue = (value: number, min: number, max: number) => (
17
+ Math.min(Math.max(value, min), max)
18
+ );
19
+
20
+ export const NumberCellEditor = (props: NumberCellEditorProps) => {
21
+ const {
22
+ value,
23
+ onValueChange,
24
+ min,
25
+ max,
26
+ step,
27
+ disableSpinners,
28
+ } = props;
29
+
30
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
31
+ const nextValue = event.currentTarget.value.trim();
32
+
33
+ if (nextValue === '') {
34
+ onValueChange(null);
35
+ return;
36
+ }
37
+
38
+ const parsedValue = Number(nextValue);
39
+
40
+ if (!Number.isNaN(parsedValue)) {
41
+ onValueChange(clampValue(parsedValue, min, max));
42
+ }
43
+ };
44
+
45
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
46
+ if (event.key !== 'Enter') {
47
+ return;
48
+ }
49
+
50
+ const rawValue = event.currentTarget.value.trim();
51
+
52
+ if (rawValue === '') {
53
+ onValueChange(null);
54
+ return;
55
+ }
56
+
57
+ const parsedValue = Number(rawValue);
58
+
59
+ if (!Number.isNaN(parsedValue)) {
60
+ onValueChange(clampValue(parsedValue, min, max));
61
+ }
62
+ };
63
+
64
+ return (
65
+ <div
66
+ className="ds-number-cell-editor__wrapper"
67
+ >
68
+ <NumberInput
69
+ containerClassName="ds-number-cell-editor__container"
70
+ defaultValue={value == null ? '' : String(value)}
71
+ autoFocus
72
+ min={min}
73
+ max={max}
74
+ step={step}
75
+ disableSpinners={disableSpinners}
76
+ onChange={handleChange}
77
+ onKeyDown={handleKeyDown}
78
+ />
79
+ </div>
80
+ );
81
+ };
82
+
83
+ NumberCellEditor.displayName = 'NumberCellEditor';
@@ -0,0 +1,11 @@
1
+ .ds-number-cell-editor__wrapper {
2
+ height: 100%;
3
+ }
4
+
5
+ .ds-number-cell-editor__container {
6
+ height: 100%;
7
+
8
+ &:focus-within {
9
+ outline: none;
10
+ }
11
+ }
@@ -1,4 +1,15 @@
1
1
  .ds-table {
2
+ &--vertical-header-text {
3
+ .ag-header-cell-label .ag-header-cell-text {
4
+ writing-mode: vertical-rl;
5
+ transform: rotate(180deg);
6
+ text-wrap: balance;
7
+ text-align: center;
8
+ flex-shrink: 0;
9
+ min-width: fit-content;
10
+ }
11
+ }
12
+
2
13
  &__select-dropdown-cell {
3
14
  width: 100%;
4
15
  }
package/src/index.scss CHANGED
@@ -28,6 +28,7 @@
28
28
  @use "components/slideover/slideover.scss";
29
29
  @use "components/table/table.scss";
30
30
  @use "components/searchBar/searchBar.scss";
31
+ @use "components/table/cellEditors/numberCellEditor.scss";
31
32
  @use "components/table/pagination/pagination.scss";
32
33
  @use "components/tooltip/tooltip.scss";
33
34
  @use "components/separator/separator.scss";
package/src/index.ts CHANGED
@@ -39,11 +39,12 @@ export { Separator } from 'Components/separator/Separator';
39
39
  export { SingleUser } from 'Components/singleUser/SingleUser';
40
40
  export { Slideover } from 'Components/slideover/Slideover';
41
41
  export { SlideoverManager } from 'Components/slideoverManager/SlideoverManager';
42
- export { BooleanCellRenderer } from 'Components/table/cellRenderers/BooleanCellRenderer';
43
- export { CheckboxCellRenderer } from 'Components/table/cellRenderers/CheckboxCellRenderer';
44
42
  export { DefaultCellRenderer } from 'Components/table/cellRenderers/DefaultCellRenderer';
43
+ export { BooleanCellRenderer } from 'Components/table/cellRenderers/BooleanCellRenderer';
45
44
  export { DSDefaultColDef } from 'Components/table/DSDefaultColDef';
46
45
  export { GridApiContext } from 'Components/table/GridApiContext';
46
+ export { NumberCellEditor } from 'Components/table/cellEditors/NumberCellEditor';
47
+ export { CheckboxCellRenderer } from 'Components/table/cellRenderers/CheckboxCellRenderer';
47
48
  export { Table } from 'Components/table/Table';
48
49
  export { Tabs } from 'Components/tabs/Tabs';
49
50
  export { Tag } from 'Components/tag/Tag';