@linzjs/step-ag-grid 29.15.0 → 29.19.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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/index.css +27 -0
  3. package/dist/src/components/Grid.d.ts +3 -2
  4. package/dist/src/components/gridFilter/GridFilterColumnsMultiSelect.d.ts +2 -1
  5. package/dist/src/components/gridForm/GridFormInlineTextInput.d.ts +15 -0
  6. package/dist/src/components/gridForm/index.d.ts +1 -0
  7. package/dist/src/components/gridHook/useGridRangeSelection.d.ts +1 -1
  8. package/dist/src/components/gridPopoverEdit/GridInlineTextInput.d.ts +5 -0
  9. package/dist/src/components/gridPopoverEdit/index.d.ts +1 -0
  10. package/dist/src/contexts/GridContext.d.ts +1 -1
  11. package/dist/src/utils/__tests__/storybookTestUtil.ts +5 -1
  12. package/dist/step-ag-grid.cjs +233 -90
  13. package/dist/step-ag-grid.cjs.map +1 -1
  14. package/dist/step-ag-grid.esm.js +232 -91
  15. package/dist/step-ag-grid.esm.js.map +1 -1
  16. package/package.json +14 -12
  17. package/src/components/Grid.tsx +33 -10
  18. package/src/components/GridPopoverHook.tsx +5 -2
  19. package/src/components/gridFilter/GridFilterColumnsMultiSelect.tsx +23 -4
  20. package/src/components/gridForm/GridFormInlineTextInput.tsx +119 -0
  21. package/src/components/gridForm/index.ts +1 -0
  22. package/src/components/gridHook/useGridContextMenu.tsx +3 -1
  23. package/src/components/gridHook/useGridCopy.ts +5 -2
  24. package/src/components/gridHook/useGridRangeSelection.ts +25 -4
  25. package/src/components/gridPopoverEdit/GridInlineTextInput.ts +13 -0
  26. package/src/components/gridPopoverEdit/index.ts +1 -0
  27. package/src/contexts/GridContext.tsx +6 -1
  28. package/src/contexts/GridContextProvider.tsx +30 -13
  29. package/src/stories/grid/GridFilterColumnsMultiSelect.stories.tsx +52 -7
  30. package/src/stories/grid/GridInlineText.stories.tsx +185 -0
  31. package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +3 -3
  32. package/src/styles/Grid.scss +5 -0
  33. package/src/styles/GridFormInlineTextInput.scss +23 -0
  34. package/src/styles/index.scss +1 -1
  35. package/src/utils/__tests__/storybookTestUtil.ts +5 -1
