@arbor-education/design-system.components 0.3.1 → 0.3.2

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 (75) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
  3. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  4. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +19 -12
  5. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  6. package/dist/components/table/DSDefaultColDef.d.ts +2 -1
  7. package/dist/components/table/DSDefaultColDef.d.ts.map +1 -1
  8. package/dist/components/table/DSDefaultColDef.js +13 -1
  9. package/dist/components/table/DSDefaultColDef.js.map +1 -1
  10. package/dist/components/table/Table.d.ts +1 -0
  11. package/dist/components/table/Table.d.ts.map +1 -1
  12. package/dist/components/table/Table.js +34 -22
  13. package/dist/components/table/Table.js.map +1 -1
  14. package/dist/components/table/Table.stories.d.ts +1 -0
  15. package/dist/components/table/Table.stories.d.ts.map +1 -1
  16. package/dist/components/table/Table.stories.js +123 -1
  17. package/dist/components/table/Table.stories.js.map +1 -1
  18. package/dist/components/table/Table.test.js +115 -1
  19. package/dist/components/table/Table.test.js.map +1 -1
  20. package/dist/components/table/cellRenderers/ButtonCellRenderer.d.ts.map +1 -1
  21. package/dist/components/table/cellRenderers/ButtonCellRenderer.js +2 -1
  22. package/dist/components/table/cellRenderers/ButtonCellRenderer.js.map +1 -1
  23. package/dist/components/table/cellRenderers/InlineTextCellRenderer.d.ts +7 -0
  24. package/dist/components/table/cellRenderers/InlineTextCellRenderer.d.ts.map +1 -0
  25. package/dist/components/table/cellRenderers/InlineTextCellRenderer.js +7 -0
  26. package/dist/components/table/cellRenderers/InlineTextCellRenderer.js.map +1 -0
  27. package/dist/components/table/cellRenderers/SelectDropdownCellEditor.d.ts +8 -0
  28. package/dist/components/table/cellRenderers/SelectDropdownCellEditor.d.ts.map +1 -0
  29. package/dist/components/table/cellRenderers/SelectDropdownCellEditor.js +19 -0
  30. package/dist/components/table/cellRenderers/SelectDropdownCellEditor.js.map +1 -0
  31. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts +8 -0
  32. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -0
  33. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +18 -0
  34. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -0
  35. package/dist/components/table/theme/baseThemeParams.d.ts +14 -0
  36. package/dist/components/table/theme/baseThemeParams.d.ts.map +1 -0
  37. package/dist/components/table/{tableTheme.js → theme/baseThemeParams.js} +3 -7
  38. package/dist/components/table/theme/baseThemeParams.js.map +1 -0
  39. package/dist/components/table/theme/defaultTheme.d.ts +2 -0
  40. package/dist/components/table/theme/defaultTheme.d.ts.map +1 -0
  41. package/dist/components/table/theme/defaultTheme.js +9 -0
  42. package/dist/components/table/theme/defaultTheme.js.map +1 -0
  43. package/dist/components/table/theme/tidyTheme.d.ts +2 -0
  44. package/dist/components/table/theme/tidyTheme.d.ts.map +1 -0
  45. package/dist/components/table/theme/tidyTheme.js +10 -0
  46. package/dist/components/table/theme/tidyTheme.js.map +1 -0
  47. package/dist/index.css +14 -0
  48. package/dist/index.css.map +1 -1
  49. package/dist/utils/focusFirstFocusableElement.d.ts +2 -0
  50. package/dist/utils/focusFirstFocusableElement.d.ts.map +1 -0
  51. package/dist/utils/focusFirstFocusableElement.js +12 -0
  52. package/dist/utils/focusFirstFocusableElement.js.map +1 -0
  53. package/dist/utils/focusFirstFocusableElement.test.d.ts +2 -0
  54. package/dist/utils/focusFirstFocusableElement.test.d.ts.map +1 -0
  55. package/dist/utils/focusFirstFocusableElement.test.js +109 -0
  56. package/dist/utils/focusFirstFocusableElement.test.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +20 -14
  59. package/src/components/table/DSDefaultColDef.ts +15 -2
  60. package/src/components/table/Table.stories.tsx +129 -1
  61. package/src/components/table/Table.test.tsx +162 -1
  62. package/src/components/table/Table.tsx +39 -18
  63. package/src/components/table/cellRenderers/ButtonCellRenderer.tsx +2 -1
  64. package/src/components/table/cellRenderers/InlineTextCellRenderer.tsx +10 -0
  65. package/src/components/table/cellRenderers/SelectDropdownCellEditor.tsx +43 -0
  66. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +37 -0
  67. package/src/components/table/table.scss +16 -0
  68. package/src/components/table/{tableTheme.ts → theme/baseThemeParams.ts} +2 -7
  69. package/src/components/table/theme/defaultTheme.ts +9 -0
  70. package/src/components/table/theme/tidyTheme.ts +10 -0
  71. package/src/utils/focusFirstFocusableElement.test.ts +150 -0
  72. package/src/utils/focusFirstFocusableElement.ts +21 -0
  73. package/dist/components/table/tableTheme.d.ts +0 -2
  74. package/dist/components/table/tableTheme.d.ts.map +0 -1
  75. package/dist/components/table/tableTheme.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, expectTypeOf, test, vi } from 'vitest';
