@arbor-education/design-system.components 0.25.6 → 0.25.8

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 (63) hide show
  1. package/CHANGELOG.md +16 -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 +2 -2
  5. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  6. package/dist/components/row/Row.d.ts +2 -0
  7. package/dist/components/row/Row.d.ts.map +1 -1
  8. package/dist/components/row/Row.js +14 -5
  9. package/dist/components/row/Row.js.map +1 -1
  10. package/dist/components/row/Row.stories.d.ts +27 -0
  11. package/dist/components/row/Row.stories.d.ts.map +1 -1
  12. package/dist/components/row/Row.stories.js +101 -0
  13. package/dist/components/row/Row.stories.js.map +1 -1
  14. package/dist/components/row/Row.test.d.ts.map +1 -1
  15. package/dist/components/row/Row.test.js +39 -1
  16. package/dist/components/row/Row.test.js.map +1 -1
  17. package/dist/components/table/Table.d.ts +6 -0
  18. package/dist/components/table/Table.d.ts.map +1 -1
  19. package/dist/components/table/Table.js +3 -0
  20. package/dist/components/table/Table.js.map +1 -1
  21. package/dist/components/table/Table.stories.d.ts +1 -0
  22. package/dist/components/table/Table.stories.d.ts.map +1 -1
  23. package/dist/components/table/Table.stories.js +41 -0
  24. package/dist/components/table/Table.stories.js.map +1 -1
  25. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts +10 -0
  26. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts.map +1 -0
  27. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js +13 -0
  28. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js.map +1 -0
  29. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts +90 -0
  30. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts.map +1 -0
  31. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js +243 -0
  32. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js.map +1 -0
  33. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts +2 -0
  34. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts.map +1 -0
  35. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js +72 -0
  36. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js.map +1 -0
  37. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
  38. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +2 -2
  39. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
  40. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +13 -0
  41. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  42. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +56 -0
  43. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  44. package/dist/index.css +8 -0
  45. package/dist/index.css.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +3 -1
  52. package/src/components/row/Row.stories.tsx +141 -0
  53. package/src/components/row/Row.test.tsx +54 -1
  54. package/src/components/row/Row.tsx +29 -10
  55. package/src/components/row/row.scss +10 -0
  56. package/src/components/table/Table.stories.tsx +45 -0
  57. package/src/components/table/Table.tsx +3 -0
  58. package/src/components/table/cellRenderers/NumberInputCellRenderer.stories.tsx +346 -0
  59. package/src/components/table/cellRenderers/NumberInputCellRenderer.test.tsx +99 -0
  60. package/src/components/table/cellRenderers/NumberInputCellRenderer.tsx +36 -0
  61. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +67 -0
  62. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +2 -0
  63. package/src/index.ts +1 -0
