@arbor-education/design-system.components 0.18.0 → 0.19.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.
- package/CHANGELOG.md +6 -0
- package/dist/components/combobox/useComboboxListboxKeyboard.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxListboxKeyboard.js +2 -0
- package/dist/components/combobox/useComboboxListboxKeyboard.js.map +1 -1
- package/dist/components/combobox/useComboboxListboxKeyboard.test.js +1 -0
- package/dist/components/combobox/useComboboxListboxKeyboard.test.js.map +1 -1
- package/dist/components/table/Table.d.ts +7 -0
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +3 -0
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +2 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +148 -2
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.d.ts +13 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.js +72 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.js.map +1 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.d.ts +2 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js +324 -0
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js.map +1 -0
- package/dist/index.css +3 -0
- package/dist/index.css.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox/useComboboxListboxKeyboard.test.tsx +1 -0
- package/src/components/combobox/useComboboxListboxKeyboard.ts +2 -0
- package/src/components/table/Table.stories.tsx +180 -2
- package/src/components/table/Table.tsx +3 -0
- package/src/components/table/cellRenderers/ComboboxCellRenderer.test.tsx +437 -0
- package/src/components/table/cellRenderers/ComboboxCellRenderer.tsx +135 -0
- package/src/components/table/table.scss +4 -0
|
@@ -1567,7 +1567,7 @@ const marksheetTidyTableColumnDefs: (ColDef | ColGroupDef)[] = [
|
|
|
1567
1567
|
{
|
|
1568
1568
|
headerName: 'Assessment Component',
|
|
1569
1569
|
field: 'assessmentComponent',
|
|
1570
|
-
cellRenderer: '
|
|
1570
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
1571
1571
|
cellRendererParams: (params: CustomCellRendererProps) => {
|
|
1572
1572
|
return {
|
|
1573
1573
|
options: assessmentComponentOptions,
|
|
@@ -1576,13 +1576,15 @@ const marksheetTidyTableColumnDefs: (ColDef | ColGroupDef)[] = [
|
|
|
1576
1576
|
};
|
|
1577
1577
|
},
|
|
1578
1578
|
editable: false,
|
|
1579
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
1579
1580
|
},
|
|
1580
1581
|
{
|
|
1581
1582
|
headerName: 'Visibility',
|
|
1582
1583
|
field: 'visibility',
|
|
1583
|
-
cellRenderer: '
|
|
1584
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
1584
1585
|
cellRendererParams: { options: visibilityOptions, placeholder: 'Select' },
|
|
1585
1586
|
editable: false,
|
|
1587
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
1586
1588
|
},
|
|
1587
1589
|
{
|
|
1588
1590
|
headerName: 'Editable',
|
|
@@ -2226,4 +2228,180 @@ export const WithCheckboxCellRenderer: Story = {
|
|
|
2226
2228
|
},
|
|
2227
2229
|
};
|
|
2228
2230
|
|
|
2231
|
+
interface SelectDropdownRowData {
|
|
2232
|
+
id: number;
|
|
2233
|
+
name: string;
|
|
2234
|
+
status: string | null;
|
|
2235
|
+
priority: string | null;
|
|
2236
|
+
category: string | null;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const statusOptions = [
|
|
2240
|
+
{ value: 'active', label: 'Active' },
|
|
2241
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
2242
|
+
{ value: 'pending', label: 'Pending' },
|
|
2243
|
+
];
|
|
2244
|
+
|
|
2245
|
+
const priorityOptions = [
|
|
2246
|
+
{ value: 'high', label: 'High' },
|
|
2247
|
+
{ value: 'medium', label: 'Medium' },
|
|
2248
|
+
{ value: 'low', label: 'Low' },
|
|
2249
|
+
];
|
|
2250
|
+
|
|
2251
|
+
const categoryOptions = [
|
|
2252
|
+
{ value: 'english', label: 'English' },
|
|
2253
|
+
{ value: 'maths', label: 'Maths' },
|
|
2254
|
+
{ value: 'science', label: 'Science' },
|
|
2255
|
+
{ value: 'history', label: 'History' },
|
|
2256
|
+
];
|
|
2257
|
+
|
|
2258
|
+
const selectDropdownSampleData: SelectDropdownRowData[] = [
|
|
2259
|
+
{ id: 1, name: 'Alice Johnson', status: 'active', priority: 'high', category: 'english' },
|
|
2260
|
+
{ id: 2, name: 'Bob Smith', status: 'inactive', priority: null, category: 'maths' },
|
|
2261
|
+
{ id: 3, name: 'Charlie Brown', status: null, priority: 'medium', category: null },
|
|
2262
|
+
{ id: 4, name: 'Diana Prince', status: 'pending', priority: 'low', category: 'science' },
|
|
2263
|
+
{ id: 5, name: 'Eve Wilson', status: 'active', priority: null, category: 'history' },
|
|
2264
|
+
];
|
|
2265
|
+
|
|
2266
|
+
const selectDropdownColDefs: (ColDef | ColGroupDef)[] = [
|
|
2267
|
+
{ field: 'name', headerName: 'Student Name', editable: false },
|
|
2268
|
+
{
|
|
2269
|
+
field: 'status',
|
|
2270
|
+
headerName: 'Status',
|
|
2271
|
+
cellRenderer: 'dsSelectDropdownCellRenderer',
|
|
2272
|
+
cellRendererParams: { options: statusOptions, placeholder: 'Select status' },
|
|
2273
|
+
editable: false,
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
field: 'priority',
|
|
2277
|
+
headerName: 'Priority',
|
|
2278
|
+
cellRenderer: 'dsSelectDropdownCellRenderer',
|
|
2279
|
+
cellRendererParams: { options: priorityOptions, placeholder: 'Select priority' },
|
|
2280
|
+
editable: false,
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
field: 'category',
|
|
2284
|
+
headerName: 'Category',
|
|
2285
|
+
cellRenderer: 'dsSelectDropdownCellRenderer',
|
|
2286
|
+
cellRendererParams: { options: categoryOptions, placeholder: 'Select category' },
|
|
2287
|
+
editable: false,
|
|
2288
|
+
},
|
|
2289
|
+
];
|
|
2290
|
+
|
|
2291
|
+
export const WithSelectDropdownCellRenderer: Story = {
|
|
2292
|
+
parameters: {
|
|
2293
|
+
docs: {
|
|
2294
|
+
description: {
|
|
2295
|
+
story:
|
|
2296
|
+
'Columns can use the `dsSelectDropdownCellRenderer` cell renderer to display a dropdown select inside the cell. Click or press Enter on a cell to open the dropdown.',
|
|
2297
|
+
},
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2300
|
+
args: {
|
|
2301
|
+
rowData: selectDropdownSampleData,
|
|
2302
|
+
columnDefs: selectDropdownColDefs,
|
|
2303
|
+
defaultColDef,
|
|
2304
|
+
domLayout: 'autoHeight',
|
|
2305
|
+
},
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
interface ComboboxRowData {
|
|
2309
|
+
id: number;
|
|
2310
|
+
name: string;
|
|
2311
|
+
subject: string | null;
|
|
2312
|
+
level: string | null;
|
|
2313
|
+
tags: string[];
|
|
2314
|
+
skills: string[];
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
const subjectOptions = [
|
|
2318
|
+
{ value: 'english-language', label: 'English Language' },
|
|
2319
|
+
{ value: 'english-literature', label: 'English Literature' },
|
|
2320
|
+
{ value: 'maths', label: 'Maths' },
|
|
2321
|
+
{ value: 'science', label: 'Science' },
|
|
2322
|
+
{ value: 'history', label: 'History' },
|
|
2323
|
+
];
|
|
2324
|
+
|
|
2325
|
+
const levelOptions = [
|
|
2326
|
+
{ value: 'foundation', label: 'Foundation' },
|
|
2327
|
+
{ value: 'intermediate', label: 'Intermediate' },
|
|
2328
|
+
{ value: 'higher', label: 'Higher' },
|
|
2329
|
+
];
|
|
2330
|
+
|
|
2331
|
+
const tagOptions = [
|
|
2332
|
+
{ value: 'coursework', label: 'Coursework' },
|
|
2333
|
+
{ value: 'exam', label: 'Exam' },
|
|
2334
|
+
{ value: 'practical', label: 'Practical' },
|
|
2335
|
+
{ value: 'oral', label: 'Oral' },
|
|
2336
|
+
];
|
|
2337
|
+
|
|
2338
|
+
const skillOptions = [
|
|
2339
|
+
{ value: 'reading', label: 'Reading' },
|
|
2340
|
+
{ value: 'writing', label: 'Writing' },
|
|
2341
|
+
{ value: 'speaking', label: 'Speaking' },
|
|
2342
|
+
{ value: 'listening', label: 'Listening' },
|
|
2343
|
+
{ value: 'critical-thinking', label: 'Critical Thinking' },
|
|
2344
|
+
];
|
|
2345
|
+
|
|
2346
|
+
const comboboxSampleData: ComboboxRowData[] = [
|
|
2347
|
+
{ id: 1, name: 'Alice Johnson', subject: 'english-language', level: 'higher', tags: ['coursework', 'exam'], skills: ['reading', 'writing', 'speaking'] },
|
|
2348
|
+
{ id: 2, name: 'Bob Smith', subject: 'maths', level: null, tags: ['exam'], skills: ['critical-thinking'] },
|
|
2349
|
+
{ id: 3, name: 'Charlie Brown', subject: null, level: 'foundation', tags: [], skills: [] },
|
|
2350
|
+
{ id: 4, name: 'Diana Prince', subject: 'english-literature', level: 'intermediate', tags: ['coursework', 'practical', 'oral'], skills: ['reading', 'writing', 'listening', 'speaking', 'critical-thinking'] },
|
|
2351
|
+
{ id: 5, name: 'Eve Wilson', subject: 'science', level: null, tags: [], skills: ['listening'] },
|
|
2352
|
+
];
|
|
2353
|
+
|
|
2354
|
+
const comboboxColDefs: (ColDef | ColGroupDef)[] = [
|
|
2355
|
+
{ field: 'name', headerName: 'Student Name', editable: false },
|
|
2356
|
+
{
|
|
2357
|
+
field: 'subject',
|
|
2358
|
+
headerName: 'Subject (single, input)',
|
|
2359
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
2360
|
+
cellRendererParams: { options: subjectOptions, placeholder: 'Select subject' },
|
|
2361
|
+
editable: false,
|
|
2362
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
field: 'level',
|
|
2366
|
+
headerName: 'Level (single, button)',
|
|
2367
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
2368
|
+
cellRendererParams: { options: levelOptions, placeholder: 'Select level', triggerVariant: 'button' },
|
|
2369
|
+
editable: false,
|
|
2370
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
field: 'tags',
|
|
2374
|
+
headerName: 'Tags (multi, button)',
|
|
2375
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
2376
|
+
cellRendererParams: { options: tagOptions, placeholder: 'Select tags', multiple: true, triggerVariant: 'button' },
|
|
2377
|
+
editable: false,
|
|
2378
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
field: 'skills',
|
|
2382
|
+
headerName: 'Skills (multi, input)',
|
|
2383
|
+
cellRenderer: 'dsComboboxCellRenderer',
|
|
2384
|
+
cellRendererParams: { options: skillOptions, placeholder: 'Select skills', multiple: true },
|
|
2385
|
+
editable: false,
|
|
2386
|
+
...Table.ComboboxCellRenderer.colDefDefaults,
|
|
2387
|
+
},
|
|
2388
|
+
];
|
|
2389
|
+
|
|
2390
|
+
export const WithComboboxCellRenderer: Story = {
|
|
2391
|
+
parameters: {
|
|
2392
|
+
docs: {
|
|
2393
|
+
description: {
|
|
2394
|
+
story:
|
|
2395
|
+
'Columns can use the `dsComboboxCellRenderer` cell renderer to display a searchable combobox inside the cell. Supports single and multi-select via the `multiple` prop. Row height adjusts automatically when chips wrap.',
|
|
2396
|
+
},
|
|
2397
|
+
},
|
|
2398
|
+
},
|
|
2399
|
+
args: {
|
|
2400
|
+
rowData: comboboxSampleData,
|
|
2401
|
+
columnDefs: comboboxColDefs,
|
|
2402
|
+
defaultColDef,
|
|
2403
|
+
domLayout: 'autoHeight',
|
|
2404
|
+
},
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2229
2407
|
export default meta;
|
|
@@ -33,6 +33,7 @@ import { TABLE_SPACING } from './tableConsts';
|
|
|
33
33
|
import { TableSettingsContext } from './TableSettingsContext';
|
|
34
34
|
import { BooleanCellRenderer } from './cellRenderers/BooleanCellRenderer';
|
|
35
35
|
import { CheckboxCellRenderer } from './cellRenderers/CheckboxCellRenderer';
|
|
36
|
+
import { ComboboxCellRenderer } from './cellRenderers/ComboboxCellRenderer';
|
|
36
37
|
import { DefaultCellRenderer } from './cellRenderers/DefaultCellRenderer';
|
|
37
38
|
|
|
38
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -198,6 +199,7 @@ export const Table = (props: TableProps) => {
|
|
|
198
199
|
dsDateCellEditor: DateCellEditor,
|
|
199
200
|
dsCheckboxCellRenderer: CheckboxCellRenderer,
|
|
200
201
|
dsBooleanCellRenderer: BooleanCellRenderer,
|
|
202
|
+
dsComboboxCellRenderer: ComboboxCellRenderer,
|
|
201
203
|
...components,
|
|
202
204
|
}}
|
|
203
205
|
{...rest}
|
|
@@ -229,6 +231,7 @@ Table.DateCellEditor = DateCellEditor;
|
|
|
229
231
|
Table.DefaultColDef = DSDefaultColDef;
|
|
230
232
|
Table.GridApiContext = GridApiContext;
|
|
231
233
|
Table.CheckboxCellRenderer = CheckboxCellRenderer;
|
|
234
|
+
Table.ComboboxCellRenderer = ComboboxCellRenderer;
|
|
232
235
|
Table.DefaultValueFormatter = defaultValueFormatter;
|
|
233
236
|
Table.TableSettingsDropdown = TableSettingsDropdown;
|
|
234
237
|
Table.TableControls = TableControls;
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { describe, expect, test, vi, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import type { SuppressKeyboardEventParams } from 'ag-grid-community';
|
|
6
|
+
import type { CustomCellRendererProps } from 'ag-grid-react';
|
|
7
|
+
import { ComboboxCellRenderer } from './ComboboxCellRenderer';
|
|
8
|
+
import type { ComboboxOption, ComboboxProps } from 'Components/combobox/types';
|
|
9
|
+
|
|
10
|
+
const options: ComboboxOption[] = [
|
|
11
|
+
{ value: 'opt1', label: 'Option 1' },
|
|
12
|
+
{ value: 'opt2', label: 'Option 2' },
|
|
13
|
+
{ value: 'opt3', label: 'Option 3' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
type MockProps = Partial<CustomCellRendererProps> & Partial<Omit<ComboboxProps, 'value'>>;
|
|
17
|
+
|
|
18
|
+
const createMockProps = (overrides: MockProps = {}): CustomCellRendererProps & ComboboxProps => ({
|
|
19
|
+
value: null,
|
|
20
|
+
node: { setDataValue: vi.fn() },
|
|
21
|
+
column: 'testField',
|
|
22
|
+
api: {
|
|
23
|
+
addEventListener: vi.fn(),
|
|
24
|
+
removeEventListener: vi.fn(),
|
|
25
|
+
},
|
|
26
|
+
options,
|
|
27
|
+
...overrides,
|
|
28
|
+
} as unknown as CustomCellRendererProps & ComboboxProps);
|
|
29
|
+
|
|
30
|
+
describe('ComboboxCellRenderer', () => {
|
|
31
|
+
test('renders a combobox input', () => {
|
|
32
|
+
render(<ComboboxCellRenderer {...createMockProps()} />);
|
|
33
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('renders with placeholder', () => {
|
|
37
|
+
render(<ComboboxCellRenderer {...createMockProps({ placeholder: 'Choose an option' })} />);
|
|
38
|
+
expect(screen.getByPlaceholderText('Choose an option')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('renders with default placeholder when none provided', () => {
|
|
42
|
+
render(<ComboboxCellRenderer {...createMockProps()} />);
|
|
43
|
+
expect(screen.getByPlaceholderText('Select...')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('opens listbox and shows options on input focus', async () => {
|
|
47
|
+
render(<ComboboxCellRenderer {...createMockProps()} />);
|
|
48
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
49
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('calls setDataValue with the selected value for single select', async () => {
|
|
54
|
+
const setDataValue = vi.fn();
|
|
55
|
+
const props = createMockProps({
|
|
56
|
+
node: { setDataValue } as unknown as CustomCellRendererProps['node'],
|
|
57
|
+
column: 'testField' as unknown as CustomCellRendererProps['column'],
|
|
58
|
+
});
|
|
59
|
+
render(<ComboboxCellRenderer {...props} />);
|
|
60
|
+
|
|
61
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
62
|
+
await userEvent.click(screen.getByText('Option 1'));
|
|
63
|
+
|
|
64
|
+
expect(setDataValue).toHaveBeenCalledWith('testField', 'opt1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('calls setDataValue with array for multiple select', async () => {
|
|
68
|
+
const setDataValue = vi.fn();
|
|
69
|
+
const props = createMockProps({
|
|
70
|
+
multiple: true,
|
|
71
|
+
node: { setDataValue } as unknown as CustomCellRendererProps['node'],
|
|
72
|
+
column: 'testField' as unknown as CustomCellRendererProps['column'],
|
|
73
|
+
});
|
|
74
|
+
render(<ComboboxCellRenderer {...props} />);
|
|
75
|
+
|
|
76
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
77
|
+
await userEvent.click(screen.getByText('Option 1'));
|
|
78
|
+
|
|
79
|
+
expect(setDataValue).toHaveBeenCalledWith('testField', ['opt1']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('does not call setDataValue when column is undefined', async () => {
|
|
83
|
+
const setDataValue = vi.fn();
|
|
84
|
+
const props = createMockProps({
|
|
85
|
+
node: { setDataValue } as unknown as CustomCellRendererProps['node'],
|
|
86
|
+
column: undefined as unknown as CustomCellRendererProps['column'],
|
|
87
|
+
});
|
|
88
|
+
render(<ComboboxCellRenderer {...props} />);
|
|
89
|
+
|
|
90
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
91
|
+
await userEvent.click(screen.getByText('Option 1'));
|
|
92
|
+
|
|
93
|
+
expect(setDataValue).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('normalizes a string cell value to an array for controlled combobox', () => {
|
|
97
|
+
render(<ComboboxCellRenderer {...createMockProps({ value: 'opt1' })} />);
|
|
98
|
+
const input = screen.getByRole('combobox');
|
|
99
|
+
expect(input).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('normalizes a string[] cell value to an array for controlled combobox', () => {
|
|
103
|
+
render(<ComboboxCellRenderer {...createMockProps({ value: ['opt1', 'opt2'], multiple: true })} />);
|
|
104
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('registers cellKeyDown listener on mount', () => {
|
|
108
|
+
const addEventListener = vi.fn();
|
|
109
|
+
const removeEventListener = vi.fn();
|
|
110
|
+
const props = createMockProps({
|
|
111
|
+
api: { addEventListener, removeEventListener } as unknown as CustomCellRendererProps['api'],
|
|
112
|
+
});
|
|
113
|
+
render(<ComboboxCellRenderer {...props} />);
|
|
114
|
+
expect(addEventListener).toHaveBeenCalledWith('cellKeyDown', expect.any(Function));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('value normalization edge cases', () => {
|
|
118
|
+
test('treats empty string value as no selection', () => {
|
|
119
|
+
render(<ComboboxCellRenderer {...createMockProps({ value: '' })} />);
|
|
120
|
+
expect(screen.getByPlaceholderText('Select...')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('normalizes a numeric cell value to a string', () => {
|
|
124
|
+
render(<ComboboxCellRenderer {...createMockProps({ value: 42 as unknown as string })} />);
|
|
125
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('normalizes a numeric array cell value to strings', () => {
|
|
129
|
+
render(<ComboboxCellRenderer {...createMockProps({ value: [1, 2] as unknown as string[], multiple: true })} />);
|
|
130
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('cellKeyDown handler', () => {
|
|
135
|
+
test('removes the cellKeyDown listener on unmount', () => {
|
|
136
|
+
const addEventListener = vi.fn();
|
|
137
|
+
const removeEventListener = vi.fn();
|
|
138
|
+
const { unmount } = render(
|
|
139
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
140
|
+
api: { addEventListener, removeEventListener } as unknown as CustomCellRendererProps['api'],
|
|
141
|
+
})}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
145
|
+
unmount();
|
|
146
|
+
expect(removeEventListener).toHaveBeenCalledWith('cellKeyDown', listener);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('focuses the combobox trigger on Enter for matching node and column', () => {
|
|
150
|
+
const addEventListener = vi.fn();
|
|
151
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
152
|
+
const mockColumn = 'testField';
|
|
153
|
+
|
|
154
|
+
render(
|
|
155
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
156
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
157
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
158
|
+
api: { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
159
|
+
})}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const trigger = screen.getByRole('combobox');
|
|
164
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
165
|
+
|
|
166
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
167
|
+
listener({ event: { key: 'Enter' }, node: mockNode, column: mockColumn });
|
|
168
|
+
|
|
169
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('focuses and clicks the button trigger on Enter when triggerVariant is "button"', () => {
|
|
173
|
+
const addEventListener = vi.fn();
|
|
174
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
175
|
+
const mockColumn = 'testField';
|
|
176
|
+
|
|
177
|
+
render(
|
|
178
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
179
|
+
'node': mockNode as unknown as CustomCellRendererProps['node'],
|
|
180
|
+
'column': mockColumn as unknown as CustomCellRendererProps['column'],
|
|
181
|
+
'api': { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
182
|
+
'triggerVariant': 'button',
|
|
183
|
+
'aria-label': 'Choose option',
|
|
184
|
+
})}
|
|
185
|
+
/>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const button = screen.getByRole('button', { name: 'Choose option' });
|
|
189
|
+
const focusSpy = vi.spyOn(button, 'focus');
|
|
190
|
+
const clickSpy = vi.spyOn(button, 'click');
|
|
191
|
+
|
|
192
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
193
|
+
listener({ event: { key: 'Enter' }, node: mockNode, column: mockColumn });
|
|
194
|
+
|
|
195
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
196
|
+
expect(clickSpy).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('does not focus trigger when the event node does not match', () => {
|
|
200
|
+
const addEventListener = vi.fn();
|
|
201
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
202
|
+
const differentNode = { setDataValue: vi.fn() };
|
|
203
|
+
const mockColumn = 'testField';
|
|
204
|
+
|
|
205
|
+
render(
|
|
206
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
207
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
208
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
209
|
+
api: { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
210
|
+
})}
|
|
211
|
+
/>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const trigger = screen.getByRole('combobox');
|
|
215
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
216
|
+
|
|
217
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
218
|
+
listener({ event: { key: 'Enter' }, node: differentNode, column: mockColumn });
|
|
219
|
+
|
|
220
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('does not focus trigger when the event column does not match', () => {
|
|
224
|
+
const addEventListener = vi.fn();
|
|
225
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
226
|
+
const mockColumn = 'testField';
|
|
227
|
+
|
|
228
|
+
render(
|
|
229
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
230
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
231
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
232
|
+
api: { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
233
|
+
})}
|
|
234
|
+
/>,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const trigger = screen.getByRole('combobox');
|
|
238
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
239
|
+
|
|
240
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
241
|
+
listener({ event: { key: 'Enter' }, node: mockNode, column: 'differentField' });
|
|
242
|
+
|
|
243
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('does not focus trigger for non-Enter keys', () => {
|
|
247
|
+
const addEventListener = vi.fn();
|
|
248
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
249
|
+
const mockColumn = 'testField';
|
|
250
|
+
|
|
251
|
+
render(
|
|
252
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
253
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
254
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
255
|
+
api: { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
256
|
+
})}
|
|
257
|
+
/>,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const trigger = screen.getByRole('combobox');
|
|
261
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
262
|
+
|
|
263
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
264
|
+
listener({ event: { key: 'ArrowDown' }, node: mockNode, column: mockColumn });
|
|
265
|
+
|
|
266
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('does not focus trigger for a FullWidthCellKeyDownEvent (no column property)', () => {
|
|
270
|
+
const addEventListener = vi.fn();
|
|
271
|
+
const mockNode = { setDataValue: vi.fn() };
|
|
272
|
+
const mockColumn = 'testField';
|
|
273
|
+
|
|
274
|
+
render(
|
|
275
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
276
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
277
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
278
|
+
api: { addEventListener, removeEventListener: vi.fn() } as unknown as CustomCellRendererProps['api'],
|
|
279
|
+
})}
|
|
280
|
+
/>,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const trigger = screen.getByRole('combobox');
|
|
284
|
+
const focusSpy = vi.spyOn(trigger, 'focus');
|
|
285
|
+
|
|
286
|
+
const listener = addEventListener.mock.calls[0]![1];
|
|
287
|
+
listener({ event: { key: 'Enter' }, node: mockNode });
|
|
288
|
+
|
|
289
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('focus restoration via MutationObserver', () => {
|
|
294
|
+
test('calls setFocusedCell when popover closes without focus leaving the cell', async () => {
|
|
295
|
+
const setFocusedCell = vi.fn();
|
|
296
|
+
const mockNode = { setDataValue: vi.fn(), rowIndex: 5 };
|
|
297
|
+
const mockColumn = 'testField';
|
|
298
|
+
|
|
299
|
+
render(
|
|
300
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
301
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
302
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
303
|
+
api: {
|
|
304
|
+
addEventListener: vi.fn(),
|
|
305
|
+
removeEventListener: vi.fn(),
|
|
306
|
+
setFocusedCell,
|
|
307
|
+
} as unknown as CustomCellRendererProps['api'],
|
|
308
|
+
})}
|
|
309
|
+
/>,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
313
|
+
await userEvent.keyboard('{Escape}');
|
|
314
|
+
|
|
315
|
+
expect(setFocusedCell).toHaveBeenCalledWith(5, mockColumn);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('does not call setFocusedCell when focus moves outside the cell', async () => {
|
|
319
|
+
const setFocusedCell = vi.fn();
|
|
320
|
+
const mockNode = { setDataValue: vi.fn(), rowIndex: 5 };
|
|
321
|
+
const mockColumn = 'testField';
|
|
322
|
+
|
|
323
|
+
render(
|
|
324
|
+
<>
|
|
325
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
326
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
327
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
328
|
+
api: {
|
|
329
|
+
addEventListener: vi.fn(),
|
|
330
|
+
removeEventListener: vi.fn(),
|
|
331
|
+
setFocusedCell,
|
|
332
|
+
} as unknown as CustomCellRendererProps['api'],
|
|
333
|
+
})}
|
|
334
|
+
/>
|
|
335
|
+
<button>Outside</button>
|
|
336
|
+
</>,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
340
|
+
await userEvent.click(screen.getByRole('button', { name: 'Outside' }));
|
|
341
|
+
|
|
342
|
+
expect(setFocusedCell).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('does not call setFocusedCell when node.rowIndex is null', async () => {
|
|
346
|
+
const setFocusedCell = vi.fn();
|
|
347
|
+
const mockNode = { setDataValue: vi.fn(), rowIndex: null };
|
|
348
|
+
const mockColumn = 'testField';
|
|
349
|
+
|
|
350
|
+
render(
|
|
351
|
+
<ComboboxCellRenderer {...createMockProps({
|
|
352
|
+
node: mockNode as unknown as CustomCellRendererProps['node'],
|
|
353
|
+
column: mockColumn as unknown as CustomCellRendererProps['column'],
|
|
354
|
+
api: {
|
|
355
|
+
addEventListener: vi.fn(),
|
|
356
|
+
removeEventListener: vi.fn(),
|
|
357
|
+
setFocusedCell,
|
|
358
|
+
} as unknown as CustomCellRendererProps['api'],
|
|
359
|
+
})}
|
|
360
|
+
/>,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
364
|
+
await userEvent.keyboard('{Escape}');
|
|
365
|
+
|
|
366
|
+
expect(setFocusedCell).not.toHaveBeenCalled();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('disconnects the MutationObserver on unmount', () => {
|
|
370
|
+
const disconnect = vi.fn();
|
|
371
|
+
vi.stubGlobal('MutationObserver', vi.fn(() => ({ observe: vi.fn(), disconnect })));
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const { unmount } = render(<ComboboxCellRenderer {...createMockProps()} />);
|
|
375
|
+
unmount();
|
|
376
|
+
expect(disconnect).toHaveBeenCalled();
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
vi.unstubAllGlobals();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('colDefDefaults', () => {
|
|
385
|
+
test('autoHeight is true', () => {
|
|
386
|
+
expect(ComboboxCellRenderer.colDefDefaults.autoHeight).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('suppressKeyboardEvent', () => {
|
|
390
|
+
const { suppressKeyboardEvent } = ComboboxCellRenderer.colDefDefaults;
|
|
391
|
+
|
|
392
|
+
const appendCell = (key: string, open: boolean) => {
|
|
393
|
+
const cell = document.createElement('div');
|
|
394
|
+
cell.className = 'ag-cell';
|
|
395
|
+
const trigger = document.createElement('input');
|
|
396
|
+
trigger.setAttribute('role', 'combobox');
|
|
397
|
+
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
398
|
+
cell.appendChild(trigger);
|
|
399
|
+
document.body.appendChild(cell);
|
|
400
|
+
return {
|
|
401
|
+
params: { event: { key, target: trigger } } as unknown as SuppressKeyboardEventParams,
|
|
402
|
+
cleanup: () => cell.remove(),
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
afterEach(() => {
|
|
407
|
+
document.querySelectorAll('.ag-cell').forEach(el => el.remove());
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('returns false when the event target has no ag-cell ancestor', () => {
|
|
411
|
+
const target = document.createElement('input');
|
|
412
|
+
document.body.appendChild(target);
|
|
413
|
+
const params = { event: { key: 'ArrowDown', target } } as unknown as SuppressKeyboardEventParams;
|
|
414
|
+
expect(suppressKeyboardEvent(params)).toBe(false);
|
|
415
|
+
target.remove();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('returns false when the combobox is closed', () => {
|
|
419
|
+
const { params } = appendCell('ArrowDown', false);
|
|
420
|
+
expect(suppressKeyboardEvent(params)).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test.each(['ArrowUp', 'ArrowDown', 'Enter', 'Escape'])(
|
|
424
|
+
'suppresses %s when the combobox is open',
|
|
425
|
+
(key) => {
|
|
426
|
+
const { params } = appendCell(key, true);
|
|
427
|
+
expect(suppressKeyboardEvent(params)).toBe(true);
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
test('does not suppress Tab when the combobox is open', () => {
|
|
432
|
+
const { params } = appendCell('Tab', true);
|
|
433
|
+
expect(suppressKeyboardEvent(params)).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|