@linzjs/step-ag-grid 29.16.0 → 30.0.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 (33) 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/gridForm/GridFormInlineTextInput.d.ts +15 -0
  5. package/dist/src/components/gridForm/index.d.ts +1 -0
  6. package/dist/src/components/gridHook/useGridRangeSelection.d.ts +1 -1
  7. package/dist/src/components/gridPopoverEdit/GridInlineTextInput.d.ts +5 -0
  8. package/dist/src/components/gridPopoverEdit/index.d.ts +1 -0
  9. package/dist/src/contexts/GridContext.d.ts +1 -1
  10. package/dist/src/utils/__tests__/storybookTestUtil.ts +5 -1
  11. package/dist/step-ag-grid.cjs +58441 -505
  12. package/dist/step-ag-grid.cjs.map +1 -1
  13. package/dist/step-ag-grid.esm.js +58025 -91
  14. package/dist/step-ag-grid.esm.js.map +1 -1
  15. package/package.json +13 -13
  16. package/src/components/Grid.tsx +33 -10
  17. package/src/components/GridPopoverHook.tsx +5 -2
  18. package/src/components/gridForm/GridFormInlineTextInput.tsx +119 -0
  19. package/src/components/gridForm/index.ts +1 -0
  20. package/src/components/gridHook/useGridContextMenu.tsx +3 -1
  21. package/src/components/gridHook/useGridCopy.ts +5 -2
  22. package/src/components/gridHook/useGridRangeSelection.ts +25 -4
  23. package/src/components/gridPopoverEdit/GridInlineTextInput.ts +13 -0
  24. package/src/components/gridPopoverEdit/index.ts +1 -0
  25. package/src/contexts/GridContext.tsx +6 -1
  26. package/src/contexts/GridContextProvider.tsx +30 -13
  27. package/src/stories/grid/GridFilterColumnsMultiSelect.stories.tsx +43 -2
  28. package/src/stories/grid/GridInlineText.stories.tsx +185 -0
  29. package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +3 -3
  30. package/src/styles/Grid.scss +5 -0
  31. package/src/styles/GridFormInlineTextInput.scss +23 -0
  32. package/src/styles/index.scss +1 -1
  33. package/src/utils/__tests__/storybookTestUtil.ts +5 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@linzjs/step-ag-grid",
3
3
  "repository": "github:linz/step-ag-grid.git",
4
4
  "license": "MIT",
5
- "version": "29.16.0",
5
+ "version": "30.0.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -28,13 +28,13 @@
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@linzjs/lui": ">=21",
31
- "ag-grid-community": "~34.2.0",
32
- "ag-grid-react": "~34.2.0",
33
31
  "lodash-es": ">=4",
34
32
  "react": ">=18",
35
33
  "react-dom": ">=18"
36
34
  },
37
35
  "dependencies": {
36
+ "ag-grid-community": "34.3.1",
37
+ "ag-grid-react": "34.3.1",
38
38
  "clsx": "^2.1.1",
39
39
  "debounce-promise": "^3.1.2",
40
40
  "matcher": "^5.0.0",
@@ -59,7 +59,8 @@
59
59
  "build-storybook": "storybook build",
60
60
  "deploy-storybook": "npx --yes -p @storybook/storybook-deployer storybook-to-ghpages",
61
61
  "chromatic": "chromatic --exit-zero-on-changes",
62
- "upgrade": "npx ncu -i --format group"
62
+ "upgrade": "npx ncu -i --format group",
63
+ "prepare": "husky"
63
64
  },