@@ -2634,8 +2634,9 @@ const useGridContextMenu = ({ contextMenuSelectRow, contextMenu: ContextMenu, co
2634
2634
  const clickedRowRef = React.useRef(null);
2635
2635
  const eventRef = React.useRef(undefined);
2636
2636
  const openMenu = React.useCallback((e) => {
2637
- if (!e)
2637
+ if (!e) {
2638
2638
  return;
2639
+ }
2639
2640
  prePopupOps();
2640
2641
  setAnchorPoint({ x: e.clientX, y: e.clientY });
2641
2642
  setOpen(true);
@@ -2905,7 +2906,11 @@ const useGridCopy = ({ ranges, rangeStartRef, rangeEndRef, hasSelectedMoreThanOn
2905
2906
  contextMenu: (GridRangeSelectContextMenu),
2906
2907
  context: { onCopy, copyType, setCopyType },
2907
2908
  });
2908
- const rangeSelectInterceptContextMenu = React.useCallback((event) => rangeStartRef.current == null ? cellContextMenu(event) : rangeSelectContextMenu(event), [cellContextMenu, rangeSelectContextMenu]);
2909
+ const rangeSelectInterceptContextMenu = React.useCallback((event) => {
2910
+ return hasSelectedMoreThanOneCellRef.current && rangeStartRef.current !== null
2911
+ ? rangeSelectContextMenu(event)
2912
+ : cellContextMenu(event);
2913
+ }, [cellContextMenu, rangeSelectContextMenu]);
2909
2914
  return {
2910
2915
  onCopyEvent,
2911
2916
  rangeSelectInterceptContextMenu,
@@ -3029,6 +3034,23 @@ const useGridRangeSelection = ({ enableRangeSelection, gridDivRef, rangeStartRef
3029
3034
  rangeEndRef.current = null;
3030
3035
  updateRangeSelectionCellClasses();
3031
3036
  }, [updateRangeSelectionCellClasses]);
3037
+ const mouseDownRef = React.useRef([0, 0]);
3038
+ const trackMouseDown = React.useCallback((e) => {
3039
+ mouseDownRef.current = [e.screenX, e.screenY];
3040
+ }, []);
3041
+ const trackMouseUp = React.useCallback((e) => {
3042
+ if (rangeEndRef.current) {
3043
+ rangeEndRef.current.clickLocation = [e.screenX, e.screenY];
3044
+ }
3045
+ }, []);
3046
+ React.useEffect(() => {
3047
+ document.addEventListener('mousedown', trackMouseDown, { capture: true });
3048
+ document.addEventListener('mouseup', trackMouseUp, { capture: true });
3049
+ return () => {
3050
+ document.removeEventListener('mousemove', trackMouseDown, { capture: true });
3051
+ document.removeEventListener('mouseup', trackMouseUp, { capture: true });
3052
+ };
3053
+ }, []);
3032
3054
  const onCellMouseOver = React.useCallback((e, mouseDown) => {
3033
3055
  if (!enableRangeSelection) {
3034
3056
  return;
@@ -3042,19 +3064,20 @@ const useGridRangeSelection = ({ enableRangeSelection, gridDivRef, rangeStartRef
3042
3064
  rangeEndRef.current = {
3043
3065
  rowId: e.node.data.id,
3044
3066
  colId: e.column.getColId(),
3045
- timestamp: Date.now(),
3067
+ clickLocation: [0, 0],
3046
3068
  };
3047
3069
  if (mouseDown) {
3048
3070
  const sortedNodes = [];
3049
3071
  e.api.forEachNodeAfterFilterAndSort((node) => sortedNodes.push(node));
3050
3072
  rangeSortedNodesRef.current = sortedNodes;
3051
3073
  setRefreshIntervalEnabled(true);
3052
- rangeStartRef.current = { ...rangeEndRef.current };
3074
+ rangeStartRef.current = { ...rangeEndRef.current, clickLocation: mouseDownRef.current };
3053
3075
  }
3054
3076
  if (rangeStartRef.current &&
3055
3077
  rangeEndRef.current &&
3056
3078
  (rangeStartRef.current.rowId !== rangeEndRef.current.rowId ||
3057
3079
  rangeStartRef.current.colId !== rangeEndRef.current.colId)) {
3080
+ e.api.stopEditing();
3058
3081
  hasSelectedMoreThanOneCellRef.current = true;
3059
3082
  window.getSelection()?.removeAllRanges();
3060
3083
  }
@@ -3259,7 +3282,7 @@ defaultPostSort = true, rowData, rowHeight = theme === 'ag-theme-step-default' ?
3259
3282
  // ─── Selection ────────────────────────────────
3260
3283
  autoSelectFirstRow, enableRangeSelection = false, externalSelectedIds, externalSelectedItems, selectColumnPinned = 'left', selectable, setExternalSelectedIds, setExternalSelectedItems,
3261
3284
  // ─── Editing ──────────────────────────────────
3262
- singleClickEdit = false, stopEditingWhenCellsLoseFocus = false,
3285
+ singleClickEdit = false, enableMultilineBulkEdit = false,
3263
3286
  // ─── Context Menu ─────────────────────────────
3264
3287
  contextMenuSelectRow = false, contextMenu,
3265
3288
  // ─── Callbacks / Events ───────────────────────
@@ -3541,10 +3564,16 @@ maxInitialWidth,
3541
3564
  * When grid is ready set the apis to the grid context and sync selected items to grid.
3542
3565
  */
3543
3566
  const onGridReady = React.useCallback((event) => {
3544
- setApis(event.api, hasExternallySelectedItems, dataTestId);
3567
+ setApis(event.api, hasExternallySelectedItems, enableMultilineBulkEdit, dataTestId);
3545
3568
  event.api.showNoRowsOverlay();
3546
3569
  synchroniseExternallySelectedItemsToGrid();
3547
- }, [dataTestId, hasExternallySelectedItems, setApis, synchroniseExternallySelectedItemsToGrid]);
3570
+ }, [
3571
+ dataTestId,
3572
+ enableMultilineBulkEdit,
3573
+ hasExternallySelectedItems,
3574
+ setApis,
3575
+ synchroniseExternallySelectedItemsToGrid,
3576
+ ]);
3548
3577
  /**
3549
3578
  * When the grid is being initialized the data may be empty.
3550
3579
  * This will resize columns when we have at least one row.
@@ -3587,6 +3616,11 @@ maxInitialWidth,
3587
3616
  * Handle double click edit
3588
3617
  */
3589
3618
  const onCellDoubleClick = React.useCallback((event) => {
3619
+ const cellEditing = event.api.getEditingCells()?.[0];
3620
+ if (cellEditing && cellEditing.colId === event.column.getColId() && cellEditing.rowIndex === event.rowIndex) {
3621
+ // Ignore double click in already editing cell
3622
+ return;
3623
+ }
3590
3624
  const editable = fnOrVar(event.colDef?.editable, event);
3591
3625
  if (editable && !invokeEditAction(event)) {
3592
3626
  void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
@@ -3671,18 +3705,25 @@ maxInitialWidth,
3671
3705
  * Handle single click edits
3672
3706
  */
3673
3707
  const onCellClicked = React.useCallback((event) => {
3674
- if (rangeStartRef.current && rangeEndRef.current) {
3708
+ if (enableRangeSelection && rangeStartRef.current && rangeEndRef.current) {
3675
3709
  // This is to detect difference between a single click and a click drag return to cell.
3676
- if (rangeEndRef.current.timestamp - rangeStartRef.current.timestamp < 100) {
3677
- clearRangeSelection();
3710
+ const maxDistanceBetweenC = Math.max(Math.abs(rangeEndRef.current.clickLocation[0] - rangeStartRef.current.clickLocation[0]), Math.abs(rangeEndRef.current.clickLocation[1] - rangeStartRef.current.clickLocation[1]));
3711
+ if (maxDistanceBetweenC > 8) {
3712
+ return;
3678
3713
  }
3679
- return;
3714
+ clearRangeSelection();
3680
3715
  }
3681
3716
  const editable = fnOrVar(event.colDef?.editable, event);
3682
3717
  if ((editable && event.colDef.singleClickEdit) ?? singleClickEdit) {
3718
+ const editingCell = event.api.getEditingCells()[0];
3719
+ // If we are editing and single click we don't want the editing to stop
3720
+ if (editingCell && editingCell.colId === event.column.getColId() && editingCell.rowIndex === event.rowIndex) {
3721
+ return;
3722
+ }
3723
+ event.api.setFocusedCell(event.rowIndex ?? 0, event.column.getColId(), event.rowPinned);
3683
3724
  void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
3684
3725
  }
3685
- }, [clearRangeSelection, singleClickEdit, startCellEditing]);
3726
+ }, [clearRangeSelection, enableRangeSelection, singleClickEdit, startCellEditing]);
3686
3727
  /**
3687
3728
  * Set of column I'd's that are prevented from auto-sizing as they are user set
3688
3729
  */
@@ -3963,7 +4004,7 @@ maxInitialWidth,
3963
4004
  event.api.sizeColumnsToFit();
3964
4005
  }
3965
4006
  }, [sizeColumns]);
3966
- return (jsxRuntime.jsxs("div", { "data-testid": dataTestId, className: clsx('Grid-container', theme, 'theme-specific', staleGrid && 'Grid-sortIsStale', gridReady && rowData && autoSized && 'Grid-ready'), children: [contextMenuComponent, rangeSelectContextMenuComponent, jsxRuntime.jsx("div", { style: { flex: 1 }, ref: gridDivRef, onCopy: onCopyEvent, children: jsxRuntime.jsx(agGridReact.AgGridReact, { theme: 'legacy', domLayout: params.domLayout, defaultColDef: defaultColDef, columnDefs: columnDefsAdjusted, getRowId: getRowId, rowData: rowData, alwaysShowVerticalScroll: params.alwaysShowVerticalScroll, animateRows: params.animateRows ?? false, doesExternalFilterPass: doesExternalFilterPass, isExternalFilterPresent: isExternalFilterPresent, maintainColumnOrder: true, noRowsOverlayComponent: noRowsOverlayComponent, onCellClicked: onCellClicked, onCellContextMenu: rangeSelectInterceptContextMenu, onCellDoubleClicked: onCellDoubleClick, onCellEditingStopped: onCellEditingStopped, onCellFocused: onCellFocused, onCellKeyDown: onCellKeyPress, onCellMouseDown: onCellMouseDown, onCellMouseOver: onCellMouseOver, onColumnMoved: columnMoved, onColumnResized: onColumnResized, onColumnVisible: () => void setInitialContentSize(), onGridReady: onGridReady, onGridSizeChanged: onGridSizeChanged, onModelUpdated: onModelUpdated, onRowClicked: params.onRowClicked, onRowDataUpdated: onRowDataUpdated, onRowDoubleClicked: params.onRowDoubleClicked, onRowDragCancel: clearHighlightRowClasses, onRowDragEnd: onRowDragEnd, onRowDragMove: onRowDragMove, onSelectionChanged: synchroniseExternalStateToGridSelection, onSortChanged: onSortChanged, pinnedBottomRowData: params.pinnedBottomRowData, pinnedTopRowData: params.pinnedTopRowData, postSortRows: params.onRowDragEnd || !defaultPostSort ? undefined : postSortRows, preventDefaultOnContextMenu: true, quickFilterParser: quickFilterParser, rowClassRules: params.rowClassRules, rowDragText: params.rowDragText, rowHeight: rowHeight, rowSelection: selectable
4007
+ return (jsxRuntime.jsxs("div", { "data-testid": dataTestId, className: clsx('Grid-container', theme, 'theme-specific', staleGrid && 'Grid-sortIsStale', gridReady && rowData && autoSized && 'Grid-ready', params.hideSelectedRow && 'Grid-hideSelectedRow'), children: [contextMenuComponent, rangeSelectContextMenuComponent, jsxRuntime.jsx("div", { style: { flex: 1 }, ref: gridDivRef, onCopy: onCopyEvent, children: jsxRuntime.jsx(agGridReact.AgGridReact, { theme: 'legacy', domLayout: params.domLayout, defaultColDef: defaultColDef, columnDefs: columnDefsAdjusted, getRowId: getRowId, rowData: rowData, alwaysShowVerticalScroll: params.alwaysShowVerticalScroll, animateRows: params.animateRows ?? false, doesExternalFilterPass: doesExternalFilterPass, isExternalFilterPresent: isExternalFilterPresent, maintainColumnOrder: true, noRowsOverlayComponent: noRowsOverlayComponent, onCellClicked: onCellClicked, onCellContextMenu: rangeSelectInterceptContextMenu, onCellDoubleClicked: onCellDoubleClick, onCellEditingStopped: onCellEditingStopped, onCellFocused: onCellFocused, onCellKeyDown: onCellKeyPress, onCellMouseDown: onCellMouseDown, onCellMouseOver: onCellMouseOver, onColumnMoved: columnMoved, onColumnResized: onColumnResized, onColumnVisible: () => void setInitialContentSize(), onGridReady: onGridReady, onGridSizeChanged: onGridSizeChanged, onModelUpdated: onModelUpdated, onRowClicked: params.onRowClicked, onRowDataUpdated: onRowDataUpdated, onRowDoubleClicked: params.onRowDoubleClicked, onRowDragCancel: clearHighlightRowClasses, onRowDragEnd: onRowDragEnd, onRowDragMove: onRowDragMove, onSelectionChanged: synchroniseExternalStateToGridSelection, onSortChanged: onSortChanged, pinnedBottomRowData: params.pinnedBottomRowData, pinnedTopRowData: params.pinnedTopRowData, postSortRows: params.onRowDragEnd || !defaultPostSort ? undefined : postSortRows, preventDefaultOnContextMenu: true, quickFilterParser: quickFilterParser, rowClassRules: params.rowClassRules, rowDragText: params.rowDragText, rowHeight: rowHeight, rowSelection: selectable
3967
4008
  ? {
3968
4009
  enableSelectionWithoutKeys: params.enableSelectionWithoutKeys ?? false,
3969
4010
  enableClickSelection: params.enableClickSelection ?? false,
@@ -3973,7 +4014,7 @@ maxInitialWidth,
3973
4014
  headerCheckbox: false,
3974
4015
  }),
3975
4016
  }
3976
- : undefined, stopEditingWhenCellsLoseFocus: stopEditingWhenCellsLoseFocus, selectionColumnDef: selectionColumnDef, suppressCellFocus: params.suppressCellFocus, suppressClickEdit: true, suppressColumnVirtualisation: suppressColumnVirtualization, suppressStartEditOnTab: true }) })] }));
4017
+ : undefined, selectionColumnDef: selectionColumnDef, suppressCellFocus: params.suppressCellFocus, suppressClickEdit: true, suppressColumnVirtualisation: suppressColumnVirtualization, suppressStartEditOnTab: true }) })] }));
3977
4018
  };
3978
4019
  const quickFilterParser = (filterStr) => {
3979
4020
  // filter is exact matches exactly groups separated by commas
@@ -4249,10 +4290,18 @@ class GridFilterColumnsMultiSelect {
4249
4290
  const norm = this.normalizeCellValue(raw);
4250
4291
  values.add(norm);
4251
4292
  });
4293
+ const { customOrder } = this.params;
4294
+ const orderMap = customOrder?.length ? new Map(customOrder.map((val, idx) => [val, idx])) : null;
4252
4295
  return Array.from(values).sort((a, b) => {
4253
- if (a === EMPTY_KEY && b !== EMPTY_KEY)
4254
- return -1;
4255
- if (b === EMPTY_KEY && a !== EMPTY_KEY)
4296
+ if (orderMap) {
4297
+ const aIdx = orderMap.get(a) ?? Infinity;
4298
+ const bIdx = orderMap.get(b) ?? Infinity;
4299
+ if (aIdx !== bIdx)
4300
+ return aIdx - bIdx;
4301
+ }
4302
+ if (a === EMPTY_KEY)
4303
+ return b === EMPTY_KEY ? 0 : -1;
4304
+ if (b === EMPTY_KEY)
4256
4305
  return 1;
4257
4306
  return a.localeCompare(b);
4258
4307
  });
@@ -4299,9 +4348,12 @@ class GridFilterColumnsMultiSelect {
4299
4348
  return this.gui;
4300
4349
  }
4301
4350
  isFilterActive() {
4302
- return this.selectedValues.size > 0;
4351
+ return this.selectedValues.size !== this.allValues.length;
4303
4352
  }
4304
4353
  doesFilterPass(p) {
4354
+ if (this.selectedValues.size === 0) {
4355
+ return false;
4356
+ }
4305
4357
  const field = this.params.colDef.field;
4306
4358
  if (!p.data || typeof p.data !== 'object') {
4307
4359
  return this.selectedValues.has(EMPTY_KEY);
@@ -4311,7 +4363,10 @@ class GridFilterColumnsMultiSelect {
4311
4363
  return this.selectedValues.has(norm);
4312
4364
  }
4313
4365
  getModel() {
4314
- return this.selectedValues.size > 0 ? { values: Array.from(this.selectedValues) } : null;
4366
+ if (this.selectedValues.size !== this.allValues.length) {
4367
+ return { values: Array.from(this.selectedValues) };
4368
+ }
4369
+ return null;
4315
4370
  }
4316
4371
  setModel(model) {
4317
4372
  this.selectedValues = new Set(model?.values || []);
@@ -4324,9 +4379,10 @@ class GridFilterColumnsMultiSelect {
4324
4379
  }
4325
4380
  }
4326
4381
  }
4327
- const createCheckboxMultiFilterParams = (labels = {}, labelFormatter) => ({
4382
+ const createCheckboxMultiFilterParams = (labels = {}, labelFormatter, customOrder) => ({
4328
4383
  labels: { [EMPTY_KEY]: DEFAULT_EMPTY_LABEL, ...labels },
4329
4384
  labelFormatter,
4385
+ customOrder,
4330
4386
  });
4331
4387
 
4332
4388
  const GridFilterHeaderIconButton = React.forwardRef(function columnsButton({ icon, title, onClick, buttonProps, disabled = false, size = 'md' }, ref) {
@@ -4773,6 +4829,7 @@ const textMatch = (text, filter) => {
4773
4829
 
4774
4830
  const CancelPromise = () => Promise.resolve(true);
4775
4831
  const useGridPopoverHook = ({ className, save, invalid, dontSaveOnExternalClick, }) => {
4832
+ const { resetFocusedCellAfterCellEditing } = useGridContext();
4776
4833
  const { anchorRef, saving, updateValue, stopEditing } = useGridPopoverContext();
4777
4834
  const saveButtonRef = React.useRef(null);
4778
4835
  const [isOpen, setOpen] = React.useState(false);
@@ -4783,6 +4840,7 @@ const useGridPopoverHook = ({ className, save, invalid, dontSaveOnExternalClick,
4783
4840
  if (reason == CloseReason.CANCEL) {
4784
4841
  await updateValue(CancelPromise, 0);
4785
4842
  stopEditing();
4843
+ resetFocusedCellAfterCellEditing();
4786
4844
  return;
4787
4845
  }
4788
4846
  if (invalid?.()) {
@@ -4791,8 +4849,9 @@ const useGridPopoverHook = ({ className, save, invalid, dontSaveOnExternalClick,
4791
4849
  }
4792
4850
  if (await updateValue(save ?? (() => Promise.resolve(true)), reason === CloseReason.TAB_FORWARD ? 1 : reason === CloseReason.TAB_BACKWARD ? -1 : 0)) {
4793
4851
  stopEditing();
4852
+ resetFocusedCellAfterCellEditing();
4794
4853
  }
4795
- }, [invalid, save, stopEditing, updateValue]);
4854
+ }, [invalid, resetFocusedCellAfterCellEditing, save, stopEditing, updateValue]);
4796
4855
  const popoverWrapper = React.useCallback((children) => {
4797
4856
  return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: anchorRef.current && (jsxRuntime.jsxs(ControlledMenu, { state: isOpen ? 'open' : 'closed', portal: true, unmountOnClose: true, anchorRef: anchorRef, saveButtonRef: saveButtonRef, menuClassName: 'step-ag-grid-react-menu', onClose: (event) => {
4798
4857
  // Prevent menu from closing when modals are invoked
@@ -5161,6 +5220,128 @@ const GridFormEditBearing = (props) => {
5161
5220
  : props.formatValue(bearingNumberParser(value)), error: bearingStringValidator(value, props.range), helpText: 'Press enter or tab to save' }) }));
5162
5221
  };
5163
5222
 
5223
+ const TextInputValidator = (props, value, data, context) => {
5224
+ if (value == null)
5225
+ return null;
5226
+ // This can happen because subcomponent is invoked without type safety
5227
+ if (typeof value !== 'string') {
5228
+ return 'Value is not a string';
5229
+ }
5230
+ if (props.required && value.trim() == '') {
5231
+ return 'Must not be empty';
5232
+ }
5233
+ if (props.maxLength && value.length > props.maxLength) {
5234
+ return `Must be no longer than ${props.maxLength} characters`;
5235
+ }
5236
+ if (props.maxBytes && stringByteLengthIsInvalid(value, props.maxBytes)) {
5237
+ return `Must be no longer than ${props.maxBytes} bytes`;
5238
+ }
5239
+ const nf = props.numberFormat;
5240
+ if (nf) {
5241
+ if (value != '' && !isFloat(value)) {
5242
+ return `Must be a valid number`;
5243
+ }
5244
+ if (value != '') {
5245
+ const number = parseFloat(value);
5246
+ if (nf.notZero && number === 0) {
5247
+ return `Must not be 0`;
5248
+ }
5249
+ if (nf.gtMin != null && number <= nf.gtMin) {
5250
+ return `Must be greater than ${nf.gtMin.toLocaleString()}`;
5251
+ }
5252
+ if (nf.geMin != null && number < nf.geMin) {
5253
+ return `Must not be less than ${nf.geMin.toLocaleString()}`;
5254
+ }
5255
+ if (nf.ltMax != null && number >= nf.ltMax) {
5256
+ return `Must be less than ${nf.ltMax.toLocaleString()}`;
5257
+ }
5258
+ if (nf.leMax != null && number > nf.leMax) {
5259
+ return `Must not be greater than ${nf.leMax.toLocaleString()}`;
5260
+ }
5261
+ if (nf.precision != null && value != '') {
5262
+ if (parseFloat(number.toPrecision(nf.precision)) != number) {
5263
+ return `Must have no more than ${nf.precision} digits precision`;
5264
+ }
5265
+ }
5266
+ if (nf.scale != null && value != '') {
5267
+ if (parseFloat(number.toFixed(nf.scale)) != number) {
5268
+ return nf.scale == 0 ? `Must be a whole number` : `Must have no more than ${nf.scale} decimal places`;
5269
+ }
5270
+ }
5271
+ }
5272
+ }
5273
+ if (props.invalid) {
5274
+ return props.invalid(value, data, context);
5275
+ }
5276
+ return null;
5277
+ };
5278
+
5279
+ const GridFormInlineTextInput = (props) => {
5280
+ const { onSave } = props;
5281
+ const { getSelectedRows, prePopupOps } = useGridContext();
5282
+ const { field, value: initialVale, data } = useGridPopoverContext();
5283
+ const initValue = React.useMemo(() => (initialVale == null ? '' : `${initialVale}`), [initialVale]);
5284
+ const [value, setValue] = React.useState(initValue);
5285
+ const invalid = React.useCallback(() => TextInputValidator(props, value, data, {}), [data, props, value]);
5286
+ React.useEffect(() => {
5287
+ prePopupOps();
5288
+ }, [prePopupOps]);
5289
+ const save = React.useCallback(async () => {
5290
+ if (invalid()) {
5291
+ return false;
5292
+ }
5293
+ const trimmedValue = value.trim();
5294
+ // No change, so don't save
5295
+ if (initValue === trimmedValue) {
5296
+ return true;
5297
+ }
5298
+ if (!onSave) {
5299
+ console.error(`No onSave handler for GridFormInlineTextInput "${String(field)}`);
5300
+ return false;
5301
+ }
5302
+ const selectedRows = getSelectedRows();
5303
+ const selectedRowIds = selectedRows.map((data) => data.id);
5304
+ return onSave({
5305
+ selectedRowIds,
5306
+ selectedRows,
5307
+ value: trimmedValue,
5308
+ });
5309
+ }, [invalid, value, initValue, onSave, getSelectedRows, field]);
5310
+ const inputRef = React.useRef(null);
5311
+ React.useEffect(() => {
5312
+ inputRef.current?.focus();
5313
+ inputRef.current?.select();
5314
+ }, []);
5315
+ const { triggerSave } = useGridPopoverHook({
5316
+ className: props.className,
5317
+ invalid,
5318
+ save,
5319
+ });
5320
+ const tabDirectionRef = React.useRef(CloseReason.BLUR);
5321
+ const tabRecorder = React.useCallback((e) => {
5322
+ if (e.key === 'Tab') {
5323
+ tabDirectionRef.current = e.shiftKey ? CloseReason.TAB_BACKWARD : CloseReason.TAB_FORWARD;
5324
+ e.preventDefault();
5325
+ e.stopPropagation();
5326
+ void triggerSave(tabDirectionRef.current);
5327
+ }
5328
+ }, [triggerSave]);
5329
+ React.useEffect(() => {
5330
+ const input = inputRef.current;
5331
+ input?.addEventListener('keydown', tabRecorder, { capture: true });
5332
+ return () => {
5333
+ input?.removeEventListener('keydown', tabRecorder, { capture: true });
5334
+ };
5335
+ }, [tabRecorder]);
5336
+ return (jsxRuntime.jsx("div", { className: clsx('GridFormInlineTextInput', invalid() && 'GridFormInlineTextInput-error'), title: String(invalid() ?? ''), children: jsxRuntime.jsx("input", { ref: inputRef, type: 'text', value: value, onChange: (e) => setValue(e.target.value), placeholder: props.placeholder ?? 'Type here', onBlur: () => {
5337
+ void triggerSave(CloseReason.BLUR);
5338
+ }, onKeyUp: (e) => {
5339
+ if (e.key === 'Enter') {
5340
+ void triggerSave(CloseReason.BLUR);
5341
+ }
5342
+ } }) }));
5343
+ };
5344
+
5164
5345
  const GridFormMessage = (props) => {
5165
5346
  const { selectedRows } = useGridPopoverContext();
5166
5347
  const [message, setMessage] = React.useState(null);
@@ -5572,62 +5753,6 @@ const TextAreaInput = (props) => {
5572
5753
  }, "data-allowtabtosave": props.allowTabToSave, children: props.value }) })] }), jsxRuntime.jsx(FormError, { error: props.error, helpText: props.helpText })] }));
5573
5754
  };
5574
5755
 
5575
- const TextInputValidator = (props, value, data, context) => {
5576
- if (value == null)
5577
- return null;
5578
- // This can happen because subcomponent is invoked without type safety
5579
- if (typeof value !== 'string') {
5580
- return 'Value is not a string';
5581
- }
5582
- if (props.required && value.trim() == '') {
5583
- return 'Must not be empty';
5584
- }
5585
- if (props.maxLength && value.length > props.maxLength) {
5586
- return `Must be no longer than ${props.maxLength} characters`;
5587
- }
5588
- if (props.maxBytes && stringByteLengthIsInvalid(value, props.maxBytes)) {
5589
- return `Must be no longer than ${props.maxBytes} bytes`;
5590
- }
5591
- const nf = props.numberFormat;
5592
- if (nf) {
5593
- if (value != '' && !isFloat(value)) {
5594
- return `Must be a valid number`;
5595
- }
5596
- if (value != '') {
5597
- const number = parseFloat(value);
5598
- if (nf.notZero && number === 0) {
5599
- return `Must not be 0`;
5600
- }
5601
- if (nf.gtMin != null && number <= nf.gtMin) {
5602
- return `Must be greater than ${nf.gtMin.toLocaleString()}`;
5603
- }
5604
- if (nf.geMin != null && number < nf.geMin) {
5605
- return `Must not be less than ${nf.geMin.toLocaleString()}`;
5606
- }
5607
- if (nf.ltMax != null && number >= nf.ltMax) {
5608
- return `Must be less than ${nf.ltMax.toLocaleString()}`;
5609
- }
5610
- if (nf.leMax != null && number > nf.leMax) {
5611
- return `Must not be greater than ${nf.leMax.toLocaleString()}`;
5612
- }
5613
- if (nf.precision != null && value != '') {
5614
- if (parseFloat(number.toPrecision(nf.precision)) != number) {
5615
- return `Must have no more than ${nf.precision} digits precision`;
5616
- }
5617
- }
5618
- if (nf.scale != null && value != '') {
5619
- if (parseFloat(number.toFixed(nf.scale)) != number) {
5620
- return nf.scale == 0 ? `Must be a whole number` : `Must have no more than ${nf.scale} decimal places`;
5621
- }
5622
- }
5623
- }
5624
- }
5625
- if (props.invalid) {
5626
- return props.invalid(value, data, context);
5627
- }
5628
- return null;
5629
- };
5630
-
5631
5756
  const GridFormSubComponentTextArea = (props) => {
5632
5757
  const { value, data, setValue, setValid, context } = React.useContext(GridSubComponentContext);
5633
5758
  const helpText = props.helpText ?? 'Press tab to save';
@@ -5806,6 +5931,11 @@ const GridEditBoolean = (colDef, editorProps) => {
5806
5931
  });
5807
5932
  };
5808
5933
 
5934
+ const GridInlineTextInput = (colDef, params) => GridCell(colDef, {
5935
+ editor: GridFormInlineTextInput,
5936
+ ...params,
5937
+ });
5938
+
5809
5939
  const GridPopoutEditMultiSelect = (colDef, props) => GridCell(colDef, {
5810
5940
  editor: GridFormMultiSelect,
5811
5941
  ...props,
@@ -5946,6 +6076,7 @@ const GridContextProvider = (props) => {
5946
6076
  const [invisibleColumnIds, _setInvisibleColumnIds] = React.useState();
5947
6077
  const testId = React.useRef();
5948
6078
  const hasExternallySelectedItemsRef = React.useRef(false);
6079
+ const enableMultilineBulkEditRef = React.useRef(false);
5949
6080
  const idsBeforeUpdate = React.useRef([]);
5950
6081
  const prePopupFocusedCell = React.useRef();
5951
6082
  const [externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync] = React.useState(false);
@@ -6010,8 +6141,9 @@ const GridContextProvider = (props) => {
6010
6141
  /**
6011
6142
  * Set the grid api when the grid is ready.
6012
6143
  */
6013
- const setApis = React.useCallback((gridApi, hasExternallySelectedItems, dataTestId) => {
6144
+ const setApis = React.useCallback((gridApi, hasExternallySelectedItems, enableMultilineBulkEdit, dataTestId) => {
6014
6145
  hasExternallySelectedItemsRef.current = hasExternallySelectedItems;
6146
+ enableMultilineBulkEditRef.current = enableMultilineBulkEdit;
6015
6147
  testId.current = dataTestId;
6016
6148
  setGridApi(gridApi);
6017
6149
  gridApi?.setGridOption('quickFilterText', quickFilter);
@@ -6156,7 +6288,7 @@ const GridContextProvider = (props) => {
6156
6288
  if (flash) {
6157
6289
  lodashEs.delay(() => {
6158
6290
  try {
6159
- !gridApi.isDestroyed && gridApi.flashCells({ rowNodes });
6291
+ !gridApi.isDestroyed() && gridApi.flashCells({ rowNodes });
6160
6292
  }
6161
6293
  catch {
6162
6294
  // ignore, flash cells sometimes throws errors as nodes have gone out of scope
@@ -6222,13 +6354,13 @@ const GridContextProvider = (props) => {
6222
6354
  * Then we size the flexed columns.
6223
6355
  */
6224
6356
  const autoSizeColumns = React.useCallback(async ({ skipHeader, colIds, userSizedColIds } = {}) => {
6225
- if (!gridApi || !gridApi.getColumnState()) {
6357
+ if (!gridApi || gridApi.isDestroyed() || !gridApi.getColumnState()) {
6226
6358
  return null;
6227
6359
  }
6228
6360
  try {
6229
6361
  const colIdsSet = colIds instanceof Set ? colIds : new Set(colIds);
6230
6362
  const getVisibleColStates = () => {
6231
- const colStates = gridApi.getColumnState();
6363
+ const colStates = gridApi.getColumnState() ?? [];
6232
6364
  return colStates.filter((colState) => {
6233
6365
  const colId = colState.colId;
6234
6366
  return (lodashEs.isEmpty(colIdsSet) || colIdsSet.has(colId)) && !userSizedColIds?.has(colId);
@@ -6288,9 +6420,11 @@ const GridContextProvider = (props) => {
6288
6420
  if (!gridApi || gridApi.isDestroyed() || startCellEditingInProgressRef.current) {
6289
6421
  return;
6290
6422
  }
6291
- if (prePopupFocusedCell.current) {
6292
- gridApi.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
6293
- prePopupFocusedCell.current = undefined;
6423
+ const cellToFocus = prePopupFocusedCell.current;
6424
+ if (cellToFocus) {
6425
+ lodashEs.defer(() => {
6426
+ gridApi.setFocusedCell(cellToFocus.rowIndex, cellToFocus.column);
6427
+ });
6294
6428
  }
6295
6429
  }, [gridApi]);
6296
6430
  // waitForExternallySelectedItemsToBeInSync can't use the state as it won't be updated during function execution
@@ -6356,9 +6490,8 @@ const GridContextProvider = (props) => {
6356
6490
  }, [anyUpdating, gridApi, prePopupOps, waitForExternallySelectedItemsToBeInSync]);
6357
6491
  const bulkEditingCompleteCallbackRef = React.useRef();
6358
6492
  const onBulkEditingComplete = React.useCallback(async () => {
6359
- resetFocusedCellAfterCellEditing();
6360
6493
  await bulkEditingCompleteCallbackRef.current?.();
6361
- }, [resetFocusedCellAfterCellEditing]);
6494
+ }, []);
6362
6495
  const setOnBulkEditingComplete = React.useCallback((cellEditingCompleteCallback) => {
6363
6496
  bulkEditingCompleteCallbackRef.current = cellEditingCompleteCallback;
6364
6497
  }, []);
@@ -6398,10 +6531,18 @@ const GridContextProvider = (props) => {
6398
6531
  return true;
6399
6532
  }
6400
6533
  const postRow = gridApi.getFocusedCell();
6401
- if (preRow?.rowIndex !== postRow?.rowIndex || preRow?.column === postRow?.column) {
6402
- // We didn't find an editable cell in the same row, or the cell column didn't change
6403
- // implying it was start/end of grid
6404
- break;
6534
+ if (enableMultilineBulkEditRef.current) {
6535
+ // Last cell in grid, nowhere to tab
6536
+ if (preRow?.rowIndex === postRow?.rowIndex && preRow?.column === postRow?.column) {
6537
+ break;
6538
+ }
6539
+ }
6540
+ else {
6541
+ if (preRow?.rowIndex !== postRow?.rowIndex || preRow?.column === postRow?.column) {
6542
+ // We didn't find an editable cell in the same row, or the cell column didn't change
6543
+ // implying it was start/end of grid
6544
+ break;
6545
+ }
6405
6546
  }
6406
6547
  if (focusedCellIsEditable()) {
6407
6548
  const focusedCell = gridApi.getFocusedCell();
@@ -6785,6 +6926,7 @@ exports.GridFilterQuick = GridFilterQuick;
6785
6926
  exports.GridFilters = GridFilters;
6786
6927
  exports.GridFormDropDown = GridFormDropDown;
6787
6928
  exports.GridFormEditBearing = GridFormEditBearing;
6929
+ exports.GridFormInlineTextInput = GridFormInlineTextInput;
6788
6930
  exports.GridFormMessage = GridFormMessage;
6789
6931
  exports.GridFormMultiSelect = GridFormMultiSelect;
6790
6932
  exports.GridFormMultiSelectGrid = GridFormMultiSelectGrid;
@@ -6794,6 +6936,7 @@ exports.GridFormSubComponentTextInput = GridFormSubComponentTextInput;
6794
6936
  exports.GridFormTextArea = GridFormTextArea;
6795
6937
  exports.GridFormTextInput = GridFormTextInput;
6796
6938
  exports.GridIcon = GridIcon;
6939
+ exports.GridInlineTextInput = GridInlineTextInput;
6797
6940
  exports.GridLoadableCell = GridLoadableCell;
6798
6941
  exports.GridNoRowsOverlay = GridNoRowsOverlay;
6799
6942
  exports.GridNoRowsOverlayFr = GridNoRowsOverlayFr;