@@ -0,0 +1,346 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import {
4
+ Controls,
5
+ Heading as DocHeading,
6
+ Markdown,
7
+ Primary as DocPrimary,
8
+ Stories,
9
+ Subtitle,
10
+ Title,
11
+ } from '@storybook/addon-docs/blocks';
12
+ import type { CustomCellRendererProps } from 'ag-grid-react';
13
+ import { NumberInputCellRenderer } from './NumberInputCellRenderer.js';
14
+ import { Table } from 'Components/table/Table';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Docs page content
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const DESCRIPTION_INTRO = [
21
+ '`NumberInputCellRenderer` is an AG Grid cell renderer that renders an editable Arbor `NumberInput`',
22
+ 'permanently inside a table cell. On change, it calls `node.setDataValue` to update the row data in-place.',
23
+ '',
24
+ 'Register it via the string key `dsNumberInputCellRenderer` (pre-registered by Arbor\'s `Table`) or by importing the component directly.',
25
+ ].join('\n');
26
+
27
+ const USAGE_GUIDANCE = [
28
+ '### When to use',
29
+ '',
30
+ '- On numeric columns where the user edits a value directly in the table without opening a form',
31
+ '- Quantities, counts, scores, or any bounded numeric field that should be editable inline',
32
+ '',
33
+ '---',
34
+ '',
35
+ '### When NOT to use',
36
+ '',
37
+ '| Situation | Use instead |',
38
+ '|---|---|',
39
+ '| Display-only number (no editing) | Plain AG Grid value formatting or `DefaultCellRenderer` |',
40
+ '| The edit requires confirmation or a side effect | Handle in a modal; do not edit inline |',
41
+ '| Free-form decimal entry without step constraints | A form in a slideover |',
42
+ ].join('\n');
43
+
44
+ const DEVELOPER_NOTES = [
45
+ '### Critical usage patterns',
46
+ '',
47
+ '**`colDef.field` must be set for changes to persist.**',
48
+ 'On change, the renderer calls `node.setDataValue(colDef.field, parsedValue)`. If `field` is not set',
49
+ 'on the column definition, the change is silently ignored.',
50
+ '',
51
+ '**Non-finite input (empty or non-numeric) is coerced to `0`.**',
52
+ 'If the user clears the input, `setDataValue` is called with `0` rather than `NaN`.',
53
+ '',
54
+ '**Pass `min`, `max`, and `disabled` via `cellRendererParams`.**',
55
+ 'These are forwarded directly to `NumberInput`.',
56
+ '',
57
+ '```tsx',
58
+ "import { Table } from '@arbor-education/design-system.components';",
59
+ '',
60
+ 'const colDefs = [',
61
+ ' {',
62
+ " field: 'quantity',",
63
+ " headerName: 'Quantity',",
64
+ " cellRenderer: 'dsNumberInputCellRenderer',",
65
+ ' cellRendererParams: { min: 0, max: 100 },',
66
+ ' },',
67
+ '];',
68
+ '```',
69
+ '',
70
+ '---',
71
+ '',
72
+ '### TypeScript types',
73
+ '',
74
+ '```ts',
75
+ "import { NumberInputCellRenderer } from '@arbor-education/design-system.components';",
76
+ '```',
77
+ '',
78
+ '`NumberInputCellRenderer` accepts `CustomCellRendererProps` from AG Grid plus `value: number | null`,',
79
+ '`min?: number`, `max?: number`, and `disabled?: boolean` (passed via `cellRendererParams`).',
80
+ ].join('\n');
81
+
82
+ const RELATED_COMPONENTS = [
83
+ '## Related components',
84
+ '',
85
+ '[Table](?path=/docs/components-table--docs) · [NumberInput](?path=/docs/components-numberinput--docs) · [CheckboxCellRenderer](?path=/docs/components-table-cellrenderers-checkboxcellrenderer--docs)',
86
+ ].join('\n');
87
+
88
+ const PROPS_INTRO = 'The preview below uses a mock AG Grid row node. Type a value or use the +/− spinners — `node.setDataValue` is wired to local state in this preview; in a real table it updates the row data in-place.';
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Custom DocsPage
92
+ // ---------------------------------------------------------------------------
93
+
94
+ function NumberInputCellRendererDocsPage() {
95
+ return (
96
+ <>
97
+ <Title />
98
+ <Subtitle />
99
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
100
+ <DocHeading>Interactive example</DocHeading>
101
+ <Markdown>{PROPS_INTRO}</Markdown>
102
+ <DocPrimary />
103
+ <Controls />
104
+ <DocHeading>Usage guidance</DocHeading>
105
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
106
+ <DocHeading>Developer notes</DocHeading>
107
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
108
+ <DocHeading>Examples</DocHeading>
109
+ <Stories title="" />
110
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
111
+ </>
112
+ );
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Meta
117
+ // ---------------------------------------------------------------------------
118
+
119
+ const meta = {
120
+ title: 'Components/Table/CellRenderers/NumberInputCellRenderer',
121
+ component: NumberInputCellRenderer,
122
+ tags: ['autodocs'],
123
+ parameters: {
124
+ layout: 'padded',
125
+ docs: { page: NumberInputCellRendererDocsPage },
126
+ },
127
+ argTypes: {
128
+ value: {
129
+ control: false,
130
+ description: 'Current numeric value. `null` is rendered as `0`. On change, the parsed float is written back via `node.setDataValue`.',
131
+ table: {
132
+ type: { summary: 'number | null' },
133
+ defaultValue: { summary: 'null' },
134
+ },
135
+ },
136
+ min: {
137
+ control: false,
138
+ description: 'Minimum allowed value. The minus spinner is disabled when the value reaches this bound.',
139
+ table: { type: { summary: 'number' } },
140
+ },
141
+ max: {
142
+ control: false,
143
+ description: 'Maximum allowed value. The plus spinner is disabled when the value reaches this bound.',
144
+ table: { type: { summary: 'number' } },
145
+ },
146
+ disabled: {
147
+ control: false,
148
+ description: 'When `true`, the input and spinners are non-interactive.',
149
+ table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
150
+ },
151
+ node: {
152
+ control: false,
153
+ description: 'AG Grid row node. The renderer calls `node.setDataValue` on each value change.',
154
+ table: { type: { summary: 'IRowNode' } },
155
+ },
156
+ colDef: {
157
+ control: false,
158
+ description: '`colDef.field` is used as the key passed to `node.setDataValue`. Changes are silently ignored when `field` is absent.',
159
+ table: { type: { summary: 'ColDef' } },
160
+ },
161
+ },
162
+ } satisfies Meta<typeof NumberInputCellRenderer>;
163
+
164
+ export default meta;
165
+ type Story = StoryObj<typeof NumberInputCellRenderer>;
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Helper
169
+ // ---------------------------------------------------------------------------
170
+
171
+ const withDescription = (story: Story, description: string): Story => ({
172
+ ...story,
173
+ parameters: {
174
+ ...story.parameters,
175
+ docs: { ...story.parameters?.docs, description: { story: description } },
176
+ },
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Template components
181
+ // ---------------------------------------------------------------------------
182
+
183
+ const InteractiveTemplate = () => {
184
+ const [count, setCount] = useState<number>(5);
185
+ const mockNode = { setDataValue: (_: unknown, val: number) => setCount(val) } as CustomCellRendererProps['node'];
186
+
187
+ return (
188
+ <NumberInputCellRenderer
189
+ {...({} as unknown as CustomCellRendererProps)}
190
+ value={count}
191
+ node={mockNode}
192
+ colDef={{ field: 'count' }}
193
+ />
194
+ );
195
+ };
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Stories
199
+ // ---------------------------------------------------------------------------
200
+
201
+ export const Default: Story = withDescription(
202
+ {
203
+ parameters: { controls: { disable: true } },
204
+ render: InteractiveTemplate,
205
+ },
206
+ 'Interactive `NumberInput` backed by a mock row node. Use the +/− spinners or type directly — `node.setDataValue` updates local state in this preview; in a real table it updates AG Grid row data in-place.',
207
+ );
208
+
209
+ export const WithMinMax: Story = withDescription(
210
+ {
211
+ parameters: { controls: { disable: true } },
212
+ render: () => {
213
+ const [val, setVal] = useState<number>(5);
214
+ const mockNode = { setDataValue: (_: unknown, v: number) => setVal(v) } as CustomCellRendererProps['node'];
215
+
216
+ return (
217
+ <NumberInputCellRenderer
218
+ {...({} as unknown as CustomCellRendererProps)}
219
+ value={val}
220
+ min={0}
221
+ max={10}
222
+ node={mockNode}
223
+ colDef={{ field: 'count' }}
224
+ />
225
+ );
226
+ },
227
+ },
228
+ 'Bounded range via `min={0}` and `max={10}`. The minus spinner disables at `0`; the plus spinner disables at `10`. Values typed outside the range are clamped on blur.',
229
+ );
230
+
231
+ export const NullValue: Story = withDescription(
232
+ {
233
+ parameters: { controls: { disable: true } },
234
+ render: () => (
235
+ <NumberInputCellRenderer
236
+ {...({} as unknown as CustomCellRendererProps)}
237
+ value={null}
238
+ node={{ setDataValue: () => {} } as unknown as CustomCellRendererProps['node']}
239
+ colDef={{ field: 'count' }}
240
+ />
241
+ ),
242
+ },
243
+ '`value={null}` is coerced to `0` before being passed to `NumberInput`. This keeps the input always controlled with a valid numeric display.',
244
+ );
245
+
246
+ export const Disabled: Story = withDescription(
247
+ {
248
+ parameters: { controls: { disable: true } },
249
+ render: () => (
250
+ <NumberInputCellRenderer
251
+ {...({} as unknown as CustomCellRendererProps)}
252
+ value={7}
253
+ disabled
254
+ node={{ setDataValue: () => {} } as unknown as CustomCellRendererProps['node']}
255
+ colDef={{ field: 'count' }}
256
+ />
257
+ ),
258
+ },
259
+ 'Disabled state — pass `disabled: true` via `cellRendererParams` to prevent editing. Useful for read-only rows or conditional editing logic.',
260
+ );
261
+
262
+ const NUMBER_INPUT_TABLE_DATA = [
263
+ { name: 'Alice Johnson', sessions: 12, score: 95 },
264
+ { name: 'Bob Smith', sessions: 8, score: 72 },
265
+ { name: 'Charlie Brown', sessions: 15, score: 88 },
266
+ { name: 'Diana Prince', sessions: 3, score: 60 },
267
+ ];
268
+
269
+ export const InATable: Story = withDescription(
270
+ {
271
+ parameters: {
272
+ controls: { disable: true },
273
+ docs: {
274
+ source: {
275
+ language: 'tsx',
276
+ code: `
277
+ import { Table } from '@arbor-education/design-system.components';
278
+
279
+ const rowData = [
280
+ { name: 'Alice Johnson', sessions: 12, score: 95 },
281
+ { name: 'Bob Smith', sessions: 8, score: 72 },
282
+ { name: 'Charlie Brown', sessions: 15, score: 88 },
283
+ { name: 'Diana Prince', sessions: 3, score: 60 },
284
+ ];
285
+
286
+ function NumberInputCellRendererExample() {
287
+ return (
288
+ <Table
289
+ rowData={rowData}
290
+ columnDefs={[
291
+ { field: 'name', headerName: 'Name', flex: 2 },
292
+ {
293
+ field: 'sessions',
294
+ headerName: 'Sessions',
295
+ flex: 1,
296
+ editable: false,
297
+ cellRenderer: 'dsNumberInputCellRenderer',
298
+ cellRendererParams: { min: 0, suppressCellFocusAndFocusFirstElement: true },
299
+ },
300
+ {
301
+ field: 'score',
302
+ headerName: 'Score (/100)',
303
+ flex: 1,
304
+ editable: false,
305
+ cellRenderer: 'dsNumberInputCellRenderer',
306
+ cellRendererParams: { min: 0, max: 100, suppressCellFocusAndFocusFirstElement: true },
307
+ },
308
+ ]}
309
+ defaultColDef={{ flex: 1, minWidth: 120 }}
310
+ domLayout="autoHeight"
311
+ />
312
+ );
313
+ }
314
+ `.trim(),
315
+ },
316
+ },
317
+ },
318
+ render: () => (
319
+ <Table
320
+ rowData={NUMBER_INPUT_TABLE_DATA}
321
+ columnDefs={[
322
+ { field: 'name', headerName: 'Name', flex: 2 },
323
+ {
324
+ field: 'sessions',
325
+ headerName: 'Sessions',
326
+ flex: 1,
327
+ editable: false,
328
+ cellRenderer: 'dsNumberInputCellRenderer',
329
+ cellRendererParams: { min: 0, suppressCellFocusAndFocusFirstElement: true },
330
+ },
331
+ {
332
+ field: 'score',
333
+ headerName: 'Score (/100)',
334
+ flex: 1,
335
+ editable: false,
336
+ cellRenderer: 'dsNumberInputCellRenderer',
337
+ cellRendererParams: { min: 0, max: 100, suppressCellFocusAndFocusFirstElement: true },
338
+ },
339
+ ]}
340
+ defaultColDef={{ flex: 1, minWidth: 120 }}
341
+ domLayout="autoHeight"
342
+ />
343
+ ),
344
+ },
345
+ '`dsNumberInputCellRenderer` wired up inside a full `Table`. Sessions has `min={0}`; Score has `min={0}` and `max={100}`. Edit any cell — AG Grid calls `node.setDataValue` which updates the row data in-place.',
346
+ );
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test, vi } 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 { CustomCellRendererProps } from 'ag-grid-react';
6
+ import { NumberInputCellRenderer } from './NumberInputCellRenderer.js';
7
+
8
+ const createMockProps = (overrides: Partial<CustomCellRendererProps & { value: number | null; min?: number; max?: number; disabled?: boolean }> = {}): CustomCellRendererProps => ({
9
+ value: 0,
10
+ node: { setDataValue: vi.fn() },
11
+ colDef: { field: 'count' },
12
+ ...overrides,
13
+ } as unknown as CustomCellRendererProps);
14
+
15
+ describe('NumberInputCellRenderer', () => {
16
+ test('renders a number input', () => {
17
+ render(<NumberInputCellRenderer {...createMockProps()} />);
18
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
19
+ });
20
+
21
+ test('renders the supplied value in the input', () => {
22
+ render(<NumberInputCellRenderer {...createMockProps({ value: 42 })} />);
23
+ expect(screen.getByRole('textbox')).toHaveValue('42');
24
+ });
25
+
26
+ test('renders null value as 0', () => {
27
+ render(<NumberInputCellRenderer {...createMockProps({ value: null })} />);
28
+ expect(screen.getByRole('textbox')).toHaveValue('0');
29
+ });
30
+
31
+ test('disables the input when disabled prop is true', () => {
32
+ render(<NumberInputCellRenderer {...createMockProps({ disabled: true })} />);
33
+ expect(screen.getByRole('textbox')).toBeDisabled();
34
+ });
35
+
36
+ test('minus button is disabled when value equals min', () => {
37
+ render(<NumberInputCellRenderer {...createMockProps({ value: 5, min: 5 })} />);
38
+ expect(screen.getByRole('button', { name: 'Minus button' })).toBeDisabled();
39
+ });
40
+
41
+ test('plus button is disabled when value equals max', () => {
42
+ render(<NumberInputCellRenderer {...createMockProps({ value: 10, max: 10 })} />);
43
+ expect(screen.getByRole('button', { name: 'Plus button' })).toBeDisabled();
44
+ });
45
+
46
+ test('calls node.setDataValue with the updated value when the input changes', async () => {
47
+ const setDataValue = vi.fn();
48
+ render(
49
+ <NumberInputCellRenderer
50
+ {...createMockProps({
51
+ value: 5,
52
+ node: { setDataValue } as unknown as CustomCellRendererProps['node'],
53
+ colDef: { field: 'count' },
54
+ })}
55
+ />,
56
+ );
57
+ setDataValue.mockClear();
58
+
59
+ await userEvent.click(screen.getByRole('button', { name: 'Plus button' }));
60
+
61
+ expect(setDataValue).toHaveBeenCalledWith('count', 6);
62
+ });
63
+
64
+ test('calls node.setDataValue with 0 when the input is cleared', async () => {
65
+ const setDataValue = vi.fn();
66
+ render(
67
+ <NumberInputCellRenderer
68
+ {...createMockProps({
69
+ value: 5,
70
+ node: { setDataValue } as unknown as CustomCellRendererProps['node'],
71
+ colDef: { field: 'count' },
72
+ })}
73
+ />,
74
+ );
75
+ setDataValue.mockClear();
76
+
77
+ await userEvent.clear(screen.getByRole('textbox'));
78
+
79
+ expect(setDataValue).toHaveBeenCalledWith('count', 0);
80
+ });
81
+
82
+ test('does not call node.setDataValue when colDef.field is undefined', async () => {
83
+ const setDataValue = vi.fn();
84
+ render(
85
+ <NumberInputCellRenderer
86
+ {...createMockProps({
87
+ value: 5,
88
+ node: { setDataValue } as unknown as CustomCellRendererProps['node'],
89
+ colDef: {},
90
+ })}
91
+ />,
92
+ );
93
+ setDataValue.mockClear();
94
+
95
+ await userEvent.click(screen.getByRole('button', { name: 'Plus button' }));
96
+
97
+ expect(setDataValue).not.toHaveBeenCalled();
98
+ });
99
+ });
@@ -0,0 +1,36 @@
1
+ import type { CustomCellRendererProps } from 'ag-grid-react';
2
+ import { NumberInput } from 'Components/formField/inputs/number/NumberInput';
3
+
4
+ type NumberInputCellRendererProps = {
5
+ value: number | null;
6
+ min?: number;
7
+ max?: number;
8
+ disabled?: boolean;
9
+ } & CustomCellRendererProps;
10
+
11
+ export const NumberInputCellRenderer = (
12
+ props: NumberInputCellRendererProps,
13
+ ) => {
14
+ const { value, min, max, disabled, node, colDef = {} } = props;
15
+
16
+ return (
17
+ <div className="cell-input-wrapper">
18
+ <NumberInput
19
+ min={min}
20
+ max={max}
21
+ disabled={disabled}
22
+ value={value ?? 0}
23
+ onChange={(e) => {
24
+ if (!colDef.field) {
25
+ return;
26
+ }
27
+ const next = parseFloat(e.currentTarget.value);
28
+ node.setDataValue(
29
+ colDef.field,
30
+ Number.isFinite(next) ? next : 0,
31
+ );
32
+ }}
33
+ />
34
+ </div>
35
+ );
36
+ };
@@ -211,6 +211,14 @@ const meta = {
211
211
  defaultValue: { summary: 'false' },
212
212
  },
213
213
  },
214
+ onCloseAutoFocus: {
215
+ control: false,
216
+ description: 'Called when focus is about to return to the ag-grid cell after the dropdown closes (e.g. after selecting an option). Call `event.preventDefault()` to stop radix\'s default restore-to-cell behaviour and move focus elsewhere yourself.',
217
+ table: {
218
+ type: { summary: '(event: Event) => void' },
219
+ defaultValue: { summary: 'undefined' },
220
+ },
221
+ },
214
222
  },
