@bmsuisse/datagrid 0.25.2 → 0.26.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/dist/index.d.ts CHANGED
@@ -161,6 +161,43 @@ interface BaseColumn<TRow> {
161
161
  * any, since `<DataGrid>` has no way to tell your content lacks one.
162
162
  */
163
163
  renderHeader?: (column: BaseColumn<TRow>) => ReactNode;
164
+ /**
165
+ * Opt-in: defaults to false — must be explicitly set (or return true) for
166
+ * `<DataGrid>` to render this column's cells as inline editors. A function
167
+ * makes it conditional per row (e.g. a "locked" row that can't be
168
+ * edited). See `DataGridProps.editing` for the accumulate-then-save
169
+ * workflow this feeds into — editable cells with no `editing` prop
170
+ * supplied just render as static text, same as if `editable` were unset.
171
+ *
172
+ * A cell doesn't become an editor just because this is true, though:
173
+ * clicking (or Enter/Space-ing) any editable cell in a row is what
174
+ * activates editors for every editable column in THAT row at once — the
175
+ * rest of the grid's rows stay static until clicked into themselves. A
176
+ * row stays active until its edits are saved or discarded, and
177
+ * activating one row never deactivates another.
178
+ */
179
+ editable?: boolean | ((row: TRow) => boolean);
180
+ /**
181
+ * Overrides the default type-based editor (see `renderDefaultEditWidget`)
182
+ * for this column's cells. Receives the cell's current value (the row's
183
+ * original value, or a still-pending edit if the user already changed it
184
+ * this session), the row itself, an `onChange` to record a new pending
185
+ * value, this cell's current validation error message (from
186
+ * `validateEdit` below) if any, and whether this specific cell is the one
187
+ * whose click just activated the row (see `editable`'s own doc) — forward
188
+ * it to your control's native `autoFocus` so the row-activating click also
189
+ * focuses the field the user actually clicked, not just some other cell
190
+ * in the same row.
191
+ */
192
+ renderEditCell?: (value: unknown, row: TRow, onChange: (next: unknown) => void, error: string | undefined, autoFocus: boolean) => ReactNode;
193
+ /**
194
+ * Validates a pending edit before it's allowed into `editing.onSave`'s
195
+ * payload. Returning a message string blocks the Save button (and shows
196
+ * the message under the cell) until the value is fixed or reverted to its
197
+ * original; returning `undefined` means valid. Runs on every change to
198
+ * this cell, including the default editors' own `onChange`.
199
+ */
200
+ validateEdit?: (value: unknown, row: TRow) => string | undefined;
164
201
  }