64
65
  "eslintConfig": {
65
66
  "extends": [
@@ -86,14 +87,14 @@
86
87
  "@chromatic-com/storybook": "^4.1.3",
87
88
  "@linzjs/lui": "^24.3.1",
88
89
  "@linzjs/style": "^5.4.0",
89
- "@linzjs/windows": "^8.3.0",
90
+ "@linzjs/windows": "^8.6.3",
90
91
  "@rollup/plugin-commonjs": "^28.0.9",
91
92
  "@rollup/plugin-json": "^6.1.0",
92
93
  "@rollup/plugin-node-resolve": "^16.0.3",
93
- "@storybook/addon-docs": "^9.1.16",
94
- "@storybook/addon-links": "^9.1.16",
95
- "@storybook/react": "^9.1.16",
96
- "@storybook/react-vite": "^9.1.16",
94
+ "@storybook/addon-docs": "^9.1.17",
95
+ "@storybook/addon-links": "^9.1.17",
96
+ "@storybook/react": "^9.1.17",
97
+ "@storybook/react-vite": "^9.1.17",
97
98
  "@testing-library/dom": "^10.4.1",
98
99
  "@testing-library/react": "^16.3.0",
99
100
  "@testing-library/user-event": "^14.6.1",
@@ -106,15 +107,14 @@
106
107
  "@types/uuid": "^11.0.0",
107
108
  "@vitejs/plugin-react-swc": "^3.11.0",
108
109
  "@vitest/ui": "^3.2.4",
109
- "ag-grid-community": "34.2.0",
110
- "ag-grid-react": "34.2.0",
111
110
  "babel-preset-react-app": "^10.1.0",
112
111
  "canvas": "^3.2.0",
113
112
  "chromatic": "^13.3.4",
114
113
  "cross-env": "^10.1.0",
115
114
  "css-loader": "^7.1.2",
116
115
  "eslint-plugin-react": "^7.37.5",
117
- "eslint-plugin-storybook": "^9.1.16",
116
+ "eslint-plugin-storybook": "^9.1.17",
117
+ "husky": "^9.1.7",
118
118
  "jsdom": "^26.1.0",
119
119
  "lodash-es": "^4.17.21",
120
120
  "mkdirp": "^3.0.1",
@@ -133,7 +133,7 @@
133
133
  "rollup-plugin-typescript2": "^0.36.0",
134
134
  "sass": "^1.96.0",
135
135
  "sass-loader": "^16.0.6",
136
- "storybook": "^9.1.16",
136
+ "storybook": "^9.1.17",
137
137
  "storybook-css-modules-preset": "^1.1.1",
138
138
  "style-loader": "^4.0.0",
139
139
  "typescript": "^5.9.3",
@@ -74,9 +74,11 @@ export interface GridProps<TData extends GridBaseRow = GridBaseRow> {
74
74
  enableClickSelection?: boolean;
75
75
  enableRangeSelection?: boolean;
76
76
  enableSelectionWithoutKeys?: boolean;
77
+ enableMultilineBulkEdit?: boolean;
77
78
  externalSelectedIds?: TData['id'][];
78
79
  externalSelectedItems?: TData[];
79
80
  hideSelectColumn?: boolean;
81
+ hideSelectedRow?: boolean;
80
82
  selectColumnPinned?: ColDef['pinned'];
81
83
  selectable?: boolean;
82
84
  setExternalSelectedIds?: (ids: TData['id'][]) => void;
@@ -85,7 +87,6 @@ export interface GridProps<TData extends GridBaseRow = GridBaseRow> {
85
87
  // ─── Editing ──────────────────────────────────────────────────
86
88
  onBulkEditingComplete?: () => Promise<void> | void;
87
89
  singleClickEdit?: boolean;
88
- stopEditingWhenCellsLoseFocus?: boolean;
89
90
 
90
91
  // ─── Context Menu ─────────────────────────────────────────────
91
92
  contextMenu?: GridContextMenuComponent<TData>;
@@ -150,7 +151,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
150
151
 
151
152
  // ─── Editing ──────────────────────────────────
152
153
  singleClickEdit = false,
153
- stopEditingWhenCellsLoseFocus = false,
154
+ enableMultilineBulkEdit = false,
154
155
 
155
156
  // ─── Context Menu ─────────────────────────────
156
157
  contextMenuSelectRow = false,
@@ -500,11 +501,17 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
500
501
  */
501
502
  const onGridReady = useCallback(
502
503
  (event: GridReadyEvent) => {
503
- setApis(event.api, hasExternallySelectedItems, dataTestId);
504
+ setApis(event.api, hasExternallySelectedItems, enableMultilineBulkEdit, dataTestId);
504
505
  event.api.showNoRowsOverlay();
505
506
  synchroniseExternallySelectedItemsToGrid();
506
507
  },
507
- [dataTestId, hasExternallySelectedItems, setApis, synchroniseExternallySelectedItemsToGrid],
508
+ [
509
+ dataTestId,
510
+ enableMultilineBulkEdit,
511
+ hasExternallySelectedItems,
512
+ setApis,
513
+ synchroniseExternallySelectedItemsToGrid,
514
+ ],
508
515
  );
509
516
 
510
517
  /**
@@ -556,6 +563,11 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
556
563
  */
557
564
  const onCellDoubleClick = useCallback(
558
565
  (event: CellDoubleClickedEvent) => {
566
+ const cellEditing = event.api.getEditingCells()?.[0];
567
+ if (cellEditing && cellEditing.colId === event.column.getColId() && cellEditing.rowIndex === event.rowIndex) {
568
+ // Ignore double click in already editing cell
569
+ return;
570
+ }
559
571
  const editable = fnOrVar(event.colDef?.editable, event);
560
572
  if (editable && !invokeEditAction(event)) {
561
573
  void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
@@ -657,19 +669,30 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
657
669
  */
658
670
  const onCellClicked = useCallback(
659
671
  (event: CellClickedEvent) => {
660
- if (rangeStartRef.current && rangeEndRef.current) {
672
+ if (enableRangeSelection && rangeStartRef.current && rangeEndRef.current) {
661
673
  // This is to detect difference between a single click and a click drag return to cell.
662
- if (rangeEndRef.current.timestamp - rangeStartRef.current.timestamp < 100) {
663
- clearRangeSelection();
674
+ const maxDistanceBetweenC = Math.max(
675
+ Math.abs(rangeEndRef.current.clickLocation[0] - rangeStartRef.current.clickLocation[0]),
676
+ Math.abs(rangeEndRef.current.clickLocation[1] - rangeStartRef.current.clickLocation[1]),
677
+ );
678
+ if (maxDistanceBetweenC > 8) {
679
+ return;
664
680
  }
665
- return;
681
+ clearRangeSelection();
666
682
  }
683
+
667
684
  const editable = fnOrVar(event.colDef?.editable, event);
668
685
  if ((editable && event.colDef.singleClickEdit) ?? singleClickEdit) {
686
+ const editingCell = event.api.getEditingCells()[0];
687
+ // If we are editing and single click we don't want the editing to stop
688
+ if (editingCell && editingCell.colId === event.column.getColId() && editingCell.rowIndex === event.rowIndex) {
689
+ return;
690
+ }
691
+ event.api.setFocusedCell(event.rowIndex ?? 0, event.column.getColId(), event.rowPinned);
669
692
  void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
670
693
  }
671
694
  },
672
- [clearRangeSelection, singleClickEdit, startCellEditing],
695
+ [clearRangeSelection, enableRangeSelection, singleClickEdit, startCellEditing],
673
696
  );
674
697
 
675
698
  /**
@@ -1020,6 +1043,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
1020
1043
  'theme-specific',
1021
1044
  staleGrid && 'Grid-sortIsStale',
1022
1045
  gridReady && rowData && autoSized && 'Grid-ready',
1046
+ params.hideSelectedRow && 'Grid-hideSelectedRow',
1023
1047
  )}
1024
1048
  >
1025
1049
  {contextMenuComponent}
@@ -1081,7 +1105,6 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
1081
1105
  }
1082
1106
  : undefined
1083
1107
  }
1084
- stopEditingWhenCellsLoseFocus={stopEditingWhenCellsLoseFocus}
1085
1108
  selectionColumnDef={selectionColumnDef}
1086
1109
  suppressCellFocus={params.suppressCellFocus}
1087
1110
  suppressClickEdit={true}
@@ -1,5 +1,6 @@
1
1
  import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
2
2
 
3
+ import { useGridContext } from '../contexts/GridContext';
3
4
  import { useGridPopoverContext } from '../contexts/GridPopoverContext';
4
5
  import { ControlledMenu } from '../react-menu3';
5
6
  import { MenuCloseEvent } from '../react-menu3/types';
@@ -27,6 +28,7 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
27
28
  invalid,
28
29
  dontSaveOnExternalClick,
29
30
  }: GridPopoverHookProps<TData>) => {
31
+ const { resetFocusedCellAfterCellEditing } = useGridContext<TData>();
30
32
  const { anchorRef, saving, updateValue, stopEditing } = useGridPopoverContext<TData>();
31
33
  const saveButtonRef = useRef<HTMLButtonElement>(null);
32
34
  const [isOpen, setOpen] = useState(false);
@@ -40,7 +42,7 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
40
42
  if (reason == CloseReason.CANCEL) {
41
43
  await updateValue(CancelPromise, 0);
42
44
  stopEditing();
43
-
45
+ resetFocusedCellAfterCellEditing();
44
46
  return;
45
47
  }
46
48
  if (invalid?.()) {
@@ -55,9 +57,10 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
55
57
  )
56
58
  ) {
57
59
  stopEditing();
60
+ resetFocusedCellAfterCellEditing();
58
61
  }
59
62
  },
60
- [invalid, save, stopEditing, updateValue],
63
+ [invalid, resetFocusedCellAfterCellEditing, save, stopEditing, updateValue],
61
64
  );
62
65
 
63
66
  const popoverWrapper = useCallback(
@@ -0,0 +1,119 @@
1
+ import clsx from 'clsx';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import { useGridContext } from '../../contexts/GridContext';
5
+ import { useGridPopoverContext } from '../../contexts/GridPopoverContext';
6
+ import { CloseReason } from '../../react-menu3/utils';
7
+ import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
8
+ import { MaybePromise } from '../../utils/util';
9
+ import { CellEditorCommon } from '../GridCell';
10
+ import { useGridPopoverHook } from '../GridPopoverHook';
11
+ import { GridBaseRow } from '../types';
12
+
13
+ export interface GridFormInlineTextInput<TData extends GridBaseRow>
14
+ extends TextInputValidatorProps<TData>,
15
+ CellEditorCommon {
16
+ placeholder?: string;
17
+ units?: string;
18
+ onSave?: (props: { selectedRows: TData[]; selectedRowIds: TData['id'][]; value: string }) => MaybePromise<boolean>;
19
+ helpText?: string;
20
+ }
21
+
22
+ export const GridFormInlineTextInput = <TData extends GridBaseRow>(props: GridFormInlineTextInput<TData>) => {
23
+ const { onSave } = props;
24
+
25
+ const { getSelectedRows, prePopupOps } = useGridContext<TData>();
26
+ const { field, value: initialVale, data } = useGridPopoverContext<TData>();
27
+
28
+ const initValue = useMemo(() => (initialVale == null ? '' : `${initialVale}`), [initialVale]);
29
+ const [value, setValue] = useState(initValue);
30
+
31
+ const invalid = useCallback(() => TextInputValidator<TData>(props, value, data, {}), [data, props, value]);
32
+
33
+ useEffect(() => {
34
+ prePopupOps();
35
+ }, [prePopupOps]);
36
+
37
+ const save = useCallback(async (): Promise<boolean> => {
38
+ if (invalid()) {
39
+ return false;
40
+ }
41
+
42
+ const trimmedValue = value.trim();
43
+ // No change, so don't save
44
+ if (initValue === trimmedValue) {
45
+ return true;
46
+ }
47
+
48
+ if (!onSave) {
49
+ console.error(`No onSave handler for GridFormInlineTextInput "${String(field)}`);
50
+ return false;
51
+ }
52
+
53
+ const selectedRows = getSelectedRows<TData>();
54
+ const selectedRowIds = selectedRows.map((data) => data.id);
55
+
56
+ return onSave({
57
+ selectedRowIds,
58
+ selectedRows,
59
+ value: trimmedValue,
60
+ });
61
+ }, [invalid, value, initValue, onSave, getSelectedRows, field]);
62
+
63
+ const inputRef = useRef<HTMLInputElement | null>(null);
64
+ useEffect(() => {
65
+ inputRef.current?.focus();
66
+ inputRef.current?.select();
67
+ }, []);
68
+
69
+ const { triggerSave } = useGridPopoverHook({
70
+ className: props.className,
71
+ invalid,
72
+ save,
73
+ });
74
+
75
+ const tabDirectionRef = useRef<string>(CloseReason.BLUR);
76
+
77
+ const tabRecorder = useCallback(
78
+ (e: KeyboardEvent) => {
79
+ if (e.key === 'Tab') {
80
+ tabDirectionRef.current = e.shiftKey ? CloseReason.TAB_BACKWARD : CloseReason.TAB_FORWARD;
81
+ e.preventDefault();
82
+ e.stopPropagation();
83
+ void triggerSave(tabDirectionRef.current);
84
+ }
85
+ },
86
+ [triggerSave],
87
+ );
88
+
89
+ useEffect(() => {
90
+ const input = inputRef.current;
91
+ input?.addEventListener('keydown', tabRecorder, { capture: true });
92
+ return () => {
93
+ input?.removeEventListener('keydown', tabRecorder, { capture: true });
94
+ };
95
+ }, [tabRecorder]);
96
+
97
+ return (
98
+ <div
99
+ className={clsx('GridFormInlineTextInput', invalid() && 'GridFormInlineTextInput-error')}
100
+ title={String(invalid() ?? '')}
101
+ >
102
+ <input
103
+ ref={inputRef}
104
+ type={'text'}
105
+ value={value}
106
+ onChange={(e) => setValue(e.target.value)}
107
+ placeholder={props.placeholder ?? 'Type here'}
108
+ onBlur={() => {
109
+ void triggerSave(CloseReason.BLUR);
110
+ }}
111
+ onKeyUp={(e) => {
112
+ if (e.key === 'Enter') {
113
+ void triggerSave(CloseReason.BLUR);
114
+ }
115
+ }}
116
+ />
117
+ </div>
118
+ );
119
+ };
@@ -1,5 +1,6 @@
1
1
  export * from './GridFormDropDown';
2
2
  export * from './GridFormEditBearing';
3
+ export * from './GridFormInlineTextInput';
3
4
  export * from './GridFormMessage';
4
5
  export * from './GridFormMultiSelect';
5
6
  export * from './GridFormMultiSelectGrid';
@@ -45,7 +45,9 @@ export const useGridContextMenu = <TData extends GridBaseRow, Context extends ob
45
45
 
46
46
  const openMenu = useCallback(
47
47
  (e: PointerEvent | null | undefined) => {
48
- if (!e) return;
48
+ if (!e) {
49
+ return;
50
+ }
49
51
  prePopupOps();
50
52
  setAnchorPoint({ x: e.clientX, y: e.clientY });
51
53
  setOpen(true);
@@ -250,8 +250,11 @@ export const useGridCopy = <TData extends GridBaseRow>({
250
250
  });
251
251
 
252
252
  const rangeSelectInterceptContextMenu = useCallback(
253
- (event: CellContextMenuEvent<TData>) =>
254
- rangeStartRef.current == null ? cellContextMenu(event) : rangeSelectContextMenu(event),
253
+ (event: CellContextMenuEvent<TData>) => {
254
+ return hasSelectedMoreThanOneCellRef.current && rangeStartRef.current !== null
255
+ ? rangeSelectContextMenu(event)
256
+ : cellContextMenu(event);
257
+ },
255
258
  [cellContextMenu, rangeSelectContextMenu],
256
259
  );
257
260
 
@@ -1,5 +1,5 @@
1
1
  import { CellMouseDownEvent, CellMouseOverEvent, IRowNode } from 'ag-grid-community';
2
- import { MutableRefObject, RefObject, useCallback, useState } from 'react';
2
+ import { MutableRefObject, RefObject, useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { useInterval } from 'usehooks-ts';
4
4
 
5
5
  import { GridBaseRow } from '../types';
@@ -7,7 +7,7 @@ import { GridBaseRow } from '../types';
7
7
  export interface CellLocation {
8
8
  rowId: string;
9
9
  colId: string;
10
- timestamp: number;
10
+ clickLocation: [number, number];
11
11
  }
12
12
 
13
13
  export interface GridRanges<TData extends GridBaseRow> {
@@ -180,6 +180,26 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
180
180
  updateRangeSelectionCellClasses();
181
181
  }, [updateRangeSelectionCellClasses]);
182
182
 
183
+ const mouseDownRef = useRef<[number, number]>([0, 0]);
184
+ const trackMouseDown = useCallback((e: MouseEvent) => {
185
+ mouseDownRef.current = [e.screenX, e.screenY];
186
+ }, []);
187
+
188
+ const trackMouseUp = useCallback((e: MouseEvent) => {
189
+ if (rangeEndRef.current) {
190
+ rangeEndRef.current.clickLocation = [e.screenX, e.screenY];
191
+ }
192
+ }, []);
193
+
194
+ useEffect(() => {
195
+ document.addEventListener('mousedown', trackMouseDown, { capture: true });
196
+ document.addEventListener('mouseup', trackMouseUp, { capture: true });
197
+ return () => {
198
+ document.removeEventListener('mousemove', trackMouseDown, { capture: true });
199
+ document.removeEventListener('mouseup', trackMouseUp, { capture: true });
200
+ };
201
+ }, []);
202
+
183
203
  const onCellMouseOver = useCallback(
184
204
  (e: CellMouseOverEvent, mouseDown?: boolean) => {
185
205
  if (!enableRangeSelection) {
@@ -194,7 +214,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
194
214
  rangeEndRef.current = {
195
215
  rowId: e.node.data.id,
196
216
  colId: e.column.getColId(),
197
- timestamp: Date.now(),
217
+ clickLocation: [0, 0],
198
218
  };
199
219
 
200
220
  if (mouseDown) {
@@ -202,7 +222,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
202
222
  e.api.forEachNodeAfterFilterAndSort((node: IRowNode<TData>) => sortedNodes.push(node));
203
223
  rangeSortedNodesRef.current = sortedNodes;
204
224
  setRefreshIntervalEnabled(true);
205
- rangeStartRef.current = { ...rangeEndRef.current };
225
+ rangeStartRef.current = { ...rangeEndRef.current, clickLocation: mouseDownRef.current };
206
226
  }
207
227
 
208
228
  if (
@@ -211,6 +231,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
211
231
  (rangeStartRef.current.rowId !== rangeEndRef.current.rowId ||
212
232
  rangeStartRef.current.colId !== rangeEndRef.current.colId)
213
233
  ) {
234
+ e.api.stopEditing();
214
235
  hasSelectedMoreThanOneCellRef.current = true;
215
236
  window.getSelection()?.removeAllRanges();
216
237
  }
@@ -0,0 +1,13 @@
1
+ import { GenericCellEditorProps, GridCell } from '../GridCell';
2
+ import { GridFormInlineTextInput } from '../gridForm/GridFormInlineTextInput';
3
+ import { GenericCellColDef } from '../gridRender/GridRenderGenericCell';
4
+ import { ColDefT, GridBaseRow } from '../types';
5
+
6
+ export const GridInlineTextInput = <TData extends GridBaseRow, TValue = any>(
7
+ colDef: GenericCellColDef<TData, TValue>,
8
+ params: GenericCellEditorProps<GridFormInlineTextInput<TData>>,
9
+ ): ColDefT<TData, TValue> =>
10
+ GridCell<TData, TValue, GridFormInlineTextInput<TData>>(colDef, {
11
+ editor: GridFormInlineTextInput,
12
+ ...params,
13
+ });
@@ -1,5 +1,6 @@
1
1
  export * from './GridButton';
2
2
  export * from './GridEditBoolean';
3
+ export * from './GridInlineTextInput';
3
4
  export * from './GridPopoutEditMultiSelect';
4
5
  export * from './GridPopoutEditMultiSelectGrid';
5
6
  export * from './GridPopoverEditBearing';
@@ -27,7 +27,12 @@ export interface GridContextType<TData extends GridBaseRow> {
27
27
  filter?: keyof ColDef | ((r: ColDef) => boolean | undefined | null | number | string),
28
28
  ) => ColDefT<TData, any>[];
29
29
  getColumnIds: (filter?: keyof ColDef | ((r: ColDef) => boolean | undefined | null | number | string)) => string[];
30
- setApis: (gridApi: GridApi | undefined, hasExternallySelectedItems: boolean, dataTestId?: string) => void;
30
+ setApis: (
31
+ gridApi: GridApi | undefined,
32
+ hasExternallySelectedItems: boolean,
33
+ enableMultilineBulkEdit: boolean,
34
+ dataTestId?: string,
35
+ ) => void;
31
36
  prePopupOps: () => void;
32
37
  setQuickFilter: (quickFilter: string) => void;
33
38
  editingCells: () => boolean;
@@ -38,6 +38,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
38
38
  const [invisibleColumnIds, _setInvisibleColumnIds] = useState<string[]>();
39
39
  const testId = useRef<string | undefined>();
40
40
  const hasExternallySelectedItemsRef = useRef(false);
41
+ const enableMultilineBulkEditRef = useRef(false);
41
42
  const idsBeforeUpdate = useRef<TData['id'][]>([]);
42
43
  const prePopupFocusedCell = useRef<CellPosition>();
43
44
  const [externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync] = useState(false);
@@ -120,8 +121,14 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
120
121
  * Set the grid api when the grid is ready.
121
122
  */
122
123
  const setApis = useCallback(
123
- (gridApi: GridApi | undefined, hasExternallySelectedItems: boolean, dataTestId?: string) => {
124
+ (
125
+ gridApi: GridApi | undefined,
126
+ hasExternallySelectedItems: boolean,
127
+ enableMultilineBulkEdit: boolean,
128
+ dataTestId?: string,
129
+ ) => {
124
130
  hasExternallySelectedItemsRef.current = hasExternallySelectedItems;
131
+ enableMultilineBulkEditRef.current = enableMultilineBulkEdit;
125
132
  testId.current = dataTestId;
126
133
  setGridApi(gridApi);
127
134
  gridApi?.setGridOption('quickFilterText', quickFilter);
@@ -164,6 +171,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
164
171
  if (!gridApi || gridApi.isDestroyed()) {
165
172
  return;
166
173
  }
174
+
167
175
  prePopupFocusedCell.current = gridApi.getFocusedCell() ?? undefined;
168
176
  }, [gridApi]);
169
177
 
@@ -323,7 +331,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
323
331
  if (flash) {
324
332
  delay(() => {
325
333
  try {
326
- !gridApi.isDestroyed && gridApi.flashCells({ rowNodes });
334
+ !gridApi.isDestroyed() && gridApi.flashCells({ rowNodes });
327
335
  } catch {
328
336
  // ignore, flash cells sometimes throws errors as nodes have gone out of scope
329
337
  }
@@ -437,7 +445,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
437
445
  */
438
446
  const autoSizeColumns = useCallback(
439
447
  async ({ skipHeader, colIds, userSizedColIds }: AutoSizeColumnsProps = {}): Promise<AutoSizeColumnsResult> => {
440
- if (!gridApi || !gridApi.getColumnState()) {
448
+ if (!gridApi || gridApi.isDestroyed() || !gridApi.getColumnState()) {
441
449
  return null;
442
450
  }
443
451
 
@@ -445,7 +453,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
445
453
  const colIdsSet = colIds instanceof Set ? colIds : new Set(colIds);
446
454
 
447
455
  const getVisibleColStates = () => {
448
- const colStates = gridApi.getColumnState();
456
+ const colStates = gridApi.getColumnState() ?? [];
449
457
  return colStates.filter((colState) => {
450
458
  const colId = colState.colId;
451
459
  return (isEmpty(colIdsSet) || colIdsSet.has(colId)) && !userSizedColIds?.has(colId);
@@ -522,9 +530,12 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
522
530
  if (!gridApi || gridApi.isDestroyed() || startCellEditingInProgressRef.current) {
523
531
  return;
524
532
  }
525
- if (prePopupFocusedCell.current) {
526
- gridApi.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
527
- prePopupFocusedCell.current = undefined;
533
+
534
+ const cellToFocus = prePopupFocusedCell.current;
535
+ if (cellToFocus) {
536
+ defer(() => {
537
+ gridApi.setFocusedCell(cellToFocus.rowIndex, cellToFocus.column);
538
+ });
528
539
  }
529
540
  }, [gridApi]);
530
541
 
@@ -607,9 +618,8 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
607
618
 
608
619
  const bulkEditingCompleteCallbackRef = useRef<() => Promise<void> | void>();
609
620
  const onBulkEditingComplete = useCallback(async () => {
610
- resetFocusedCellAfterCellEditing();
611
621
  await bulkEditingCompleteCallbackRef.current?.();
612
- }, [resetFocusedCellAfterCellEditing]);
622
+ }, []);
613
623
 
614
624
  const setOnBulkEditingComplete = useCallback(
615
625
  (cellEditingCompleteCallback: (() => Promise<void> | void) | undefined) => {
@@ -661,10 +671,17 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
661
671
  return true;
662
672
  }
663
673
  const postRow = gridApi.getFocusedCell();
664
- if (preRow?.rowIndex !== postRow?.rowIndex || preRow?.column === postRow?.column) {
665
- // We didn't find an editable cell in the same row, or the cell column didn't change
666
- // implying it was start/end of grid
667
- break;
674
+ if (enableMultilineBulkEditRef.current) {
675
+ // Last cell in grid, nowhere to tab
676
+ if (preRow?.rowIndex === postRow?.rowIndex && preRow?.column === postRow?.column) {
677
+ break;
678
+ }
679
+ } else {
680
+ if (preRow?.rowIndex !== postRow?.rowIndex || preRow?.column === postRow?.column) {
681
+ // We didn't find an editable cell in the same row, or the cell column didn't change
682
+ // implying it was start/end of grid
683
+ break;
684
+ }
668
685
  }
669
686
 
670
687
  if (focusedCellIsEditable()) {
@@ -9,6 +9,7 @@ import {
9
9
  GridFilterColumnsMultiSelect,
10
10
  } from 'components/gridFilter/GridFilterColumnsMultiSelect';
11
11
  import { useMemo, useState } from 'react';
12
+ import { expect, userEvent, within } from 'storybook/test';
12
13
 
13
14
  import {
14
15
  ColDefT,
@@ -21,8 +22,15 @@ import {
21
22
  GridProps,
22
23
  GridUpdatingContextProvider,
23
24
  GridWrapper,
25
+ wait,
24
26
  } from '../..';
25
- import { waitForGridReady } from '../../utils/__tests__/storybookTestUtil';
27
+ import {
28
+ getNumberOfGridRows,
29
+ gridColumnValues,
30
+ waitForGridReady,
31
+ waitForGridRows,
32
+ } from '../../utils/__tests__/storybookTestUtil';
33
+ import { findQuick } from '../../utils/__tests__/testQuick';
26
34
 
27
35
  export default {
28
36
  title: 'Components / Grids',
@@ -124,4 +132,37 @@ const GridFilterColumnsMultiSelectTemplate: StoryFn<typeof Grid<ITestRow>> = (pr
124
132
  };
125
133
 
126
134
  export const _FilterColumnsMultiSelectExample = GridFilterColumnsMultiSelectTemplate.bind({});
127
- _FilterColumnsMultiSelectExample.play = waitForGridReady;
135
+ _FilterColumnsMultiSelectExample.play = async (context) => {
136
+ const { canvasElement } = context;
137
+ await waitForGridReady(context);
138
+ await waitForGridRows({ grid: canvasElement });
139
+
140
+ const setGridFilterAndCheckResultingRows = async (filterString: string, expected: string[] | null) => {
141
+ await filterGridUsingColumnFilter(filterString);
142
+ await wait(500);
143
+ if (expected !== null) {
144
+ expect(gridColumnValues('id', canvasElement)).toEqual(expected);
145
+ return;
146
+ }
147
+ await waitForGridReady(context);
148
+ expect(getNumberOfGridRows()).toEqual(0);
149
+ };
150
+
151
+ await setGridFilterAndCheckResultingRows('Tester', ['1001', '1002', '1004', '1005', '1006']);
152
+ await setGridFilterAndCheckResultingRows('Select All', ['1000', '1001', '1002', '1003', '1004', '1005', '1006']);
153
+ await setGridFilterAndCheckResultingRows('Select All', null); // Check that deselecting Select All filters out all rows
154
+ };
155
+
156
+ const filterGridUsingColumnFilter = async (filterString: string): Promise<void> => {
157
+ const user = userEvent.setup({
158
+ delay: 100,
159
+ });
160
+ const headerCellFilterButton = await findQuick({ classes: `.ag-header-cell-filter-button` });
161
+ await expect(headerCellFilterButton).toBeInTheDocument();
162
+ await user.click(headerCellFilterButton);
163
+ const canvas = within(document.body);
164
+ const regex = new RegExp(filterString, 'i'); // case‑insensitive
165
+ const filterCheckbox = await canvas.findByRole('checkbox', { name: regex });
166
+ await expect(filterCheckbox).toBeInTheDocument();
167
+ await user.click(filterCheckbox);
168
+ };