@arbor-education/design-system.components 0.25.7 → 0.25.9

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 (51) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/.snyk +3 -0
  3. package/CHANGELOG.md +12 -0
  4. package/CODEOWNERS +2 -1
  5. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  6. package/dist/components/datePicker/DatePicker.js +8 -6
  7. package/dist/components/datePicker/DatePicker.js.map +1 -1
  8. package/dist/components/datePicker/DatePickerCalendar.d.ts +10 -0
  9. package/dist/components/datePicker/DatePickerCalendar.d.ts.map +1 -0
  10. package/dist/components/datePicker/DatePickerCalendar.js +9 -0
  11. package/dist/components/datePicker/DatePickerCalendar.js.map +1 -0
  12. package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -1
  13. package/dist/components/dateTimePicker/DateTimePicker.js +11 -8
  14. package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -1
  15. package/dist/components/table/Table.d.ts +6 -0
  16. package/dist/components/table/Table.d.ts.map +1 -1
  17. package/dist/components/table/Table.js +3 -0
  18. package/dist/components/table/Table.js.map +1 -1
  19. package/dist/components/table/Table.stories.d.ts +1 -0
  20. package/dist/components/table/Table.stories.d.ts.map +1 -1
  21. package/dist/components/table/Table.stories.js +41 -0
  22. package/dist/components/table/Table.stories.js.map +1 -1
  23. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts +10 -0
  24. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts.map +1 -0
  25. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js +13 -0
  26. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js.map +1 -0
  27. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts +90 -0
  28. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts.map +1 -0
  29. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js +243 -0
  30. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js.map +1 -0
  31. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts +2 -0
  32. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts.map +1 -0
  33. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js +72 -0
  34. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js.map +1 -0
  35. package/dist/index.css +0 -33
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/components/datePicker/DatePicker.tsx +30 -35
  43. package/src/components/datePicker/DatePickerCalendar.tsx +46 -0
  44. package/src/components/dateTimePicker/DateTimePicker.tsx +66 -38
  45. package/src/components/dateTimePicker/dateTimePicker.scss +0 -2
  46. package/src/components/table/Table.stories.tsx +45 -0
  47. package/src/components/table/Table.tsx +3 -0
  48. package/src/components/table/cellRenderers/NumberInputCellRenderer.stories.tsx +346 -0
  49. package/src/components/table/cellRenderers/NumberInputCellRenderer.test.tsx +99 -0
  50. package/src/components/table/cellRenderers/NumberInputCellRenderer.tsx +36 -0
  51. package/src/index.ts +1 -0
@@ -13,12 +13,24 @@ import {
13
13
  type DateDisplayFormat,
14
14
  type DateTimePickerDisplayFormat,
15
15
  } from 'Components/datePicker/dateInputUtils';
16
+ import { DatePickerCalendar } from 'Components/datePicker/DatePickerCalendar';
16
17
  import { DatePickerCalendarHeader } from 'Components/datePicker/DatePickerCalendarHeader';
17
18
  import { TextInput } from 'Components/formField/inputs/text/TextInput';
18
- import { TimeInput, type TimeGranularity, type TimeValue } from 'Components/formField/inputs/time/TimeInput';
19
+ import {
20
+ TimeInput,
21
+ type TimeGranularity,
22
+ type TimeValue,
23
+ } from 'Components/formField/inputs/time/TimeInput';
19
24
  import { Popover } from 'radix-ui';
20
- import { useCallback, useContext, useEffect, useRef, useState, type ChangeEvent, type PointerEvent } from 'react';
21
- import { DayPicker } from 'react-day-picker';
25
+ import {
26
+ useCallback,
27
+ useContext,
28
+ useEffect,
29
+ useRef,
30
+ useState,
31
+ type ChangeEvent,
32
+ type PointerEvent,
33
+ } from 'react';
22
34
  import { PopupParentContext } from 'Utils/popupParent/PopupParentContext';
23
35
 
24
36
  export type DateTimePickerProps = {
@@ -62,7 +74,11 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
62
74
  (date: Date) =>
63
75
  displayFormat === 'native'
64
76
  ? formatNativeDateTimeInputValue(date, granularity)
65
- : formatDateTimeInputValue(date, displayFormat as DateDisplayFormat, granularity),
77
+ : formatDateTimeInputValue(
78
+ date,
79
+ displayFormat as DateDisplayFormat,
80
+ granularity,
81
+ ),
66
82
  [displayFormat, granularity],
67
83
  );
68
84
 
@@ -70,31 +86,43 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
70
86
  (raw: string) =>
71
87
  displayFormat === 'native'
72
88
  ? parseNativeDateTimeInputValue(raw, granularity)
73
- : parseDateTimeInputValue(raw, displayFormat as DateDisplayFormat, granularity),
89
+ : parseDateTimeInputValue(
90
+ raw,
91
+ displayFormat as DateDisplayFormat,
92
+ granularity,
93
+ ),
74
94
  [displayFormat, granularity],
75
95
  );
76
96
 