165
202
  type StringColumn<TRow> = BaseColumn<TRow> & {
166
203
  type: "string";
@@ -191,6 +228,8 @@ declare function getColumnValue<TRow>(column: ColumnDef<TRow>, row: TRow): unkno
191
228
  declare function isSortable<TRow>(column: ColumnDef<TRow>): boolean;
192
229
  /** True only if a column explicitly opts in (`filterable: true`) — the default is false. */
193
230
  declare function isFilterable<TRow>(column: ColumnDef<TRow>): boolean;
231
+ /** True only if a column explicitly opts in to `editable` — for this specific row, when `editable` is a predicate. Default is false. */
232
+ declare function isEditable<TRow>(column: ColumnDef<TRow>, row: TRow): boolean;
194
233
 
195
234
  /**
196
235
  * Default filter widget for `type: "boolean"` columns: a 3-way All/Yes/No
@@ -450,6 +489,14 @@ declare function renderDefaultFilterWidget<TRow>(column: ColumnDef<TRow>, value:
450
489
  declare function defaultAlign<TRow>(column: ColumnDef<TRow>): ColumnAlign;
451
490
  /** The literal Tailwind class for a column's default alignment; see `defaultAlign`. */
452
491
  declare function alignClassName<TRow>(column: ColumnDef<TRow>): string;
492
+ /**
493
+ * Parses an arbitrary raw cell value into a `Date`, or `null` if it isn't
494
+ * one. Exported (unlike `toNumber` below, which stays internal) because
495
+ * `edit/DateEditor.tsx` needs the exact same date-only-vs-datetime parsing
496
+ * this module already uses for display — a second copy would risk drifting
497
+ * from the UTC-midnight fix documented below.
498
+ */
499
+ declare function toDate(value: unknown): Date | null;
453
500
  /**
454
501
  * Renders a raw cell value as a display string, using the type-appropriate
455
502
  * default formatter for `column.type`. Columns with a custom `cell` render
@@ -457,6 +504,118 @@ declare function alignClassName<TRow>(column: ColumnDef<TRow>): string;
457
504
  */
458
505
  declare function defaultFormat<TRow>(column: ColumnDef<TRow>, value: unknown): string;
459
506
 
507
+ /**
508
+ * One row carrying at least one pending edit, as handed to
509
+ * `DataGridEditingOptions.onSave`. `values` is keyed by column id and holds
510
+ * only the columns that actually changed — not a full patched `TRow` — so
511
+ * this works uniformly for `accessorKey` columns (which have one obvious
512
+ * field to write back into) and `accessorFn` columns (which may not: a
513
+ * derived/computed column has no single field of `TRow` an edit could patch
514
+ * back onto).
515
+ */
516
+ interface EditedRow<TRow> {
517
+ rowId: string;
518
+ /** The row as it looked when its first still-pending edit was made — not necessarily the row's current on-screen data if it was refetched since. */
519
+ row: TRow;
520
+ values: Record<string, unknown>;
521
+ }
522
+
523
+ /**
524
+ * Contract every built-in edit widget implements — the `renderEditCell`
525
+ * equivalent of `filter/widget-types.ts`'s `FilterWidgetProps`. `TColumn` is
526
+ * narrowed to the specific column shape the widget knows how to render (e.g.
527
+ * `EnumColumn` for `EnumEditor`) — `renderDefaultEditWidget` picks the right
528
+ * widget by `column.type` before handing it a column, so the widget itself
529
+ * never has to branch on `type`.
530
+ *
531
+ * `value` is the cell's current value: the row's original value, or a
532
+ * still-pending edit if the user already changed it this session.
533
+ * `onChange` records a new pending value — it does not itself validate or
534
+ * persist anything. `error`, when set, is this cell's current
535
+ * `column.validateEdit` message; every built-in widget shows it as small
536
+ * text under its control and marks the control `aria-invalid`.
537
+ *
538
+ * `rowId` exists purely so each built-in widget's own `data-testid` (`edit-
539
+ * ${rowId}-${column.id}`) stays unique per cell — unlike a filter widget,
540
+ * which renders exactly once per column (in the header), an edit widget
541
+ * renders once per (row, column) pair, so a column-id-only testid would
542
+ * collide across every row the moment a grid has more than one.
543
+ *
544
+ * `autoFocus` is true for exactly one cell per row-activation: the specific
545
+ * column whose static cell the user clicked (or Enter/Space'd) to turn the
546
+ * whole row into editors — every other cell in that same row gets `false`.
547
+ * Every built-in widget forwards it straight to its underlying control's own
548
+ * native `autoFocus` prop, so the browser focuses that one control the
549
+ * moment it mounts, without an extra click to actually start typing.
550
+ */
551
+ interface EditWidgetProps<TColumn> {
552
+ column: TColumn;
553
+ rowId: string;
554
+ value: unknown;
555
+ onChange: (next: unknown) => void;
556
+ error?: string;
557
+ autoFocus?: boolean;
558
+ }
559
+ /**
560
+ * The `id` every built-in widget gives its own error `<span>`, so its
561
+ * control's `aria-describedby` can point at it — a screen reader announces
562
+ * `aria-invalid` alone with no indication of *why*, so the association
563
+ * matters, not just the flag. Exported for a custom `renderEditCell` that
564
+ * wants the same convention instead of inventing its own id scheme.
565
+ */
566
+ declare function editErrorId(rowId: string, columnId: string): string;
567
+
568
+ /**
569
+ * The error text every built-in editor shows under its control, associated
570
+ * with it via `id` + the control's own `aria-describedby={editErrorId(...)}`
571
+ * — pulled out to one place so the a11y wiring/styling convention can't
572
+ * drift between the 5 editors the way five independent copies of the same
573
+ * `<span>` inevitably would.
574
+ */
575
+ declare function EditFieldError({ id, message }: {
576
+ id: string;
577
+ message: string;
578
+ }): ReactElement;
579
+
580
+ /** Default inline editor for `type: "boolean"` columns: a single checkbox. */
581
+ declare function BooleanEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<BooleanColumn<TRow>>): ReactElement;
582
+
583
+ /**
584
+ * Default inline editor for `type: "date"` / `type: "datetime"` columns: a
585
+ * native `<input type="date">`/`<input type="datetime-local">`. Emits a
586
+ * `Date` (or `null` for an emptied field), via `parseISO` rather than the
587
+ * bare `new Date(...)` constructor — same convention `DateRangeFilter`'s
588
+ * `parseBound` and `format.ts`'s `toDate` already use, since a native date
589
+ * input's value is a date-only `"YYYY-MM-DD"` string for `type: "date"`,
590
+ * which the JS spec would otherwise parse as UTC midnight instead of local
591
+ * midnight.
592
+ */
593
+ declare function DateEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<DateColumn<TRow>>): ReactElement;
594
+
595
+ /** Default inline editor for `type: "enum"` columns: a single-value select over `column.options`. */
596
+ declare function EnumEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<EnumColumn<TRow>>): ReactElement;
597
+
598
+ /**
599
+ * Default inline editor for `type: "number"` / `type: "currency"` columns: a
600
+ * native number input. Emits `null` for an emptied field (not `NaN` or `""`)
601
+ * so `validateEdit`/`onSave` see an explicit "cleared" value; otherwise emits
602
+ * a real `number`, never the input's raw string.
603
+ */
604
+ declare function NumberEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<NumberColumn<TRow>>): ReactElement;
605
+
606
+ /** Default inline editor for `type: "string"` columns: a plain text input. */
607
+ declare function StringEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<StringColumn<TRow>>): ReactElement;
608
+
609
+ /**
610
+ * Picks and renders the default inline-edit widget for a column, based
611
+ * purely on `column.type` — the `renderEditCell` equivalent of
612
+ * `filter/registry.tsx`'s `renderDefaultFilterWidget`. `<DataGrid>` calls
613
+ * this itself for any editable column that doesn't set its own
614
+ * `renderEditCell`; a consuming app only reaches for it directly to wrap or
615
+ * extend one of these defaults rather than writing an editor from scratch.
616
+ */
617
+ declare function renderDefaultEditWidget<TRow>(column: ColumnDef<TRow>, rowId: string, value: unknown, onChange: (next: unknown) => void, error?: string, autoFocus?: boolean): ReactElement;
618
+
460
619
  /**
461
620
  * Column visibility keyed by `ColumnDef.id`. A missing key means visible —
462
621
  * this matches TanStack Table's own `VisibilityState` convention, so a
@@ -616,6 +775,56 @@ interface DataGridVirtualizeOptions {
616
775
  /** Whether more rows exist beyond what's currently loaded. `onEndReached` never fires while this is `false`. Defaults to `true`. */
