@bmsuisse/datagrid 0.26.0 → 0.27.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
@@ -172,9 +172,20 @@ interface BaseColumn<TRow> {
172
172
  * A cell doesn't become an editor just because this is true, though:
173
173
  * clicking (or Enter/Space-ing) any editable cell in a row is what
174
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.
175
+ * rest of the grid's rows stay static until clicked into themselves. At
176
+ * most one row is active at a time: activating a row deactivates whatever
177
+ * row was active before it (edit row by row, not several open at once) —
178
+ * this does NOT discard the row you switch away from, its own pending
179
+ * edits stay exactly as they were, just no longer shown as editors.
180
+ *
181
+ * Until a cell is activated, its content renders inside a clickable
182
+ * `<span role="button">` (this is what makes clicking it activate the
183
+ * row) — a column that's both `editable` and has a custom `cell`
184
+ * returning its own focusable content (a link, a button) produces nested
185
+ * interactive elements once wrapped this way. Prefer a non-focusable
186
+ * element (styled to look like a link, e.g.) in `cell` for a column
187
+ * that's also `editable`, the same caution `renderHeader` needs under
188
+ * `sortable`.
178
189
  */
179
190
  editable?: boolean | ((row: TRow) => boolean);
180
191
  /**
@@ -506,7 +517,7 @@ declare function defaultFormat<TRow>(column: ColumnDef<TRow>, value: unknown): s
506
517
 
507
518
  /**
508
519
  * One row carrying at least one pending edit, as handed to
509
- * `DataGridEditingOptions.onSave`. `values` is keyed by column id and holds
520
+ * `EditingOptions.onSave`. `values` is keyed by column id and holds
510
521
  * only the columns that actually changed — not a full patched `TRow` — so
511
522
  * this works uniformly for `accessorKey` columns (which have one obvious
512
523
  * field to write back into) and `accessorFn` columns (which may not: a
@@ -519,6 +530,58 @@ interface EditedRow<TRow> {
519
530
  row: TRow;
520
531
  values: Record<string, unknown>;
521
532
  }
533
+ /**
534
+ * Drives the built-in inline-editing workflow shared by `<DataGrid>` and
535
+ * `<TreeDataGrid>`: cells in a column with `editable` set become
536
+ * interactive editors, and every change accumulates locally (never written
537
+ * back onto the caller's `data`, never sent anywhere on its own) until the
538
+ * user clicks the Save button this renders in a bar above the grid — or
539
+ * Discard, to drop everything back to unedited. There is no partial/per-cell
540
+ * autosave; see `onSave` below for the all-or-nothing commit point.
541
+ *
542
+ * This package has no translation/i18n layer anywhere — `saveLabel`/
543
+ * `discardLabel` are plain strings (or a count-aware function) you supply
544
+ * directly, the same convention every other caller-facing string in this
545
+ * package already follows (`header`, menu item labels, etc.).
546
+ */
547
+ interface EditingOptions<TRow> {
548
+ /**
549
+ * Called once, when the user clicks Save, with every row currently
550
+ * holding at least one pending edit and no validation error
551
+ * (`validateEdit`-failing rows are excluded — Save itself stays disabled
552
+ * while ANY row has one, so in practice this only ever fires once none
553
+ * do). Pending-edit state stays exactly as it was until this resolves:
554
+ * **throw or reject to leave every pending edit in place** (e.g. your own
555
+ * save request failed) rather than losing the user's in-progress edits —
556
+ * nothing of this package's own surfaces for that case, so show your own
557
+ * error UI. Resolve (or return) normally to clear every pending edit that
558
+ * was included in this call — and only those; an edit made to a different
559
+ * cell while this was still in flight is never touched by this call's own
560
+ * resolution, whenever it lands.
561
+ */
562
+ onSave: (edits: EditedRow<TRow>[]) => void | Promise<void>;
563
+ /** Called after the built-in Discard button clears every pending edit. Omit for no extra behavior beyond that clear. */
564
+ onDiscard?: () => void;
565
+ /**
566
+ * Label for the Save button. A plain string renders as-is — no
567
+ * placeholder substitution, since there's no templating/i18n layer here.
568
+ * Pass a function instead for a count-aware label, e.g. `(count) =>
569
+ * `Apply ${count} change${count === 1 ? "" : "s"}``. Defaults to that
570
+ * exact "Save N change(s)" function.
571
+ */
572
+ saveLabel?: string | ((changedRowCount: number) => ReactNode);
573
+ /** Label for the Discard button. Same convention as `saveLabel`. Defaults to `"Discard"`. */
574
+ discardLabel?: string | ((changedRowCount: number) => ReactNode);
575
+ /**
576
+ * Disables the Save and Discard buttons and shows Save in a pending
577
+ * state — set this while your own `onSave` promise is in flight (a
578
+ * caller-driven flag, not something this package infers on its own, since
579
+ * `onSave` may resolve without the grid ever finding out your own server
580
+ * call afterward e.g. still triggered a background refetch). Defaults to
581
+ * false.
582
+ */
583
+ saving?: boolean;
584
+ }
522
585
 
523
586
  /**
524
587
  * Contract every built-in edit widget implements — the `renderEditCell`
@@ -600,6 +663,19 @@ declare function EnumEditor<TRow>({ column, rowId, value, onChange, error, autoF
600
663
  * native number input. Emits `null` for an emptied field (not `NaN` or `""`)
601
664
  * so `validateEdit`/`onSave` see an explicit "cleared" value; otherwise emits
602
665
  * a real `number`, never the input's raw string.
666
+ *
667
+ * Keeps its own local `text` state instead of deriving the input's `value`
668
+ * straight from the `value` prop on every render — a native
669
+ * `type="number"` input's own `.value` getter reads back as `""` while the
670
+ * user is mid-typing a string that isn't yet a complete valid float ("12.",
671
+ * "-", "1e"), even though the field still visually shows what they typed.
672
+ * Deriving the controlled `value` prop straight from a re-parsed number
673
+ * would feed that `""` back in on every such keystroke, wiping the decimal
674
+ * point/minus sign before the user can finish typing it. `lastEmittedRef` +
675
+ * the effect below only resync `text` from an EXTERNAL change to `value`
676
+ * (Escape-revert, switching rows and back, a row's data refetched) — the
677
+ * same pattern `NumberComparisonFilter` already uses for the identical
678
+ * reason; see that component's own doc.
603
679
  */
604
680
  declare function NumberEditor<TRow>({ column, rowId, value, onChange, error, autoFocus, }: EditWidgetProps<NumberColumn<TRow>>): ReactElement;
605
681
 
@@ -616,6 +692,98 @@ declare function StringEditor<TRow>({ column, rowId, value, onChange, error, aut
616
692
  */
617
693
  declare function renderDefaultEditWidget<TRow>(column: ColumnDef<TRow>, rowId: string, value: unknown, onChange: (next: unknown) => void, error?: string, autoFocus?: boolean): ReactElement;
618
694
 
695
+ /** One row's accumulated pending edits. */
696
+ interface PendingRowEdit<TRow> {
697
+ /** The row as it looked when its first still-pending edit was made this session. */
698
+ row: TRow;
699
+ values: Map<string, unknown>;
700
+ }
701
+ /**
702
+ * Everything a cell-rendering path (`renderEditableCell`) needs to decide
703
+ * whether/how to render a given cell as an editor. Bundled into one object
704
+ * so `<DataGrid>` can hand it through a `useRef` (see that component's own
705
+ * doc on why — TanStack's `flexRender` treats `columnDef.cell` as a
706
+ * component type, so a closure that changes identity on every edit would
707
+ * remount the whole table body) while `<TreeDataGrid>`, which has no such
708
+ * memoized column-def layer, can just read the hook's plain `ctx` value
709
+ * directly every render.
710
+ */
711
+ interface EditingCellContext<TRow> {
712
+ pendingEdits: Map<string, PendingRowEdit<TRow>>;
713
+ editErrors: Map<string, Map<string, string>>;
714
+ /**
715
+ * The one row currently showing editors, or `null`. Deliberately NOT the
716
+ * same thing as "has a pending edit": a row can be active with nothing
717
+ * changed yet (just clicked into). At most one at a time — activating a
718
+ * row deactivates whatever was active before it (edit row by row, not
719
+ * several open at once). This does NOT discard the row you switch away
720
+ * from: its own pending edits stay exactly as they were in `pendingEdits`,
721
+ * just no longer shown as editors.
722
+ */
723
+ activeRowId: string | null;
724
+ /** Set only by the click that activated a row — see `EditWidgetProps.autoFocus`'s own doc for why this exists. */
725
+ autoFocusTarget: {
726
+ rowId: string;
727
+ columnId: string;
728
+ } | null;
729
+ getRowId: (row: TRow) => string;
730
+ onEdit: (column: ColumnDef<TRow>, row: TRow, value: unknown) => void;
731
+ /** Activates `rowId` (deactivating whatever row was active before it) and requests autofocus on `columnId`'s editor specifically — the column whose static cell was actually clicked. */
732
+ onActivateRow: (rowId: string, columnId: string) => void;
733
+ }
734
+ interface EditingState<TRow> {
735
+ pendingEdits: Map<string, PendingRowEdit<TRow>>;
736
+ editErrors: Map<string, Map<string, string>>;
737
+ hasEditErrors: boolean;
738
+ /** The live editing context — read directly during a render body that has no remount-avoidance concerns of its own (e.g. `<TreeDataGrid>`'s plain per-row render). */
739
+ ctx: EditingCellContext<TRow> | undefined;
740
+ /** Same context, mirrored into a ref every render — for a caller (`<DataGrid>`) whose cell-rendering closures must stay referentially stable and so can't close over `ctx` directly. */
741
+ ctxRef: {
742
+ current: EditingCellContext<TRow> | undefined;
743
+ };
744
+ handleSaveEdits: () => Promise<void>;
745
+ handleDiscardEdits: () => void;
746
+ }
747
+ /**
748
+ * Owns the full accumulate-then-save inline-editing state machine: pending
749
+ * edits, validation errors, which row is active, autofocus targeting, and
750
+ * the Save/Discard commit logic — shared by `<DataGrid>` and
751
+ * `<TreeDataGrid>` so the tricky parts (the async-save race around edits
752
+ * made while `onSave` is in flight, the one-shot autoFocus consumption) only
753
+ * exist, and only need fixing, in one place.
754
+ */
755
+ declare function useEditingState<TRow>(editing: EditingOptions<TRow> | undefined, getRowId: (row: TRow) => string): EditingState<TRow>;
756
+
757
+ /**
758
+ * Renders one cell for a column that may or may not be `editable`, given the
759
+ * live editing context (or `undefined` when no `editing` prop was supplied
760
+ * at all). Shared by `<DataGrid>` and `<TreeDataGrid>` so the click-to-
761
+ * activate/Escape-to-revert/autoFocus behavior — and its bug fixes — only
762
+ * exist in one place; see `useEditingState`'s own module doc.
763
+ *
764
+ * Not itself responsible for choosing WHICH raw value to start from
765
+ * (`<DataGrid>` reads it off TanStack's `CellContext.getValue()`,
766
+ * `<TreeDataGrid>` off a plain accessor) — callers pass `rawValue` in.
767
+ */
768
+ declare function renderEditableCell<TRow>(column: ColumnDef<TRow>, row: TRow, rawValue: unknown, ctx: EditingCellContext<TRow> | undefined): ReactNode;
769
+
770
+ interface EditingBarProps<TRow> {
771
+ editing: EditingOptions<TRow>;
772
+ pendingRowCount: number;
773
+ editErrors: Map<string, Map<string, string>>;
774
+ onSave: () => void;
775
+ onDiscard: () => void;
776
+ /** `data-testid` for the outer bar element. Defaults to `"datagrid-edit-bar"` (also the default for the Save/Discard buttons' own testids, unless overridden below). */
777
+ testIdPrefix?: string;
778
+ }
779
+ /**
780
+ * The Save/Discard bar `<DataGrid>` and `<TreeDataGrid>` both render above
781
+ * themselves once at least one pending edit exists — shared so the two
782
+ * grids' editing UI (and its `aria-live`/row-naming behavior) can't drift
783
+ * apart from each other.
784
+ */
785
+ declare function EditingBar<TRow>({ editing, pendingRowCount, editErrors, onSave, onDiscard, testIdPrefix, }: EditingBarProps<TRow>): ReactElement;
786
+
619
787
  /**
620
788
  * Column visibility keyed by `ColumnDef.id`. A missing key means visible —
621
789
  * this matches TanStack Table's own `VisibilityState` convention, so a
@@ -775,56 +943,6 @@ interface DataGridVirtualizeOptions {
775
943
  /** Whether more rows exist beyond what's currently loaded. `onEndReached` never fires while this is `false`. Defaults to `true`. */
776
944
  hasMore?: boolean;
777
945
  }
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
- }
828
946
  interface DataGridProps<TRow> {
829
947
  columns: ColumnDef<TRow>[];
830
948
  dataSource: DataSource<TRow>;
@@ -1010,10 +1128,10 @@ interface DataGridProps<TRow> {
1010
1128
  * locally, and a Save/Discard bar appears above the grid once at least
1011
1129
  * one exists. Omit entirely to disable — every `editable` column then
1012
1130
  * 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.
1131
+ * same as if `editable` were never set. See `EditingOptions` for the full
1132
+ * contract — shared with `<TreeDataGrid>`'s own `editing` prop.
1015
1133
  */
1016
- editing?: DataGridEditingOptions<TRow>;
1134
+ editing?: EditingOptions<TRow>;
1017
1135
  }
1018
1136
 
1019
1137
  /**
@@ -1273,6 +1391,18 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
1273
1391
  isRowSelectionDisabled?: (row: TRow) => boolean;
1274
1392
  /** Alternates body row backgrounds for readability on wide/dense tables. Defaults to true. */
1275
1393
  zebra?: boolean;
1394
+ /**
1395
+ * Enables the same built-in inline-editing workflow `<DataGrid>` has:
1396
+ * any column with `editable` set renders as an interactive editor
1397
+ * (clicking one editable cell activates every editable column in that
1398
+ * row, one row active at a time), edits accumulate locally, and a
1399
+ * Save/Discard bar appears above the tree once at least one exists. Omit
1400
+ * entirely to disable — every `editable` column then just falls back to
1401
+ * its normal static `cell`/`defaultFormat` rendering, same as if
1402
+ * `editable` were never set. See `EditingOptions` (shared with
1403
+ * `<DataGrid>`) for the full contract.
1404
+ */
1405
+ editing?: EditingOptions<TRow>;
1276
1406
  }
1277
1407
 
1278
1408
  /**
@@ -1289,7 +1419,7 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
1289
1419
  * column's cell matches `treeColumnId` (defaults to `columns[0]`); every
1290
1420
  * other column renders exactly like it would in `<DataGrid>`.
1291
1421
  */
1292
- declare function TreeDataGrid<TRow>({ columns, data, treeColumnId, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, selectedIds: controlledSelectedIds, onSelectedIdsChange, getRowSelectionState, isRowSelectionDisabled, zebra, }: TreeDataGridProps<TRow>): ReactElement;
1422
+ declare function TreeDataGrid<TRow>({ columns, data, treeColumnId, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, selectedIds: controlledSelectedIds, onSelectedIdsChange, getRowSelectionState, isRowSelectionDisabled, zebra, editing, }: TreeDataGridProps<TRow>): ReactElement;
1293
1423
 
1294
1424
  interface FlatTreeRow<TRow> {
1295
1425
  row: TRow;
@@ -1345,4 +1475,4 @@ interface UseTreeStateOptions<TRow> extends TreeAccessors<TRow> {
1345
1475
  */
1346
1476
  declare function useTreeState<TRow>({ data, getRowId, getChildren, hasChildren, onLoadChildren, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, }: UseTreeStateOptions<TRow>): TreeStateController<TRow>;
1347
1477
 
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 };
1478
+ 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 DataGridProps, type DataSource, type DateColumn, DateEditor, DateRangeFilter, type DebouncedCallback, EditFieldError, type EditWidgetProps, type EditedRow, EditingBar, type EditingBarProps, type EditingCellContext, type EditingOptions, type EditingState, 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 PendingRowEdit, 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, renderEditableCell, resolveMenuItems, storageKeyFor, toDate, useDebouncedCallback, useEditingState, useGridState, useTreeState };