@linzjs/step-ag-grid 32.1.0 → 32.2.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/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": "32.1.0",
5
+ "version": "32.2.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -27,7 +27,7 @@
27
27
  "node": ">=22"
28
28
  },
29
29
  "peerDependencies": {
30
- "@linzjs/lui": ">=21",
30
+ "@linzjs/lui": ">=24",
31
31
  "lodash-es": ">=4",
32
32
  "react": ">=18",
33
33
  "react-dom": ">=18"
@@ -46,7 +46,7 @@
46
46
  "build": "node --eval \"fs.mkdirSync('./dist', { recursive: true })\" && run-s clean lintall css bundle",
47
47
  "yalc": "run-s clean css bundle && yalc push",
48
48
  "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
49
- "bundle": "rollup -c",
49
+ "bundle": "tsc -p tsconfig.json && rollup -c",
50
50
  "css": "sass ./src/styles/index.scss:dist/index.css --no-source-map",
51
51
  "test": "cross-env vitest run",
52
52
  "test:watch": "vitest --watch",
@@ -59,7 +59,7 @@
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 npm-check-updates -i --format group -c 1d",
63
63
  "prepare": "husky"
64
64
  },
65
65
  "browserslist": {
@@ -75,50 +75,49 @@
75
75
  ]
76
76
  },
77
77
  "devDependencies": {
78
- "@chromatic-com/storybook": "^5.2.1",
79
- "@linzjs/lui": "^24.17.0",
80
- "@linzjs/windows": "^11.2.0",
78
+ "@chromatic-com/storybook": "^5.3.0",
79
+ "@linzjs/lui": "^24.20.0",
80
+ "@linzjs/windows": "^13.0.0",
81
81
  "@rollup/plugin-commonjs": "^29.0.3",
82
82
  "@rollup/plugin-json": "^6.1.0",
83
83
  "@rollup/plugin-node-resolve": "^16.0.3",
84
- "@storybook/addon-docs": "^10.4.6",
85
- "@storybook/addon-links": "^10.4.6",
86
- "@storybook/react": "^10.4.6",
87
- "@storybook/react-vite": "^10.4.6",
84
+ "rollup-plugin-esbuild": "^6.2.1",
85
+ "@storybook/addon-docs": "^10.5.7",
86
+ "@storybook/addon-links": "^10.5.7",
87
+ "@storybook/react-vite": "^10.5.7",
88
88
  "@testing-library/dom": "^10.4.1",
89
89
  "@testing-library/react": "^16.3.2",
90
- "@testing-library/user-event": "^14.6.1",
90
+ "@testing-library/user-event": "^14.6.3",
91
91
  "@types/debounce-promise": "^3.1.9",
92
92
  "@types/jest": "^30.0.0",
93
93
  "@types/jsdom": "^28.0.3",
94
94
  "@types/lodash-es": "^4.17.12",
95
- "@types/node": "^24.13.2",
96
- "@types/react": "^18.3.27",
95
+ "@types/node": "^24.13.3",
96
+ "@types/react": "^18.3.31",
97
97
  "@types/react-dom": "^18.3.7",
98
- "@vitejs/plugin-react": "^6.0.3",
99
- "@vitest/ui": "^4.1.9",
100
- "chromatic": "^17.6.0",
98
+ "@vitejs/plugin-react": "^6.0.5",
99
+ "@vitest/ui": "^4.1.10",
100
+ "chromatic": "^17.8.0",
101
101
  "cross-env": "^10.1.0",
102
102
  "husky": "^9.1.7",
103
103
  "jsdom": "29.1.1",
104
104
  "lodash-es": "^4.18.1",
105
105
  "npm-run-all": "^4.1.5",
106
- "oxfmt": "^0.56.0",
107
- "oxlint": "^1.71.0",
108
- "postcss": "^8.5.15",
106
+ "oxfmt": "^0.62.0",
107
+ "oxlint": "^1.77.0",
108
+ "postcss": "^8.5.26",
109
109
  "postcss-scss": "^4.0.9",
110
110
  "react": ">=19",
111
- "react-dom": "^19.2.7",
112
- "rollup": "^4.62.2",
111
+ "react-dom": "^19.2.8",
112
+ "rollup": "^4.62.4",
113
113
  "rollup-plugin-copy": "^3.5.0",
114
114
  "rollup-plugin-peer-deps-external": "^2.2.4",
115
115
  "rollup-plugin-postcss": "^4.0.2",
116
- "rollup-plugin-typescript2": "^0.37.0",
117
- "sass": "^1.101.0",
118
- "storybook": "^10.4.6",
119
- "typescript": "^6.0.3",
120
- "vite": "^8.1.0",
121
- "vitest": "^4.1.9"
116
+ "sass": "^1.102.0",
117
+ "storybook": "^10.5.7",
118
+ "typescript": "^7.0.2",
119
+ "vite": "^8.2.1",
120
+ "vitest": "^4.1.10"
122
121
  },