617
776
  hasMore?: boolean;
618
777
  }
778
+ /**
779
+ * Drives `<DataGrid>`'s built-in inline-editing workflow: cells in a column
780
+ * with `editable` set become interactive editors, and every change
781
+ * accumulates locally (never written back onto the caller's `data`, never
782
+ * sent anywhere on its own) until the user clicks the Save button this
783
+ * renders in a bar above the grid — or Discard, to drop everything back to
784
+ * unedited. There is no partial/per-cell autosave; see `onSave` below for
785
+ * the all-or-nothing commit point.
786
+ *
787
+ * This package has no translation/i18n layer anywhere — `saveLabel`/
788
+ * `discardLabel` are plain strings (or a count-aware function) you supply
789
+ * directly, the same convention every other caller-facing string in this
790
+ * package already follows (`header`, menu item labels, etc.).
791
+ */
792
+ interface DataGridEditingOptions<TRow> {
793
+ /**
794
+ * Called once, when the user clicks Save, with every row currently
795
+ * holding at least one pending edit and no validation error
796
+ * (`validateEdit`-failing rows are excluded — Save itself stays disabled
797
+ * while ANY row has one, so in practice this only ever fires once none
798
+ * do). `<DataGrid>` keeps its pending-edit state exactly as it was until
799
+ * this resolves: **throw or reject to leave every pending edit in place**
800
+ * (e.g. your own save request failed) rather than losing the user's
801
+ * in-progress edits — `<DataGrid>` surfaces nothing of its own for that
802
+ * case, so show your own error UI. Resolve (or return) normally to clear
803
+ * every pending edit that was included in this call.
804
+ */
805
+ onSave: (edits: EditedRow<TRow>[]) => void | Promise<void>;
806
+ /** Called after the built-in Discard button clears every pending edit. Omit for no extra behavior beyond that clear. */
807
+ onDiscard?: () => void;
808
+ /**
809
+ * Label for the Save button. A plain string renders as-is — no
810
+ * placeholder substitution, since there's no templating/i18n layer here.
811
+ * Pass a function instead for a count-aware label, e.g. `(count) =>
812
+ * `Apply ${count} change${count === 1 ? "" : "s"}``. Defaults to that
813
+ * exact "Save N change(s)" function.
814
+ */
815
+ saveLabel?: string | ((changedRowCount: number) => ReactNode);
816
+ /** Label for the Discard button. Same convention as `saveLabel`. Defaults to `"Discard"`. */
817
+ discardLabel?: string | ((changedRowCount: number) => ReactNode);
818
+ /**
819
+ * Disables the Save and Discard buttons and shows Save in a pending
820
+ * state — set this while your own `onSave` promise is in flight (a
821
+ * caller-driven flag, not something `<DataGrid>` infers on its own, since
822
+ * `onSave` may resolve without `<DataGrid>` ever finding out your own
823
+ * server call afterward e.g. still triggered a background refetch).
824
+ * Defaults to false.
825
+ */
826
+ saving?: boolean;
827
+ }
619
828
  interface DataGridProps<TRow> {
620
829
  columns: ColumnDef<TRow>[];
621
830
  dataSource: DataSource<TRow>;
@@ -795,6 +1004,16 @@ interface DataGridProps<TRow> {
795
1004
  onExpandedGroupsChange?: (expanded: Record<string, boolean>) => void;
796
1005
  /** Alternates body row backgrounds for readability on wide/dense tables. Defaults to true. */
797
1006
  zebra?: boolean;
1007
+ /**
1008
+ * Enables the built-in inline-editing workflow: any column with
1009
+ * `editable` set renders as an interactive editor, edits accumulate
1010
+ * locally, and a Save/Discard bar appears above the grid once at least
1011
+ * one exists. Omit entirely to disable — every `editable` column then
1012
+ * just falls back to its normal static `cell`/`defaultFormat` rendering,
1013
+ * same as if `editable` were never set. See `DataGridEditingOptions` for
1014
+ * the full contract.
1015
+ */
1016
+ editing?: DataGridEditingOptions<TRow>;
798
1017
  }
799
1018
 
800
1019
  /**
@@ -805,7 +1024,7 @@ interface DataGridProps<TRow> {
805
1024
  * (`"server"`). Column filter UI defaults to the type-appropriate widget
806
1025
  * from `src/filter` unless a column supplies its own `cell` renderer.
807
1026
  */
808
- declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, }: DataGridProps<TRow>): ReactElement;
1027
+ declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, editing, }: DataGridProps<TRow>): ReactElement;
809
1028
 