77
- const resolvedPlaceholder = placeholder ?? (
78
- isNative
79
- ? getNativeDateTimePlaceholder(granularity)
80
- : getDateTimePlaceholder(displayFormat as DateDisplayFormat, granularity)
81
- );
97
+ const resolvedPlaceholder
98
+ = placeholder
99
+ ?? (isNative
100
+ ? getNativeDateTimePlaceholder(granularity)
101
+ : getDateTimePlaceholder(
102
+ displayFormat as DateDisplayFormat,
103
+ granularity,
104
+ ));
82
105
 
83
106
  const initialValue = value ?? defaultValue;
84
107
  const isControlled = value !== undefined;
85
108
  const [month, setMonth] = useState(initialValue ?? new Date());
86
- const [selectedDateTime, setSelectedDateTime] = useState<Date | undefined>(initialValue);
109
+ const [selectedDateTime, setSelectedDateTime] = useState<Date | undefined>(
110
+ initialValue,
111
+ );
87
112
  const [inputValue, setInputValue] = useState(
88
113
  initialValue ? formatInputValue(initialValue) : '',
89
114
  );
90
- const [timeInputValue, setTimeInputValue] = useState(initialValue ? formatTimeInputValue(initialValue, granularity) : '');
115
+ const [timeInputValue, setTimeInputValue] = useState(
116
+ initialValue ? formatTimeInputValue(initialValue, granularity) : '',
117
+ );
91
118
  const [isPickerOpen, setIsPickerOpen] = useState(false);
92
119
 
93
120
  const inputRef = useRef<HTMLInputElement>(null);
94
121
  const selectedDateTimeRef = useRef(selectedDateTime);
95
122
  selectedDateTimeRef.current = selectedDateTime;
96
123
 
97
- const showFieldHint = isNative && selectedDateTime === undefined && inputValue === '';
124
+ const showFieldHint
125
+ = isNative && selectedDateTime === undefined && inputValue === '';
98
126
  const hasCustomEmptyHint = showFieldHint && placeholder !== undefined;
99
127
 
100
128
  /** Custom empty hint in native mode: open only on pointer (not Tab focus). */
@@ -163,7 +191,11 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
163
191
  return;
164
192
  }
165
193
 
166
- const nextDateTime = mergeDateAndTime(selectedDateTime, nextTimeValue, granularity);
194
+ const nextDateTime = mergeDateAndTime(
195
+ selectedDateTime,
196
+ nextTimeValue,
197
+ granularity,
198
+ );
167
199
  setSelectedDateTime(nextDateTime);
168
200
  setInputValue(formatInputValue(nextDateTime));
169
201
  onChange?.(nextDateTime);
@@ -178,7 +210,11 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
178
210
  return;
179
211
  }
180
212
 
181
- const nextDateTime = mergeDateAndTime(nextDate, timeInputValue, granularity);
213
+ const nextDateTime = mergeDateAndTime(
214
+ nextDate,
215
+ timeInputValue,
216
+ granularity,
217
+ );
182
218
  setSelectedDateTime(nextDateTime);
183
219
  setMonth(nextDateTime);
184
220
  setInputValue(formatInputValue(nextDateTime));
@@ -204,10 +240,9 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
204
240
  return (
205
241
  <div className={classNames('ds-date-time-picker', className)}>
206
242
  <div
207
- className={classNames(
208
- 'ds-date-time-picker__field',
209
- { 'ds-date-time-picker__field--show-hint': showFieldHint },
210
- )}
243
+ className={classNames('ds-date-time-picker__field', {
244
+ 'ds-date-time-picker__field--show-hint': showFieldHint,
245
+ })}
211
246
  >
212
247
  <TextInput
213
248
  ref={inputRef}
@@ -232,7 +267,10 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
232
267
  )
233
268
  : null}
234
269
  </div>
235
- <Popover.Root open={isPickerOpen} onOpenChange={open => setIsPickerOpen(open)}>
270
+ <Popover.Root
271
+ open={isPickerOpen}
272
+ onOpenChange={open => setIsPickerOpen(open)}
273
+ >
236
274
  <Popover.Trigger asChild>
237
275
  <Button
238
276
  className="ds-date-time-picker__button"
@@ -263,26 +301,16 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
263
301
  highlightStringMatches={highlightStringMatches}
264
302
  hasError={hasError}
265
303
  />
266
- <DatePickerCalendarHeader month={month} onMonthChange={setMonth} />
267
- <DayPicker
268
- className="ds-date-time-picker__calendar"
304
+ <DatePickerCalendarHeader
269
305
  month={month}
270
306
  onMonthChange={setMonth}
271
- autoFocus
272
- role="application"
273
- mode="single"
274
- selected={selectedDateTime}
275
- onSelect={handleDayPickerSelect}
276
- hideNavigation
277
- footer={(
278
- <Button
279
- variant="text-link"
280
- onClick={handleTodayClick}
281
- className="ds-date-time-picker__today-button"
282
- >
283
- Today
284
- </Button>
285
- )}
307
+ />
308
+ <DatePickerCalendar
309
+ month={month}
310
+ setMonth={setMonth}
311
+ selectedDate={selectedDateTime}
312
+ onDayPickerSelect={handleDayPickerSelect}
313
+ handleTodayClick={handleTodayClick}
286
314
  />
