@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.
- package/.github/CODEOWNERS +1 -0
- package/.snyk +3 -0
- package/CHANGELOG.md +12 -0
- package/CODEOWNERS +2 -1
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.js +8 -6
- package/dist/components/datePicker/DatePicker.js.map +1 -1
- package/dist/components/datePicker/DatePickerCalendar.d.ts +10 -0
- package/dist/components/datePicker/DatePickerCalendar.d.ts.map +1 -0
- package/dist/components/datePicker/DatePickerCalendar.js +9 -0
- package/dist/components/datePicker/DatePickerCalendar.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -1
- package/dist/components/dateTimePicker/DateTimePicker.js +11 -8
- package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -1
- package/dist/components/table/Table.d.ts +6 -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 +1 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +41 -0
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts +10 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.js +13 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.js.map +1 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts +90 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js +243 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js.map +1 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts +2 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js +72 -0
- package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js.map +1 -0
- package/dist/index.css +0 -33
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/datePicker/DatePicker.tsx +30 -35
- package/src/components/datePicker/DatePickerCalendar.tsx +46 -0
- package/src/components/dateTimePicker/DateTimePicker.tsx +66 -38
- package/src/components/dateTimePicker/dateTimePicker.scss +0 -2
- package/src/components/table/Table.stories.tsx +45 -0
- package/src/components/table/Table.tsx +3 -0
- package/src/components/table/cellRenderers/NumberInputCellRenderer.stories.tsx +346 -0
- package/src/components/table/cellRenderers/NumberInputCellRenderer.test.tsx +99 -0
- package/src/components/table/cellRenderers/NumberInputCellRenderer.tsx +36 -0
- 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 {
|
|
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 {
|
|
21
|
-
|
|
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(
|
|
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(
|
|
89
|
+
: parseDateTimeInputValue(
|
|
90
|
+
raw,
|
|
91
|
+
displayFormat as DateDisplayFormat,
|
|
92
|
+
granularity,
|
|
93
|
+
),
|
|
74
94
|
[displayFormat, granularity],
|
|
75
95
|
);
|
|
76
96
|
|
|
77
|
-
const resolvedPlaceholder
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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>(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
267
|
-
<DayPicker
|
|
268
|
-
className="ds-date-time-picker__calendar"
|
|
304
|
+
<DatePickerCalendarHeader
|
|
269
305
|
month={month}
|
|
270
306
|
onMonthChange={setMonth}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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>
|
|
@@ -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
|
+
);
|