810
1029
  interface GridStateController {
811
1030
  state: GridState;
@@ -1126,4 +1345,4 @@ interface UseTreeStateOptions<TRow> extends TreeAccessors<TRow> {
1126
1345
  */
1127
1346
  declare function useTreeState<TRow>({ data, getRowId, getChildren, hasChildren, onLoadChildren, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, }: UseTreeStateOptions<TRow>): TreeStateController<TRow>;
1128
1347
 
1129
- export { ActionsMenu, type BaseColumn, type BooleanColumn, BooleanFilter, type ClientProcessingResult, type ColumnAlign, type ColumnDef, type ColumnGroup, ColumnSelector, type ColumnSelectorProps, type ColumnVisibility, type CompositeFilterDescriptor, DataGrid, type DataGridProps, type DataSource, type DateColumn, DateRangeFilter, type DebouncedCallback, type EnumColumn, EnumFilter, type EnumOption, type FilterDescriptor, type FilterOperator, type FilterWidgetProps, type FlatTreeRow, type GridState, type GridStateController, type MenuItem, type MenuItemContext, type NumberColumn, NumberComparisonFilter, NumberHistogramFilter, type NumberHistogramFilterProps, NumberRangeFilter, type ResolvedMenuItem, type SortDescriptor, type StringColumn, StringFilter, type TreeAccessors, TreeDataGrid, type TreeDataGridProps, type TreeStateController, UNARY_OPERATORS, type UseTreeStateOptions, alignClassName, canHideColumn, canHideGroup, countVisible, defaultAlign, defaultFormat, evaluateFilter, facetedNumberValues, fieldKey, flattenTree, getColumnValue, groupColumns, isColumnVisible, isCompositeFilterDescriptor, isFilterDescriptor, isFilterable, isSortable, processClientData, renderDefaultFilterWidget, resolveMenuItems, storageKeyFor, useDebouncedCallback, useGridState, useTreeState };
1348
+ export { ActionsMenu, type BaseColumn, type BooleanColumn, BooleanEditor, BooleanFilter, type ClientProcessingResult, type ColumnAlign, type ColumnDef, type ColumnGroup, ColumnSelector, type ColumnSelectorProps, type ColumnVisibility, type CompositeFilterDescriptor, DataGrid, type DataGridEditingOptions, type DataGridProps, type DataSource, type DateColumn, DateEditor, DateRangeFilter, type DebouncedCallback, EditFieldError, type EditWidgetProps, type EditedRow, type EnumColumn, EnumEditor, EnumFilter, type EnumOption, type FilterDescriptor, type FilterOperator, type FilterWidgetProps, type FlatTreeRow, type GridState, type GridStateController, type MenuItem, type MenuItemContext, type NumberColumn, NumberComparisonFilter, NumberEditor, NumberHistogramFilter, type NumberHistogramFilterProps, NumberRangeFilter, type ResolvedMenuItem, type SortDescriptor, type StringColumn, StringEditor, StringFilter, type TreeAccessors, TreeDataGrid, type TreeDataGridProps, type TreeStateController, UNARY_OPERATORS, type UseTreeStateOptions, alignClassName, canHideColumn, canHideGroup, countVisible, defaultAlign, defaultFormat, editErrorId, evaluateFilter, facetedNumberValues, fieldKey, flattenTree, getColumnValue, groupColumns, isColumnVisible, isCompositeFilterDescriptor, isEditable, isFilterDescriptor, isFilterable, isSortable, processClientData, renderDefaultEditWidget, renderDefaultFilterWidget, resolveMenuItems, storageKeyFor, toDate, useDebouncedCallback, useGridState, useTreeState };