2
- import { render, screen, waitFor } from '@testing-library/react';
2
+ import { act, render, screen, waitFor } from '@testing-library/react';
3
3
  import { Table, TABLE_SPACING } from './Table';
4
4
  import '@testing-library/jest-dom/vitest';
5
5
  import { BulkActionsDropdown } from 'Components/table/BulkActionsDropdown';
@@ -7,6 +7,7 @@ import { HideColumnsDropdown } from 'Components/table/HideColumnsDropdown';
7
7
  import { TableSettingsDropdown } from './pagination/TableSettingsDropdown';
8
8
  import userEvent from '@testing-library/user-event';
9
9
  import type { GridApi } from 'ag-grid-enterprise';
10
+ import * as focusFirstFocusableElementModule from 'Utils/focusFirstFocusableElement';
10
11
 
11
12
  describe('Table', () => {
12
13
  test('renders table container', () => {
@@ -1235,6 +1236,125 @@ describe('Table', () => {
1235
1236
  });
1236
1237
  });
1237
1238
 
1239
+ describe('SelectDropdownCellRenderer', () => {
1240
+ const options = [{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }];
1241
+ const columnDefs = [{
1242
+ field: 'selectField',
1243
+ headerName: 'Select Field',
1244
+ cellRenderer: 'dsSelectDropdownCellRenderer',
1245
+ cellRendererParams: { options: options, placeholder: 'Placeholder Text' },
1246
+ }];
1247
+
1248
+ test('renders with placeholder', () => {
1249
+ const rowData = [{ selectField: null }];
1250
+ render(
1251
+ <Table
1252
+ columnDefs={columnDefs}
1253
+ rowData={rowData}
1254
+ />,
1255
+ );
1256
+ expect(screen.getByText('Placeholder Text')).toBeInTheDocument();
1257
+ });
1258
+
1259
+ test('renders with option', () => {
1260
+ const rowData = [{ selectField: 'option1' }];
1261
+ render(
1262
+ <Table
1263
+ columnDefs={columnDefs}
1264
+ rowData={rowData}
1265
+ />,
1266
+ );
1267
+ expect(screen.getByText('Option 1')).toBeInTheDocument();
1268
+ });
1269
+
1270
+ test('renders with value not in options as raw string', () => {
1271
+ const rowData = [{ selectField: 'unknown-value' }];
1272
+ render(
1273
+ <Table
1274
+ columnDefs={columnDefs}
1275
+ rowData={rowData}
1276
+ />,
1277
+ );
1278
+ expect(screen.getByText('unknown-value')).toBeInTheDocument();
1279
+ });
1280
+
1281
+ test('renders with empty string shows placeholder', () => {
1282
+ const rowData = [{ selectField: '' }];
1283
+ render(
1284
+ <Table
1285
+ columnDefs={columnDefs}
1286
+ rowData={rowData}
1287
+ />,
1288
+ );
1289
+ expect(screen.getByText('Placeholder Text')).toBeInTheDocument();
1290
+ });
1291
+
1292
+ test('renders dropdown button variant in cell', () => {
1293
+ const rowData = [{ selectField: 'option1' }];
1294
+ const { container } = render(
1295
+ <Table
1296
+ columnDefs={columnDefs}
1297
+ rowData={rowData}
1298
+ />,
1299
+ );
1300
+ const dropdownButton = container.querySelector('.ds-table__select-dropdown-cell.ds-button--dropdown');
1301
+ expect(dropdownButton).toBeInTheDocument();
1302
+ });
1303
+ });
1304
+
1305
+ describe('SelectDropdownCellEditor', () => {
1306
+ const options = [
1307
+ { label: 'Option 1', value: 'option1' },
1308
+ { label: 'Option 2', value: 'option2' },
1309
+ { label: 'Option 3', value: 'option3' },
1310
+ ];
1311
+ const editableSelectColumnDefs = [{
1312
+ field: 'selectField',
1313
+ headerName: 'Select Field',
1314
+ cellRenderer: 'dsSelectDropdownCellRenderer',
1315
+ cellEditor: 'dsSelectDropdownCellEditor',
1316
+ cellRendererParams: { options, placeholder: 'Placeholder Text' },
1317
+ cellEditorParams: { options, placeholder: 'Placeholder Text' },
1318
+ editable: true,
1319
+ }];
1320
+
1321
+ async function renderAndStartEditing(rowData: { selectField: string }[]) {
1322
+ let gridApi: GridApi | null = null;
1323
+ render(
1324
+ <Table
1325
+ columnDefs={editableSelectColumnDefs}
1326
+ rowData={rowData}
1327
+ onGridReady={(e) => {
1328
+ gridApi = e.api;
1329
+ }}
1330
+ />,
1331
+ );
1332
+ await waitFor(() => expect(gridApi).toBeTruthy());
1333
+ await act(async () => {
1334
+ gridApi!.startEditingCell({ rowIndex: 0, colKey: 'selectField' });
1335
+ });
1336
+ const editorWrapper = await waitFor(() => document.querySelector('.ds-table__select-dropdown-editor'));
1337
+ return editorWrapper!.querySelector('button')!;
1338
+ }
1339
+
1340
+ test('opens editor with dropdown; selecting option updates cell', async () => {
1341
+ const trigger = await renderAndStartEditing([{ selectField: 'option1' }]);
1342
+ expect(trigger).toHaveTextContent('Option 1');
1343
+ await userEvent.click(trigger);
1344
+ await userEvent.click(screen.getByText('Option 2'));
1345
+ await waitFor(() => expect(screen.getByText('Option 2')).toBeInTheDocument());
1346
+ });
1347
+
1348
+ test('Escape closes dropdown without changing value', async () => {
1349
+ const trigger = await renderAndStartEditing([{ selectField: 'option1' }]);
1350
+ await userEvent.click(trigger);
1351
+ await waitFor(() => expect(screen.getByText('Option 2')).toBeInTheDocument());
1352
+ await userEvent.keyboard('{Escape}');
1353
+ expect(screen.queryByText('Option 2')).not.toBeInTheDocument();
1354
+ expect(screen.getByText('Option 1')).toBeInTheDocument();
1355
+ });
1356
+ });
1357
+
1238
1358
  describe('Cell Editing', () => {
1239
1359
  describe('with literal data values', () => {
1240
1360
  test('supports editing text fields', async () => {
@@ -1400,4 +1520,45 @@ describe('Table', () => {
1400
1520
  });
1401
1521
  });
1402
1522
  });
1523
+
1524
+ describe('supressCellFocusAndFocusFirstElement', () => {
1525
+ const handleClick = vi.fn();
1526
+ const columnDefs = [{
1527
+ field: 'action',
1528
+ headerName: 'Action',
1529
+ cellRenderer: 'dsButtonCellRenderer',
1530
+ cellRendererParams: {
1531
+ supressCellFocusAndFocusFirstElement: true,
1532
+ },
1533
+ }];
1534
+ const rowData = [{
1535
+ action: {
1536
+ children: 'Im a lovely button',
1537
+ onClick: handleClick,
1538
+ },
1539
+ }];
1540
+
1541
+ test('setting supressCellFocusAndFocusFirstElement to true should sadd the ds-table__cell--supress-focus class', async () => {
1542
+ render(<Table columnDefs={columnDefs} rowData={rowData} />);
1543
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1544
+ });
1545
+
1546
+ test('setting supressCellFocusAndFocusFirstElement to true should suppress cell focus and focus the first element', async () => {
1547
+ const focusFirstFocusableElementSpy = vi.spyOn(
1548
+ focusFirstFocusableElementModule,
1549
+ 'focusFirstFocusableElement',
1550
+ );
1551
+ render(<Table columnDefs={columnDefs} rowData={rowData} />);
1552
+ await waitFor(() => expect(screen.getByRole('grid')).toBeInTheDocument());
1553
+ await waitFor(() => expect(screen.getByText('Im a lovely button')).toBeInTheDocument());
1554
+ // first tab should focus the table heading
1555
+ await userEvent.tab();
1556
+ expect(screen.getByRole('columnheader')).toHaveFocus();
1557
+ // second tab should focus the button bypassing the cell
1558
+ await userEvent.tab();
1559
+ expect(focusFirstFocusableElementSpy).toHaveBeenCalledTimes(1);
1560
+ const button = screen.getByText('Im a lovely button');
1561
+ expect(button).toHaveFocus();
1562
+ });
1563
+ });
1403
1564
  });
@@ -1,6 +1,6 @@
1
1
  import { AllEnterpriseModule, ModuleRegistry, type GridApi } from 'ag-grid-enterprise';
2
2
  import { AgGridReact, type AgGridReactProps } from 'ag-grid-react';
3
- import { tableTheme } from './tableTheme';
3
+ import { defaultTheme } from './theme/defaultTheme';
4
4
  import classNames from 'classnames';
5
5
  import { createContext, useRef, useState, type ReactNode } from 'react';
6
6
  import { TableFooter } from './TableFooter';
@@ -15,8 +15,13 @@ import { HideColumnsDropdown } from './HideColumnsDropdown';
15
15
  import { useTableSettings, type UseTableSettingsParams } from './useTableSettings';
16
16
  import { setAgGridLicenseKey } from 'Utils/setAgGridLicenseKey';
17
17
  import { toggleRowSelectionInCurrentRange } from './toggleRowSelectionInCurrentRange';
18
- import { defaultValueFormatter, DSDefaultColDef } from './DSDefaultColDef';
18
+ import { defaultValueFormatter, DSDefaultColDef, shouldSuppressFocus } from './DSDefaultColDef';
19
19
  import { ButtonCellRenderer } from './cellRenderers/ButtonCellRenderer';
20
+ import { InlineTextCellRenderer } from './cellRenderers/InlineTextCellRenderer';
21
+ import { SelectDropdownCellRenderer } from './cellRenderers/SelectDropdownCellRenderer';
22
+ import { SelectDropdownCellEditor } from './cellRenderers/SelectDropdownCellEditor';
23
+ import { tidyTheme } from './theme/tidyTheme';
24
+ import { focusFirstFocusableElement } from 'Utils/focusFirstFocusableElement';
20
25
 
21
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
27
  type TableProps<TData = any> = {
@@ -29,6 +34,7 @@ type TableProps<TData = any> = {
29
34
  'headerTestId'?: string;
30
35
  'enableSimultaneousRangeAndRowSelection'?: boolean;
31
36
  'disableDragSelect'?: boolean;
37
+ 'tableTheme'?: string;
32
38
  } & UseTableSettingsParams & AgGridReactProps<TData>;
33
39
 
34
40
  setAgGridLicenseKey();
@@ -75,6 +81,8 @@ export const Table = (props: TableProps) => {
75
81
  defaultColDef = {},
76
82
  context = {},
77
83
  components = {},
84
+ tableTheme,
85
+ onCellFocused,
78
86
  ...rest
79
87
  } = props;
80
88
 
@@ -100,6 +108,24 @@ export const Table = (props: TableProps) => {
100
108
 
101
109
  const { hasColumnBorders, tableSpacing } = settings;
102
110
 
111
+ const getTheme = () => {
112
+ if (tableTheme === 'tidy') {
113
+ return tidyTheme;
114
+ }
115
+ return defaultTheme.withParams({
116
+ headerRowBorder: hasColumnBorders,
117
+ rowBorder: hasColumnBorders,
118
+ wrapperBorder: hasColumnBorders,
119
+ columnBorder: hasColumnBorders,
120
+ spacing: {
121
+ [TABLE_SPACING.XS]: 0,
122
+ [TABLE_SPACING.S]: 'var(--spacing-xsmall)',
123
+ [TABLE_SPACING.M]: 'var(--spacing-small)',
124
+ [TABLE_SPACING.L]: 'var(--spacing-medium)',
125
+ }[tableSpacing],
126
+ });
127
+ };
128
+
103
129
  return (
104
130
  <GridApiContext.Provider value={gridApi}>
105
131
  <TableSettingsContext.Provider
@@ -126,22 +152,7 @@ export const Table = (props: TableProps) => {
126
152
  </TableHeader>
127
153
  )}
128
154
  <AgGridReact
129
- // TODO: make this overridable when we come to do Tidy Table theming
130
- theme={tableTheme.withParams({
131
- headerRowBorder: hasColumnBorders,
132
- rowBorder: hasColumnBorders,
133
- wrapperBorder: hasColumnBorders,
134
- columnBorder: hasColumnBorders,
135
- spacing: {
136
- // These are offset by one from the usual spacing tokens. We still use the token values to make spacing
137
- // consistent with other parts of the DS, but --spacing-xsmall was too large for xsmall spacing, and so on
138
- // for the other tokens
139
- [TABLE_SPACING.XS]: 0,
140
- [TABLE_SPACING.S]: 'var(--spacing-xsmall)',
141
- [TABLE_SPACING.M]: 'var(--spacing-small)',
142
- [TABLE_SPACING.L]: 'var(--spacing-medium)',
143
- }[tableSpacing],
144
- })}
155
+ theme={getTheme()}
145
156
  onGridReady={(event) => {
146
157
  const { api } = event;
147
158
  setGridApi(api);
@@ -162,6 +173,13 @@ export const Table = (props: TableProps) => {
162
173
  onCellSelectionChanged(event);
163
174
  }
164
175
  }}
176
+ onCellFocused={(params) => {
177
+ if (shouldSuppressFocus(params) && document.activeElement) {
178
+ focusFirstFocusableElement(document.activeElement as HTMLElement);
179
+ }
180
+
181
+ onCellFocused?.(params);
182
+ }}
165
183
  defaultColDef={{ ...DSDefaultColDef, ...defaultColDef }}
166
184
  context={{
167
185
  settings,
@@ -169,6 +187,9 @@ export const Table = (props: TableProps) => {
169
187
  }}
170
188
  components={{
171
189
  dsButtonCellRenderer: ButtonCellRenderer,
190
+ dsInlineTextCellRenderer: InlineTextCellRenderer,
191
+ dsSelectDropdownCellRenderer: SelectDropdownCellRenderer,
192
+ dsSelectDropdownCellEditor: SelectDropdownCellEditor,
172
193
  ...components,
173
194
  }}
174
195
  {...rest}
@@ -1,4 +1,5 @@
1
1
  import type { CustomCellRendererProps } from 'ag-grid-react';
2
+ import classNames from 'classnames';
2
3
  import { Button, type ButtonProps } from 'Components/button/Button';
3
4
 
4
5
  type ButtonCellRendererProps = CustomCellRendererProps & { value: ButtonProps };
@@ -21,5 +22,5 @@ export const ButtonCellRenderer = (props: ButtonCellRendererProps) => {
21
22
  childToRender = resolvedValue;
22
23
  }
23
24
 
24
- return <Button {...value}>{childToRender}</Button>;
25
+ return <Button {...value} className={classNames('ds-button-cell-renderer', value.className)}>{childToRender}</Button>;
25
26
  };
@@ -0,0 +1,10 @@
1
+ import type { CustomCellRendererProps } from 'ag-grid-react';
2
+
3
+ type InlineTextCellRendererProps = CustomCellRendererProps & { value: string };
4
+
5
+ export const InlineTextCellRenderer = (props: InlineTextCellRendererProps) => {
6
+ const { value, valueFormatted } = props;
7
+ const valueToRender = String(valueFormatted ?? value ?? '');
8
+
9
+ return <span className="ds-table__inline-text-cell">{valueToRender}</span>;
10
+ };
@@ -0,0 +1,43 @@
1
+ import { memo, useCallback, useRef } from 'react';
2
+ import type { CustomCellEditorProps } from 'ag-grid-react';
3
+ import { SelectDropdown } from 'Components/formField/inputs/selectDropdown/SelectDropdown';
4
+ import type { SelectDropdownItemProps } from 'Components/formField/inputs/selectDropdown/items/item/SelectDropdownItem';
5
+ import { PopupParentContext } from 'Utils/PopupParentContext';
6
+
7
+ export type SelectDropdownCellEditorParams = {
8
+ options: SelectDropdownItemProps[];
9
+ placeholder?: string;
10
+ };
11
+
12
+ export const SelectDropdownCellEditor = memo((props: CustomCellEditorProps & SelectDropdownCellEditorParams) => {
13
+ const { value, onValueChange, stopEditing } = props;
14
+ const options = props.options ?? [];
15
+ const placeholder = props.placeholder ?? 'Select';
16
+ const wrapperRef = useRef<HTMLDivElement>(null);
17
+
18
+ const initialSelectedValues = value != null && value !== '' ? [String(value)] : [];
19
+
20
+ const handleSelectionChange = useCallback(
21
+ (selected: string[]) => {
22
+ const newValue = selected.length > 0 ? selected[0] : '';
23
+ onValueChange(newValue);
24
+ stopEditing();
25
+ },
26
+ [onValueChange, stopEditing],
27
+ );
28
+
29
+ return (
30
+ <PopupParentContext.Provider value={wrapperRef}>
31
+ <div ref={wrapperRef} className="ds-table__select-dropdown-editor">
32
+ <SelectDropdown
33
+ options={options}
34
+ placeholder={placeholder}
35
+ initialSelectedValues={initialSelectedValues}
36
+ onSelectionChange={handleSelectionChange}
37
+ />
38
+ </div>
39
+ </PopupParentContext.Provider>
40
+ );
41
+ });
42
+
43
+ SelectDropdownCellEditor.displayName = 'SelectDropdownCellEditor';
@@ -0,0 +1,37 @@
1
+ import type { CustomCellRendererProps } from 'ag-grid-react';
2
+ import type { SelectDropdownItemProps } from 'Components/formField/inputs/selectDropdown/items/item/SelectDropdownItem';
3
+ import { Button } from 'Components/button/Button';
4
+
5
+ export type SelectDropdownCellRendererProps = CustomCellRendererProps & {
6
+ options?: SelectDropdownItemProps[];
7
+ placeholder?: string;
8
+ };
9
+
10
+ export const SelectDropdownCellRenderer = (props: SelectDropdownCellRendererProps) => {
11
+ const { value, valueFormatted, placeholder = 'Select' } = props;
12
+ const options = props.options ?? [];
13
+
14
+ const selectedValue = valueFormatted ?? value;
15
+ const valueStr = value != null && value !== '' ? String(value) : '';
16
+
17
+ const option = options.find((opt: SelectDropdownItemProps) => opt.value === valueStr);
18
+ const displayText = selectedValue != null && selectedValue !== ''
19
+ ? (option?.label ?? option?.value ?? String(selectedValue))
20
+ : placeholder;
21
+
22
+ const handleMouseDown = (e: React.MouseEvent) => {
23
+ e.preventDefault();
24
+ e.stopPropagation();
25
+ };
26
+
27
+ return (
28
+ <Button
29
+ variant="dropdown"
30
+ iconRightName="chevron-down"
31
+ className="ds-table__select-dropdown-cell"
32
+ onMouseDown={handleMouseDown}
33
+ >
34
+ {displayText}
35
+ </Button>
36
+ );
37
+ };
@@ -1,4 +1,16 @@
1
1
  .ds-table {
2
+ &__inline-text-cell {
3
+ padding: var(--spacing-small) var(--spacing-medium);
4
+ border-radius: var(--form-field-radius);
5
+ border: var(--border-weight) solid var(--form-field-text-default-color-border);
6
+ background: var(--form-field-text-default-color-background);
7
+ color: var(--form-field-text-default-color-text);
8
+ }
9
+
10
+ &__select-dropdown-cell {
11
+ width: 100%;
12
+ }
13
+
2
14
  &__container {
3
15
  .ag-header-cell-resize {
4
16
  // AG-Grid gives these a z-index of 2 which interferes with our stacking elements
@@ -52,4 +64,8 @@
52
64
  &__cell--invalid {
53
65
  box-shadow: inset 0 0 0 2px var(--form-field-text-error-color-border);
54
66
  }
67
+ }
68
+
69
+ .ag-cell-focus.ds-table__cell--supress-focus:focus-within {
70
+ border-color: transparent;
55
71
  }
@@ -1,8 +1,5 @@
1
- import { themeQuartz } from 'ag-grid-enterprise';
2
-
3
- export const tableTheme = themeQuartz.withParams({
1
+ export const baseThemeParams = {
4
2
  accentColor: 'var(--color-brand-700)',
5
- backgroundColor: 'var(--tables-ag-grid-rows-even-color-background)',
6
3
  borderColor: 'var(--tables-ag-grid-rows-color-border)',
7
4
  borderRadius: 'var(--border-radius-small)',
8
5
  checkboxBorderRadius: 'var(--border-radius-xsmall)',
@@ -13,6 +10,4 @@ export const tableTheme = themeQuartz.withParams({
13
10
  headerFontSize: 'var(--type-body-p-size)',
14
11
  headerFontWeight: 'var(--type-body-bold-weight)',
15
12
  iconButtonBorderRadius: 'var(--border-radius-round)',
16
- oddRowBackgroundColor: 'var(--tables-ag-grid-rows-odd-color-background)',
17
- invalidColor: 'var(--form-field-text-error-color-border)',
18
- });
13
+ };
@@ -0,0 +1,9 @@
1
+ import { themeQuartz } from 'ag-grid-enterprise';
2
+ import { baseThemeParams } from './baseThemeParams';
3
+
4
+ export const defaultTheme = themeQuartz.withParams({
5
+ ...baseThemeParams,
6
+ backgroundColor: 'var(--tables-ag-grid-rows-even-color-background)',
7
+ oddRowBackgroundColor: 'var(--tables-ag-grid-rows-odd-color-background)',
8
+ invalidColor: 'var(--form-field-text-error-color-border)',
9
+ });
@@ -0,0 +1,10 @@
1
+ import { themeQuartz } from 'ag-grid-enterprise';
2
+ import { baseThemeParams } from './baseThemeParams';
3
+
4
+ export const tidyTheme = themeQuartz.withParams({
5
+ ...baseThemeParams,
6
+ backgroundColor: 'var(--color-mono-white)',
7
+ rowBorder: false,
8
+ wrapperBorder: false,
9
+ headerBackgroundColor: 'var(--color-mono-white)',
10
+ });
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
2
+ import { focusFirstFocusableElement } from './focusFirstFocusableElement';
3
+
4
+ describe('focusFirstFocusableElement', () => {
5
+ let container: HTMLElement;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ document.body.removeChild(container);
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ test('does nothing when containerElementRef is null', () => {
18
+ const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus');
19
+
20
+ focusFirstFocusableElement(null);
21
+
22
+ expect(focusSpy).not.toHaveBeenCalled();
23
+ });
24
+
25
+ test('does nothing when container has no focusable elements', () => {
26
+ container.innerHTML = '<div>No focusable elements</div>';
27
+ const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus');
28
+
29
+ focusFirstFocusableElement(container);
30
+
31
+ expect(focusSpy).not.toHaveBeenCalled();
32
+ });
33
+
34
+ test('focuses the first button element', () => {
35
+ container.innerHTML = '<button>First Button</button><button>Second Button</button>';
36
+ const firstButton = container.querySelector('button') as HTMLElement;
37
+ const focusSpy = vi.spyOn(firstButton, 'focus');
38
+
39
+ focusFirstFocusableElement(container);
40
+
41
+ expect(focusSpy).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ test('focuses the first input element', () => {
45
+ container.innerHTML = '<input type="text" /><input type="text" />';
46
+ const firstInput = container.querySelector('input') as HTMLElement;
47
+ const focusSpy = vi.spyOn(firstInput, 'focus');
48
+
49
+ focusFirstFocusableElement(container);
50
+
51
+ expect(focusSpy).toHaveBeenCalledTimes(1);
52
+ });
53
+
54
+ test('focuses the first link element', () => {
55
+ container.innerHTML = '<a href="#">First Link</a><a href="#">Second Link</a>';
56
+ const firstLink = container.querySelector('a') as HTMLElement;
57
+ const focusSpy = vi.spyOn(firstLink, 'focus');
58
+
59
+ focusFirstFocusableElement(container);
60
+
61
+ expect(focusSpy).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ test('focuses the first select element', () => {
65
+ container.innerHTML = '<select><option>Option 1</option></select><select><option>Option 2</option></select>';
66
+ const firstSelect = container.querySelector('select') as HTMLElement;
67
+ const focusSpy = vi.spyOn(firstSelect, 'focus');
68
+
69
+ focusFirstFocusableElement(container);
70
+
71
+ expect(focusSpy).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ test('focuses the first textarea element', () => {
75
+ container.innerHTML = '<textarea></textarea><textarea></textarea>';
76
+ const firstTextarea = container.querySelector('textarea') as HTMLElement;
77
+ const focusSpy = vi.spyOn(firstTextarea, 'focus');
78
+
79
+ focusFirstFocusableElement(container);
80
+
81
+ expect(focusSpy).toHaveBeenCalledTimes(1);
82
+ });
83
+
84
+ test('focuses element with tabindex attribute', () => {
85
+ container.innerHTML = '<div tabindex="0">Focusable div</div><div tabindex="1">Another focusable div</div>';
86
+ const firstDiv = container.querySelector('[tabindex]') as HTMLElement;
87
+ const focusSpy = vi.spyOn(firstDiv, 'focus');
88
+
89
+ focusFirstFocusableElement(container);
90
+
91
+ expect(focusSpy).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ test('ignores disabled buttons', () => {
95
+ container.innerHTML = '<button disabled>Disabled</button><button>Enabled</button>';
96
+ const enabledButton = container.querySelector('button:not([disabled])') as HTMLElement;
97
+ const focusSpy = vi.spyOn(enabledButton, 'focus');
98
+
99
+ focusFirstFocusableElement(container);
100
+
101
+ expect(focusSpy).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ test('ignores hidden inputs', () => {
105
+ container.innerHTML = '<input type="hidden" /><input type="text" />';
106
+ const textInput = container.querySelector('input[type="text"]') as HTMLElement;
107
+ const focusSpy = vi.spyOn(textInput, 'focus');
108
+
109
+ focusFirstFocusableElement(container);
110
+
111
+ expect(focusSpy).toHaveBeenCalledTimes(1);
112
+ });
113
+
114
+ test('ignores elements with tabindex="-1"', () => {
115
+ container.innerHTML = '<div tabindex="-1">Not focusable</div><div tabindex="0">Focusable</div>';
116
+ const focusableDiv = container.querySelector('[tabindex="0"]') as HTMLElement;
117
+ const focusSpy = vi.spyOn(focusableDiv, 'focus');
118
+
119
+ focusFirstFocusableElement(container);
120
+
121
+ expect(focusSpy).toHaveBeenCalledTimes(1);
122
+ });
123
+
124
+ test('focuses the first focusable element when multiple types exist', () => {
125
+ container.innerHTML = '<button>Button</button><input type="text" /><a href="#">Link</a>';
126
+ const firstButton = container.querySelector('button') as HTMLElement;
127
+ const focusSpy = vi.spyOn(firstButton, 'focus');
128
+
129
+ focusFirstFocusableElement(container);
130
+
131
+ expect(focusSpy).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ test('focuses the first focusable element in nested structure', () => {
135
+ container.innerHTML = `
136
+ <div>
137
+ <div>Non-focusable</div>
138
+ <button>Nested Button</button>
139
+ </div>
140
+ <button>Outer Button</button>
141
+ `;
142
+ const nestedButton = container.querySelector('button') as HTMLElement;
143
+ const focusSpy = vi.spyOn(nestedButton, 'focus');
144
+
145
+ focusFirstFocusableElement(container);
146
+
147
+ expect(focusSpy).toHaveBeenCalledTimes(1);
148
+ expect(nestedButton.textContent).toBe('Nested Button');
149
+ });
150
+ });
@@ -0,0 +1,21 @@
1
+ const focusableElementsQuery = 'button:enabled, [href], input:not(input[type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';
2
+
3
+ export const focusFirstFocusableElement = (
4
+ containerElement: HTMLElement | null,
5
+ ) => {
6
+ if (
7
+ containerElement
8
+ && typeof containerElement.querySelector === 'function'
9
+ ) {
10
+ const firstFocusableElement = containerElement.querySelector(
11
+ focusableElementsQuery,
12
+ ) as HTMLElement;
13
+
14
+ if (
15
+ firstFocusableElement
16
+ && typeof firstFocusableElement.focus === 'function'
17
+ ) {
18
+ firstFocusableElement.focus();
19
+ }
20
+ }
21
+ };
@@ -1,2 +0,0 @@
1
- export declare const tableTheme: import("ag-grid-enterprise").Theme<import("ag-grid-enterprise").ThemeDefaultParams>;
2
- //# sourceMappingURL=tableTheme.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tableTheme.d.ts","sourceRoot":"","sources":["../../../src/components/table/tableTheme.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU,qFAerB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"tableTheme.js","sourceRoot":"","sources":["../../../src/components/table/tableTheme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC;IAC/C,WAAW,EAAE,wBAAwB;IACrC,eAAe,EAAE,kDAAkD;IACnE,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,4BAA4B;IAC1C,oBAAoB,EAAE,6BAA6B;IACnD,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,6BAA6B;IACzC,QAAQ,EAAE,yBAAyB;IACnC,SAAS,EAAE,kCAAkC;IAC7C,cAAc,EAAE,yBAAyB;IACzC,gBAAgB,EAAE,8BAA8B;IAChD,sBAAsB,EAAE,4BAA4B;IACpD,qBAAqB,EAAE,iDAAiD;IACxE,YAAY,EAAE,2CAA2C;CAC1D,CAAC,CAAC"}