215
223
  } satisfies Meta<typeof SelectDropdownCellRenderer>;
216
224
 
@@ -718,3 +726,62 @@ const columnDefs = [
718
726
  },
719
727
  '(fillCell + colored) Marksheet with grade-driven cell colours. Each cell uses `backgroundColor` (derived per-row from the current grade) and `fillCell: true` so the colour spans the cell edge-to-edge, plus `cellStyle: { padding: 0 }` to strip ag-grid\'s default cell padding. `iconClickOnly: true` is set so cell selection works — click anywhere in a cell to select it (or shift/drag for a range), and only the chevron icon opens the grade picker. Text colour auto-switches between dark and white for legibility against each background.',
720
728
  );
729
+
730
+ export const CustomCloseFocus: Story = withDescription(
731
+ {
732
+ parameters: {
733
+ controls: { disable: true },
734
+ docs: {
735
+ source: {
736
+ language: 'tsx',
737
+ code: `
738
+ import { Table } from '@arbor-education/design-system.components';
739
+
740
+ const columnDefs = [
741
+ { field: 'name', headerName: 'Name', flex: 2 },
742
+ {
743
+ field: 'status',
744
+ headerName: 'Status',
745
+ cellRenderer: 'dsSelectDropdownCellRenderer',
746
+ cellRendererParams: {
747
+ options: statusOptions,
748
+ onCloseAutoFocus: (event) => {
749
+ event.preventDefault();
750
+ window.alert('Dropdown closed — running custom focus handler');
751
+ },
752
+ },
753
+ },
754
+ ];
755
+
756
+ <Table rowData={rowData} columnDefs={columnDefs} domLayout="autoHeight" />
757
+ `.trim(),
758
+ },
759
+ },
760
+ },
761
+ render: () => (
762
+ <Table
763
+ rowData={SELECT_IN_TABLE_DATA}
764
+ columnDefs={[
765
+ { field: 'name', headerName: 'Name', flex: 2 },
766
+ {
767
+ field: 'status',
768
+ headerName: 'Status',
769
+ flex: 1,
770
+ editable: false,
771
+ cellRenderer: 'dsSelectDropdownCellRenderer',
772
+ cellRendererParams: {
773
+ options: STATUS_OPTIONS,
774
+ onCloseAutoFocus: (event: Event) => {
775
+ event.preventDefault();
776
+ window.alert('Dropdown closed — running custom focus handler');
777
+ },
778
+ },
779
+ },
780
+ ]}
781
+ defaultColDef={{ flex: 1, minWidth: 120 }}
782
+ domLayout="autoHeight"
783
+ />
784
+ ),
785
+ },
786
+ 'Passes `onCloseAutoFocus` via `cellRendererParams` to override what happens to focus when the dropdown closes. Open a Status dropdown and pick an option (or press Escape) — instead of radix restoring focus to the cell, the handler calls `event.preventDefault()` and fires a `window.alert`. In a real app you would move focus to a specific element here rather than alert.',
787
+ );
@@ -52,6 +52,7 @@ export const SelectDropdownCellRenderer = (
52
52
  backgroundColor,
53
53
  fillCell = false,
54
54
  iconClickOnly = false,
55
+ onCloseAutoFocus,
55
56
  } = props;