123
122
  "husky": {
124
123
  "hooks": {
@@ -1106,6 +1106,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
1106
1106
  <div style={{ flex: 1 }} ref={gridDivRef} onCopy={onCopyEvent}>
1107
1107
  <AgGridReact
1108
1108
  theme={'legacy'}
1109
+ tooltipShowDelay={500}
1109
1110
  domLayout={params.domLayout}
1110
1111
  defaultColDef={defaultColDef}
1111
1112
  columnDefs={columnDefsAdjusted}
@@ -2,6 +2,7 @@ import { CellFocusedEvent, IRowNode } from 'ag-grid-community';
2
2
  import { CustomCellEditorProps } from 'ag-grid-react';
3
3
  import clsx from 'clsx';
4
4
  import { useCallback, useEffect, useRef } from 'react';
5
+ import { isEmpty } from 'lodash-es';
5
6
 
6
7
  import { useGridContext } from '../../contexts/GridContext';
7
8
  import { fnOrVar } from '../../utils/util';
@@ -11,10 +12,11 @@ import { GenericCellColDef } from '../gridRender';
11
12
  import { ColDefT, GridBaseRow } from '../types';
12
13
  import { useSharedInterval } from '../../utils/useSharedInterval';
13
14
 
14
- const BooleanCellRenderer = (props: CustomCellEditorProps) => {
15
+ const BooleanCellRenderer = <TData extends GridBaseRow>(props: CustomCellEditorProps) => {
15
16
  const { onValueChange, value, api, node, column, colDef, data, eGridCell } = props;
16
17
  const inputRef = useRef<HTMLInputElement>(null);
17
- const { updatingCells, redrawRows, resetFocusedCellAfterCellEditing, prePopupOps } = useGridContext();
18
+ const { updatingCells, redrawRows, resetFocusedCellAfterCellEditing, getSelectedRows, prePopupOps } =
19
+ useGridContext<TData>();
18
20
 
19
21
  useEffect(() => {
20
22
  const checkFocus = (event: CellFocusedEvent) => {
@@ -38,9 +40,26 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
38
40
  return;
39
41
  }
40
42
 
43
+ const multiEdit = params.multiEdit ?? false;
44
+ let selectedRows: TData[] = [];
45
+ if (multiEdit) {
46
+ // Sort clicked row to top among all selected rows
47
+ selectedRows = getSelectedRows<TData>().sort((a, b) => {
48
+ const aa = a.id === data.id;
49
+ const bb = b.id === data.id;
50
+ if (aa && !bb) return -1;
51
+ if (!aa && bb) return 1;
52
+ return 0;
53
+ });
54
+ }
55
+ if (isEmpty(selectedRows)) {
56
+ selectedRows = [data as TData];
57
+ }
58
+ const selectedRowIds = selectedRows.map((row) => row.id);
59
+
41
60
  const nodes: IRowNode[] = [];
42
61
  api.forEachNode((n) => {
43
- if (n.data.id === data.id) {
62
+ if (selectedRowIds.includes(n.data.id)) {
44
63
  nodes.push(n);
45
64
  }
46
65
  });
@@ -50,7 +69,6 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
50
69
  const checked = !value;
51
70
  onValueChange(checked);
52
71
  const field = props.colDef?.field ?? props.colDef?.colId ?? '';
53
- const selectedRows = nodes.map((node) => node.data);
54
72
  void updatingCells({ selectedRows, field }, async () => {
55
73
  redrawRows(nodes);
56
74
  return params.onClick({ selectedRows, selectedRowIds: selectedRows.map((r) => r.id), checked });
@@ -59,12 +77,13 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
59
77
  });
60
78
  }, [
61
79
  api,
62
- data.id,
80
+ data,
63
81
  onValueChange,
64
82
  prePopupOps,
65
83
  props.colDef?.cellEditorParams,
66
84
  props.colDef?.colId,
67
85
  props.colDef?.field,
86
+ getSelectedRows,
68
87
  redrawRows,
69
88
  resetFocusedCellAfterCellEditing,
70
89
  updatingCells,
@@ -75,9 +94,16 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
75
94
  if (isDisabled) {
76
95
  return;
77
96
  }
78
- const cell: HTMLElement | null = eGridCell?.querySelector('.ag-cell-focus .grid-edit-boolean input.ag-checkbox-input:not(:disabled)');
79
- if (cell && cell.ownerDocument.activeElement !== cell) {
80
- cell.focus();
97
+ const cell: HTMLElement | null = eGridCell?.querySelector(
98
+ '.ag-cell-focus .grid-edit-boolean input.ag-checkbox-input:not(:disabled)',
99
+ );
100
+ if (cell) {
101
+ // When you redraw a cell the activeElement moves to the cell div away from input
102
+ // Refocus here if the cell div is active back to the input
103
+ const activeElement = cell.ownerDocument.activeElement;
104
+ if (activeElement && activeElement.getAttribute('role') === 'gridcell' && activeElement !== cell) {
105
+ cell.focus();
106
+ }
81
107
  }
82
108
  });
83
109
 
@@ -105,6 +131,7 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
105
131
  };
106
132
 
107
133
  export interface GridEditBooleanEditorProps<TData extends GridBaseRow> extends CellEditorCommon {
134
+ multiEdit?: boolean;
108
135
  onClick: (props: {
109
136
  selectedRows: TData[];
110
137
  selectedRowIds: TData['id'][];
@@ -0,0 +1 @@
1
+ declare module '*.scss';
@@ -1,8 +1,3 @@
1
- import '../../styles/GridTheme.scss';
2
- import '../../styles/index.scss';
3
- import '@linzjs/lui/dist/scss/base.scss';
4
- import '@linzjs/lui/dist/fonts';
5
-
6
1
  import { Meta, StoryFn } from '@storybook/react-vite';
7
2
  import { useMemo, useState } from 'react';
8
3
 
@@ -16,10 +11,17 @@ import {
16
11
  GridPopoverEditDropDown,
17
12
  GridUpdatingContextProvider,
18
13
  primitiveToSelectOption,
19
- wait,
20
14
  } from '../..';
15
+ import { expect, userEvent, waitFor } from 'storybook/test';
16
+
21
17
  import { waitForGridReady } from '../../utils/__tests__/storybookTestUtil';
22
- import { IFormTestRow } from './FormTest';
18
+
19
+ interface IBoolTestRow {
20
+ id: number;
21
+ name: string;
22
+ boldSingleSelect: boolean;
23
+ boldMultiSelect: boolean;
24
+ }
23
25
 
24
26
  export default {
25
27
  title: 'Components / Grids',
@@ -39,11 +41,11 @@ export default {
39
41
 
40
42
  const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
41
43
  const [rowData, setRowData] = useState([
42
- { id: 1000, name: 'IS IS DP12345', nameType: 'IS', numba: 'IX', plan: 'DP 12345', bold: true },
43
- { id: 1001, name: 'PEG V SD523', nameType: 'PEG', numba: 'V', plan: 'SD 523', bold: false },
44
- ] as IFormTestRow[]);
44
+ { id: 1000, name: 'IS IS DP12345', boldSingleSelect: true, boldMultiSelect: false },
45
+ { id: 1001, name: 'PEG V SD523', boldSingleSelect: true, boldMultiSelect: false },
46
+ ] as IBoolTestRow[]);
45
47
 
46
- const columnDefs: ColDefT<IFormTestRow>[] = useMemo(
48
+ const columnDefs: ColDefT<IBoolTestRow>[] = useMemo(
47
49
  () => [
48
50
  GridCell({
49
51
  field: 'id',
@@ -63,16 +65,15 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
63
65
  ),
64
66
  GridEditBoolean(
65
67
  {
66
- field: 'bold',
68
+ field: 'boldSingleSelect',
69
+ headerName: 'Bold single-select',
67
70
  },
68
71
  {
72
+ // multiEdit defaults to false — only the clicked row is updated
69
73
  onClick: async ({ selectedRowIds, checked }) => {
70
- await wait(1000);
71
- setRowData((rowData) => {
72
- // eslint-disable-next-line no-console
73
- console.log('onchange', selectedRowIds, checked);
74
- return rowData.map((row) => (selectedRowIds.includes(row.id) ? { ...row, bold: checked } : row));
75
- });
74
+ setRowData((prev) =>
75
+ prev.map((row) => (selectedRowIds.includes(row.id) ? { ...row, boldSingleSelect: checked } : row)),
76
+ );
76
77
  return true;
77
78
  },
78
79
  },
@@ -95,16 +96,15 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
95
96
  ),
96
97
  GridEditBoolean(
97
98
  {
98
- colId: 'bold2',
99
- field: 'bold',
99
+ field: 'boldMultiSelect',
100
+ headerName: 'Bold multi-select',
100
101
  },
101
102
  {
103
+ multiEdit: true,
102
104
  onClick: async ({ selectedRowIds, checked }) => {
103
- setRowData((rowData) => {
104
- // eslint-disable-next-line no-console
105
- console.log('onchange', selectedRowIds, checked);
106
- return rowData.map((row) => (selectedRowIds.includes(row.id) ? { ...row, bold: checked } : row));
107
- });
105
+ setRowData((prev) =>
106
+ prev.map((row) => (selectedRowIds.includes(row.id) ? { ...row, boldMultiSelect: checked } : row)),
107
+ );
108
108
  return true;
109
109
  },
110
110
  },
@@ -119,9 +119,9 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
119
119
  theme={'ag-theme-step-view-list-default'}
120
120
  columnDefs={columnDefs}
121
121
  rowData={rowData}
122
- selectable={false}
122
+ selectable={true}
123
123
  singleClickEdit={true}
124
- rowSelection="single"
124
+ rowSelection="multiple"
125
125
  domLayout={'autoHeight'}
126
126
  />
127
127
  </>
@@ -130,3 +130,89 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
130
130
 
131
131
  export const _EditBoolean = GridPopoutEditBooleanTemplate.bind({});
132
132
  _EditBoolean.play = waitForGridReady;
133
+
134
+ export const _EditBooleanRetainsFocusAfterToggle = GridPopoutEditBooleanTemplate.bind({});
135
+ _EditBooleanRetainsFocusAfterToggle.play = async (context) => {
136
+ await waitForGridReady(context);
137
+ const { canvasElement } = context;
138
+
139
+ // Use boldMultiSelect (no async delay) so the redraw happens immediately
140
+ const getBoolInput = () =>
141
+ canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"] input.ag-checkbox-input') as HTMLElement;
142
+
143
+ await userEvent.click(canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"]') as HTMLElement);
144
+ await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
145
+
146
+ // Space toggles the checkbox, which triggers redrawRows — ag-grid moves focus from the
147
+ // input to the cell div (role="gridcell"). The shared interval should detect this and restore focus.
148
+ await userEvent.keyboard(' ');
149
+
150
+ // Re-query the input each retry since redraw replaces the DOM node
151
+ await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
152
+ };
153
+
154
+ const selectRow = async (canvasElement: HTMLElement, rowId: string) => {
155
+ await userEvent.click(
156
+ canvasElement.querySelector(`.ag-row[row-id="${rowId}"] .ag-cell[col-id="ag-Grid-SelectionColumn"]`) as HTMLElement,
157
+ );
158
+ await waitFor(() =>
159
+ expect(canvasElement.querySelector(`.ag-row[row-id="${rowId}"].ag-row-selected`)).toBeInTheDocument(),
160
+ );
161
+ };
162
+
163
+ const getBoolInput = (canvasElement: HTMLElement, rowId: string, colId: string) =>
164
+ canvasElement.querySelector(
165
+ `.ag-row[row-id="${rowId}"] .ag-cell[col-id="${colId}"] input.ag-checkbox-input`,
166
+ ) as HTMLInputElement;
167
+
168
+ export const _EditBooleanSingleSelectOnlyAffectsClickedRow = GridPopoutEditBooleanTemplate.bind({});
169
+ _EditBooleanSingleSelectOnlyAffectsClickedRow.play = async (context) => {
170
+ await waitForGridReady(context);
171
+ const { canvasElement } = context;
172
+
173
+ // Select both rows so we can prove single-select ignores the multi-row selection
174
+ await selectRow(canvasElement, '1000');
175
+ await selectRow(canvasElement, '1001');
176
+
177
+ // Both rows start with boldSingleSelect: true
178
+ await waitFor(() => {
179
+ expect(getBoolInput(canvasElement, '1000', 'boldSingleSelect').checked).toBe(true);
180
+ expect(getBoolInput(canvasElement, '1001', 'boldSingleSelect').checked).toBe(true);
181
+ });
182
+
183
+ // Click row 1000's boldSingleSelect — multiEdit defaults to false, so only row 1000 changes
184
+ await userEvent.click(
185
+ canvasElement.querySelector('.ag-row[row-id="1000"] .ag-cell[col-id="boldSingleSelect"]') as HTMLElement,
186
+ );
187
+
188
+ await waitFor(() => {
189
+ expect(getBoolInput(canvasElement, '1000', 'boldSingleSelect').checked).toBe(false);
190
+ expect(getBoolInput(canvasElement, '1001', 'boldSingleSelect').checked).toBe(true); // unchanged
191
+ });
192
+ };
193
+
194
+ export const _EditBooleanMultiSelectEditsAllSelectedRows = GridPopoutEditBooleanTemplate.bind({});
195
+ _EditBooleanMultiSelectEditsAllSelectedRows.play = async (context) => {
196
+ await waitForGridReady(context);
197
+ const { canvasElement } = context;
198
+
199
+ // Select both rows before clicking
200
+ await selectRow(canvasElement, '1000');
201
+ await selectRow(canvasElement, '1001');
202
+
203
+ // Both rows start with boldMultiSelect: false
204
+ await waitFor(() => {
205
+ expect(getBoolInput(canvasElement, '1000', 'boldMultiSelect').checked).toBe(false);
206
+ expect(getBoolInput(canvasElement, '1001', 'boldMultiSelect').checked).toBe(false);
207
+ });
208
+
209
+ // Click row 1000's boldMultiSelect — multiEdit: true, so all selected rows change
210
+ await userEvent.click(
211
+ canvasElement.querySelector('.ag-row[row-id="1000"] .ag-cell[col-id="boldMultiSelect"]') as HTMLElement,
212
+ );
213
+
214
+ await waitFor(() => {
215
+ expect(getBoolInput(canvasElement, '1000', 'boldMultiSelect').checked).toBe(true);
216
+ expect(getBoolInput(canvasElement, '1001', 'boldMultiSelect').checked).toBe(true); // also changed
217
+ });
218
+ };
package/src/utils/util.ts CHANGED
@@ -54,7 +54,7 @@ export const fnOrVar = (fn: any, param?: any) => (typeof fn === 'function' ? fn(
54
54
  export const sanitiseFileName = (filename: string): string => {
55
55
  const valid = filename
56
56
  .trim()
57
- .replaceAll(/([\/\\])+/g, '-')
57
+ .replaceAll(/([/\\])+/g, '-')
58
58
  .replaceAll(/\s+/g, '_')
59
59
  .replaceAll(/[^\w\-āēīōūĀĒĪŌŪ.]/g, '');
60
60
  const parts = valid.split('.');
@@ -1,2 +0,0 @@
1
- declare const _default: import("vite").UserConfigFnObject;
2
- export default _default;