287
315
  </div>
288
316
  </Popover.Content>
@@ -13,5 +13,3 @@
13
13
  .ds-date-time-picker__time {
14
14
  width: 100%;
15
15
  }
16
-
17
- @include shell.ds-date-picker-shell-calendar("date-time-picker");
@@ -87,6 +87,7 @@ const DEVELOPER_NOTES = [
87
87
  '| `dsSelectDropdownCellRenderer` | `Table.SelectDropdownCellRenderer` | Renders a `SelectDropdown` in a cell |',
88
88
  '| `dsBooleanCellRenderer` | `Table.BooleanCellRenderer` | Read-only boolean display |',
89
89
  '| `dsCheckboxCellRenderer` | `Table.CheckboxCellRenderer` | Editable boolean checkbox |',
90
+ '| `dsNumberInputCellRenderer` | `Table.NumberInputCellRenderer` | Editable numeric input |',
90
91
  '| `dsDateCellEditor` | `Table.DateCellEditor` | Date picker cell editor |',
91
92
  '',
92
93
  '---',
@@ -2408,4 +2409,48 @@ export const WithComboboxCellRenderer: Story = {
2408
2409
  },
2409
2410
  };
2410
2411
 
2412
+ const numberInputSampleData = [
2413
+ { name: 'Alice Johnson', sessions: 12, score: 95 },
2414
+ { name: 'Bob Smith', sessions: 8, score: 72 },
2415
+ { name: 'Charlie Brown', sessions: 15, score: 88 },
2416
+ { name: 'Diana Prince', sessions: 3, score: 60 },
2417
+ ];
2418
+
2419
+ const numberInputColDefs: (ColDef | ColGroupDef)[] = [
2420
+ { field: 'name', headerName: 'Name', flex: 2 },
2421
+ {
2422
+ field: 'sessions',
2423
+ headerName: 'Sessions',
2424
+ flex: 1,
2425
+ editable: false,
2426
+ cellRenderer: 'dsNumberInputCellRenderer',
2427
+ cellRendererParams: { min: 0, suppressCellFocusAndFocusFirstElement: true },
2428
+ },
2429
+ {
2430
+ field: 'score',
2431
+ headerName: 'Score (/100)',
2432
+ flex: 1,
2433
+ editable: false,
2434
+ cellRenderer: 'dsNumberInputCellRenderer',
2435
+ cellRendererParams: { min: 0, max: 100, suppressCellFocusAndFocusFirstElement: true },
2436
+ },
2437
+ ];
2438
+
2439
+ export const WithNumberInputCellRenderer: Story = {
2440
+ parameters: {
2441
+ docs: {
2442
+ description: {
2443
+ story:
2444
+ 'Columns can use the `dsNumberInputCellRenderer` cell renderer to display an editable numeric input inside the cell. Pass `min` and `max` via `cellRendererParams` to constrain the allowed range — the spinners disable at the bounds.',
2445
+ },
2446
+ },
2447
+ },
2448
+ args: {
2449
+ rowData: numberInputSampleData,
2450
+ columnDefs: numberInputColDefs,
2451
+ defaultColDef,
2452
+ domLayout: 'autoHeight',
2453
+ },
2454
+ };
2455
+
2411
2456
  export default meta;
@@ -35,6 +35,7 @@ import { BooleanCellRenderer } from './cellRenderers/BooleanCellRenderer.js';
35
35
  import { CheckboxCellRenderer } from './cellRenderers/CheckboxCellRenderer.js';
36
36
  import { ComboboxCellRenderer } from './cellRenderers/ComboboxCellRenderer.js';
37
37
  import { DefaultCellRenderer } from './cellRenderers/DefaultCellRenderer.js';
38
+ import { NumberInputCellRenderer } from './cellRenderers/NumberInputCellRenderer.js';
38
39
 
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  type TableProps<TData = any> = {
@@ -210,6 +211,7 @@ export const Table = (props: TableProps) => {
210
211
  dsCheckboxCellRenderer: CheckboxCellRenderer,
211
212
  dsBooleanCellRenderer: BooleanCellRenderer,
212
213
  dsComboboxCellRenderer: ComboboxCellRenderer,
214
+ dsNumberInputCellRenderer: NumberInputCellRenderer,
213
215
  ...components,
214
216
  }}
215
217
  {...rest}
@@ -245,6 +247,7 @@ Table.ComboboxCellRenderer = ComboboxCellRenderer;
245
247
  Table.DefaultValueFormatter = defaultValueFormatter;
246
248
  Table.TableSettingsDropdown = TableSettingsDropdown;
247
249
  Table.TableControls = TableControls;
250
+ Table.NumberInputCellRenderer = NumberInputCellRenderer;
248
251
 
249
252
  Table.Spacing = TABLE_SPACING;
250
253
  Table.SettingsItems = TABLE_SETTINGS_ITEMS;
@@ -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
+ );