56
57
 
57
58
  const textContrast = backgroundColor ? getTextContrast(backgroundColor) : null;
@@ -113,6 +114,7 @@ export const SelectDropdownCellRenderer = (
113
114
  onOpenChange={setIsOpen}
114
115
  multiple={false}
115
116
  iconClickOnly={iconClickOnly}
117
+ onCloseAutoFocus={onCloseAutoFocus}
116
118
  onSelectionChange={(newValue) => {
117
119
  if (column && newValue[0] != null) {
118
120
  const selectedOption = rawOptions.find(
package/src/index.ts CHANGED
@@ -69,6 +69,7 @@ export { DSDefaultColDef } from 'Components/table/DSDefaultColDef';
69
69
  export { GridApiContext } from 'Components/table/GridApiContext';
70
70
  export { NumberCellEditor } from 'Components/table/cellEditors/NumberCellEditor';
71
71
  export { CheckboxCellRenderer } from 'Components/table/cellRenderers/CheckboxCellRenderer';
72
+ export { NumberInputCellRenderer } from 'Components/table/cellRenderers/NumberInputCellRenderer';
72
73
  export { Table } from 'Components/table/Table';
73
74
  export { Tabs } from 'Components/tabs/Tabs';
74
75
  export { Tag } from 'Components/tag/Tag';