@linzjs/step-ag-grid 30.3.0 → 30.4.1

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 (32) hide show
  1. package/dist/src/components/gridForm/GridFormInlineTextInput.d.ts +5 -0
  2. package/dist/src/contexts/GridPopoverContext.d.ts +1 -1
  3. package/dist/src/react-menu3/positionUtils/placeArrowHorizontal.d.ts +2 -2
  4. package/dist/src/react-menu3/positionUtils/placeArrowVertical.d.ts +2 -2
  5. package/dist/src/react-menu3/positionUtils/placeLeftorRight.d.ts +2 -2
  6. package/dist/src/react-menu3/positionUtils/placeToporBottom.d.ts +2 -2
  7. package/dist/src/react-menu3/positionUtils/positionMenu.d.ts +3 -3
  8. package/dist/src/react-menu3/types.d.ts +1 -1
  9. package/dist/step-ag-grid.cjs +35 -24
  10. package/dist/step-ag-grid.cjs.map +1 -1
  11. package/dist/step-ag-grid.esm.js +35 -24
  12. package/dist/step-ag-grid.esm.js.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/Grid.tsx +3 -3
  15. package/src/components/gridForm/GridFormInlineTextInput.tsx +9 -1
  16. package/src/contexts/GridContextProvider.tsx +3 -3
  17. package/src/contexts/GridPopoverContext.tsx +2 -2
  18. package/src/contexts/GridUpdatingContextProvider.tsx +10 -7
  19. package/src/lui/reactUtils.tsx +1 -1
  20. package/src/lui/timeoutHook.tsx +1 -1
  21. package/src/react-menu3/components/ControlledMenu.tsx +3 -3
  22. package/src/react-menu3/components/SubMenu.tsx +1 -1
  23. package/src/react-menu3/hooks/useItemState.ts +1 -1
  24. package/src/react-menu3/positionUtils/placeArrowHorizontal.ts +2 -2
  25. package/src/react-menu3/positionUtils/placeArrowVertical.ts +2 -2
  26. package/src/react-menu3/positionUtils/placeLeftorRight.ts +2 -2
  27. package/src/react-menu3/positionUtils/placeToporBottom.ts +2 -2
  28. package/src/react-menu3/positionUtils/positionMenu.ts +3 -3
  29. package/src/react-menu3/types.ts +1 -1
  30. package/src/stories/grid/GridInlineText.stories.tsx +99 -18
  31. package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +12 -3
  32. package/src/utils/deferredPromise.ts +2 -2
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": "30.3.0",
5
+ "version": "30.4.1",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -116,7 +116,7 @@
116
116
  "eslint-plugin-storybook": "^9.1.17",
117
117
  "husky": "^9.1.7",
118
118
  "jsdom": "^26.1.0",
119
- "lodash-es": "^4.17.21",
119
+ "lodash-es": "^4.17.23",
120
120
  "mkdirp": "^3.0.1",
121
121
  "npm-run-all": "^4.1.5",
122
122
  "postcss": "^8.5.6",
@@ -206,7 +206,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
206
206
  const { updatedDep, anyUpdating, updatingCols } = useContext(GridUpdatingContext);
207
207
 
208
208
  const gridDivRef = useRef<HTMLDivElement>(null);
209
- const lastSelectedIds = useRef<TData['id'][] | undefined>();
209
+ const lastSelectedIds = useRef<TData['id'][]>(undefined);
210
210
 
211
211
  const [staleGrid, setStaleGrid] = useState(false);
212
212
  const [autoSized, setAutoSized] = useState(false);
@@ -299,7 +299,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
299
299
  }
300
300
  }, [autoSizeColumns, gridRenderState, maxInitialWidth, params, rowData, sizeColumns, sizeColumnsToFit]);
301
301
 
302
- const lastOwnerDocumentRef = useRef<Document>();
302
+ const lastOwnerDocumentRef = useRef<Document>(undefined);
303
303
  const wasVisibleRef = useRef(false);
304
304
 
305
305
  /**
@@ -858,7 +858,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
858
858
  });
859
859
  }, []);
860
860
 
861
- const gridElementRef = useRef<Element>();
861
+ const gridElementRef = useRef<Element>(undefined);
862
862
  const onRowDragMove = useCallback(
863
863
  (event: RowDragMoveEvent) => {
864
864
  if (startDragYRef.current === null) {
@@ -16,6 +16,7 @@ export interface GridFormInlineTextInput<TData extends GridBaseRow>
16
16
  placeholder?: string;
17
17
  units?: string;
18
18
  onSave?: (props: { selectedRows: TData[]; selectedRowIds: TData['id'][]; value: string }) => MaybePromise<boolean>;
19
+ onChange?: (props: { selectedRows: TData[]; selectedRowIds: TData['id'][]; value: string }) => void;
19
20
  helpText?: string;
20
21
  }
21
22
 
@@ -103,7 +104,14 @@ export const GridFormInlineTextInput = <TData extends GridBaseRow>(props: GridFo
103
104
  ref={inputRef}
104
105
  type={'text'}
105
106
  value={value}
106
- onChange={(e) => setValue(e.target.value)}
107
+ onChange={(e) => {
108
+ setValue(e.target.value);
109
+ if (props.onChange) {
110
+ const selectedRows = getSelectedRows<TData>();
111
+ const selectedRowIds = selectedRows.map((data) => data.id);
112
+ props.onChange({ selectedRows, selectedRowIds, value: e.target.value });
113
+ }
114
+ }}
107
115
  placeholder={props.placeholder ?? 'Type here'}
108
116
  onBlur={() => {
109
117
  void triggerSave(CloseReason.BLUR);
@@ -36,11 +36,11 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
36
36
  const [gridReady, setGridReady] = useState(false);
37
37
  const [quickFilter, _setQuickFilter] = useState('');
38
38
  const [invisibleColumnIds, _setInvisibleColumnIds] = useState<string[]>();
39
- const testId = useRef<string | undefined>();
39
+ const testId = useRef<string>(undefined);
40
40
  const hasExternallySelectedItemsRef = useRef(false);
41
41
  const enableMultilineBulkEditRef = useRef(false);
42
42
  const idsBeforeUpdate = useRef<TData['id'][]>([]);
43
- const prePopupFocusedCell = useRef<CellPosition>();
43
+ const prePopupFocusedCell = useRef<CellPosition>(undefined);
44
44
  const [externallySelectedItemsAreInSync, _setExternallySelectedItemsAreInSync] = useState(false);
45
45
  const externalFilters = useRef<GridFilterExternal<TData>[]>([]);
46
46
 
@@ -621,7 +621,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
621
621
  [anyUpdating, gridApi, prePopupOps, setExternallySelectedItemsAreInSync, waitForExternallySelectedItemsToBeInSync],
622
622
  );
623
623
 
624
- const bulkEditingCompleteCallbackRef = useRef<() => Promise<void> | void>();
624
+ const bulkEditingCompleteCallbackRef = useRef<() => Promise<void> | void>(undefined);
625
625
  const onBulkEditingComplete = useCallback(async () => {
626
626
  await bulkEditingCompleteCallbackRef.current?.();
627
627
  }, []);
@@ -3,7 +3,7 @@ import { createContext, RefObject, useContext } from 'react';
3
3
  import { GridBaseRow } from '../components/types';
4
4
 
5
5
  export interface GridPopoverContextType<TData extends GridBaseRow> {
6
- anchorRef: RefObject<Element>;
6
+ anchorRef: RefObject<Element | undefined>;
7
7
  saving: boolean;
8
8
  setSaving: (saving: boolean) => void;
9
9
  stopEditing: () => void;
@@ -20,7 +20,7 @@ export interface GridPopoverContextType<TData extends GridBaseRow> {
20
20
  }
21
21
 
22
22
  export const GridPopoverContext = createContext<GridPopoverContextType<any>>({
23
- anchorRef: { current: null },
23
+ anchorRef: { current: undefined },
24
24
  saving: false,
25
25
  setSaving: () => {},
26
26
  stopEditing: () => {},
@@ -35,13 +35,16 @@ export const GridUpdatingContextProvider = (props: PropsWithChildren) => {
35
35
  const fieldUpdatingIds = updatingBlocks.current[field] ?? (updatingBlocks.current[field] = []);
36
36
  fieldUpdatingIds.push(idRef);
37
37
  });
38
- resetUpdating();
39
- await fn();
40
- castArray(fields).forEach((field) => {
41
- const fieldUpdatingIds = updatingBlocks.current[field];
42
- remove(fieldUpdatingIds, (idList) => idList === idRef);
43
- });
44
- resetUpdating();
38
+ try {
39
+ resetUpdating();
40
+ await fn();
41
+ } finally {
42
+ castArray(fields).forEach((field) => {
43
+ const fieldUpdatingIds = updatingBlocks.current[field];
44
+ remove(fieldUpdatingIds, (idList) => idList === idRef);
45
+ });
46
+ resetUpdating();
47
+ }
45
48
  },
46
49
  [resetUpdating],
47
50
  );
@@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react';
6
6
  * @param value Value to track.
7
7
  */
8
8
  export const usePrevious = <T,>(value: T): T | undefined => {
9
- const ref = useRef<T>();
9
+ const ref = useRef<T>(undefined);
10
10
  useEffect(() => {
11
11
  ref.current = value;
12
12
  }, [value]);
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useRef } from 'react';
8
8
  * but there's no way to enforce that, so it would lead to bugs.
9
9
  */
10
10
  export const useTimeoutHook = () => {
11
- const timeout = useRef<ReturnType<typeof setTimeout>>();
11
+ const timeout = useRef<ReturnType<typeof setTimeout>>(undefined);
12
12
 
13
13
  /**
14
14
  * Clear any pending timeouts.
@@ -35,7 +35,7 @@ export const ControlledMenuFr = (
35
35
  }: ControlledMenuProps & { saveButtonRef?: MutableRefObject<HTMLButtonElement | null> },
36
36
  externalRef: ForwardedRef<HTMLUListElement>,
37
37
  ) => {
38
- const containerRef = useRef<HTMLElement>();
38
+ const containerRef = useRef<HTMLElement>(undefined);
39
39
  const scrollNodesRef = useRef<{ anchors?: HTMLDivElement[] }>({});
40
40
  const { anchorRef, state } = restProps;
41
41
 
@@ -114,8 +114,8 @@ export const ControlledMenuFr = (
114
114
  [isWithinMenu],
115
115
  );
116
116
 
117
- const lastTabDownEl = useRef<Element>();
118
- const lastEnterDownEl = useRef<Element>();
117
+ const lastTabDownEl = useRef<Element>(undefined);
118
+ const lastEnterDownEl = useRef<Element>(undefined);
119
119
  const handleKeyboardTabAndEnter = useCallback(
120
120
  (isDown: boolean) => (ev: KeyboardEvent) => {
121
121
  const thisDocument = anchorRef?.current ? anchorRef?.current.ownerDocument : document;
@@ -182,7 +182,7 @@ export const SubMenuFr = ({
182
182
  const isDisabled = !!disabled;
183
183
  const isOpen = isMenuOpen(state);
184
184
  const containerRef = useRef(null);
185
- const timeoutId = useRef<ReturnType<typeof setTimeout>>();
185
+ const timeoutId = useRef<ReturnType<typeof setTimeout>>(undefined);
186
186
 
187
187
  const stopTimer = () => {
188
188
  if (timeoutId.current) {
@@ -14,7 +14,7 @@ export const useItemState = (
14
14
  ) => {
15
15
  const { submenuCloseDelay } = useContext(ItemSettingsContext);
16
16
  const { isParentOpen, isSubmenuOpen, dispatch, updateItems } = useContext(MenuListItemContext);
17
- const timeoutId = useRef<ReturnType<typeof setTimeout>>();
17
+ const timeoutId = useRef<ReturnType<typeof setTimeout>>(undefined);
18
18
 
19
19
  const setHover = () => {
20
20
  !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
@@ -1,7 +1,7 @@
1
- import { MutableRefObject } from 'react';
1
+ import { RefObject } from 'react';
2
2
 
3
3
  export const placeArrowHorizontal = (p: {
4
- arrowRef: MutableRefObject<HTMLElement | null>;
4
+ arrowRef: RefObject<HTMLElement | undefined>;
5
5
  menuX: number;
6
6
  anchorRect: DOMRect;
7
7
  containerRect: DOMRect;
@@ -1,7 +1,7 @@
1
- import { MutableRefObject } from 'react';
1
+ import { RefObject } from 'react';
2
2
 
3
3
  export const placeArrowVertical = (p: {
4
- arrowRef: MutableRefObject<HTMLElement | null>;
4
+ arrowRef: RefObject<HTMLElement | undefined>;
5
5
  menuY: number;
6
6
  anchorRect: DOMRect;
7
7
  containerRect: DOMRect;
@@ -1,4 +1,4 @@
1
- import { MutableRefObject } from 'react';
1
+ import { RefObject } from 'react';
2
2
 
3
3
  import { MenuDirection } from '../types';
4
4
  import { getPositionHelpers } from './getPositionHelpers';
@@ -9,7 +9,7 @@ export interface placeLeftorRightParams {
9
9
  placeLeftorRightY: number;
10
10
  placeLeftX: number;
11
11
  placeRightX: number;
12
- arrowRef: MutableRefObject<HTMLElement | null>;
12
+ arrowRef: RefObject<HTMLElement | undefined>;
13
13
  arrow?: boolean;
14
14
  direction: MenuDirection;
15
15
  position: 'auto' | 'anchor' | 'initial';
@@ -1,4 +1,4 @@
1
- import { MutableRefObject } from 'react';
1
+ import { RefObject } from 'react';
2
2
 
3
3
  import { MenuDirection } from '../types';
4
4
  import { getPositionHelpers } from './getPositionHelpers';
@@ -9,7 +9,7 @@ export interface placeToporBottomParams {
9
9
  placeToporBottomX: number;
10
10
  placeTopY: number;
11
11
  placeBottomY: number;
12
- arrowRef: MutableRefObject<HTMLElement | null>;
12
+ arrowRef: RefObject<HTMLElement | undefined>;
13
13
  arrow?: boolean;
14
14
  direction: 'left' | 'right' | 'top' | 'bottom';
15
15
  position: 'auto' | 'anchor' | 'initial';
@@ -1,4 +1,4 @@
1
- import { MutableRefObject } from 'react';
1
+ import { RefObject } from 'react';
2
2
 
3
3
  import { MenuDirection } from '../types';
4
4
  import { getPositionHelpers } from './getPositionHelpers';
@@ -8,8 +8,8 @@ import { placeToporBottom } from './placeToporBottom';
8
8
  interface positionMenuProps {
9
9
  offsetX: number;
10
10
  offsetY: number;
11
- arrowRef: MutableRefObject<HTMLDivElement | null>;
12
- anchorRef: MutableRefObject<Element | null>;
11
+ arrowRef: RefObject<HTMLDivElement | undefined>;
12
+ anchorRef: RefObject<Element | undefined>;
13
13
  arrow?: boolean;
14
14
  direction: MenuDirection;
15
15
  position: 'auto' | 'anchor' | 'initial';
@@ -411,7 +411,7 @@ export interface ControlledMenuProps extends RootMenuProps, ExtraMenuProps {
411
411
  *
412
412
  * *Don't set this prop for context menu*
413
413
  */
414
- anchorRef?: React.RefObject<Element>;
414
+ anchorRef?: React.RefObject<Element | undefined>;
415
415
  skipOpen?: React.MutableRefObject<boolean>;
416
416
  /**
417
417
  * If `true`, the menu list element will gain focus after menu is open.
@@ -3,8 +3,9 @@ import '../../styles/index.scss';
3
3
  import '@linzjs/lui/dist/scss/base.scss';
4
4
  import '@linzjs/lui/dist/fonts';
5
5
 
6
- import { Meta, StoryFn } from '@storybook/react-vite';
6
+ import type { Meta, StoryFn } from '@storybook/react-vite';
7
7
  import { useCallback, useContext, useMemo, useState } from 'react';
8
+ import { expect, screen, userEvent, within } from 'storybook/test';
8
9
 
9
10
  import {
10
11
  ActionButton,
@@ -27,6 +28,11 @@ interface IFormTestRow {
27
28
  text2: string | null;
28
29
  }
29
30
 
31
+ const initialRows: IFormTestRow[] = [
32
+ { id: 1000, text1: 'a text1', text2: 'a text2' },
33
+ { id: 1001, text1: 'b text1', text2: 'b text2' },
34
+ ];
35
+
30
36
  export default {
31
37
  title: 'Components / Grids',
32
38
  component: Grid,
@@ -42,11 +48,6 @@ export default {
42
48
  <Story />
43
49
  </GridContextProvider>
44
50
  </GridUpdatingContextProvider>
45
- <GridUpdatingContextProvider>
46
- <GridContextProvider>
47
- <Story />
48
- </GridContextProvider>
49
- </GridUpdatingContextProvider>
50
51
  </div>
51
52
  ),
52
53
  ],
@@ -55,18 +56,7 @@ export default {
55
56
  const GridInlineTextTemplate: StoryFn<typeof Grid<IFormTestRow>> = (props: GridProps<IFormTestRow>) => {
56
57
  const { selectRowsWithFlashDiff } = useContext(GridContext);
57
58
  const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
58
- const [rowData, setRowData] = useState([
59
- {
60
- id: 1000,
61
- text1: 'a text1',
62
- text2: 'a text2',
63
- },
64
- {
65
- id: 1001,
66
- text1: 'b text1',
67
- text2: 'b text2',
68
- },
69
- ] as IFormTestRow[]);
59
+ const [rowData, setRowData] = useState<IFormTestRow[]>(initialRows);
70
60
 
71
61
  const columnDefs: ColDefT<IFormTestRow>[] = useMemo(
72
62
  () => [
@@ -183,3 +173,94 @@ const GridInlineTextTemplate: StoryFn<typeof Grid<IFormTestRow>> = (props: GridP
183
173
 
184
174
  export const _EditInlineText = GridInlineTextTemplate.bind({});
185
175
  _EditInlineText.play = waitForGridReady;
176
+
177
+ const _InlineText_ChangeEventsTemplate: StoryFn<typeof Grid<IFormTestRow>> = (props: GridProps<IFormTestRow>) => {
178
+ const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
179
+ const [isDirty, setIsDirty] = useState(false);
180
+ const [rowData, setRowData] = useState<IFormTestRow[]>(initialRows);
181
+
182
+ // Use a named handler for onChange
183
+ const handleInlineEditChange = () => {
184
+ setIsDirty(true);
185
+ };
186
+
187
+ const columnDefs: ColDefT<IFormTestRow>[] = useMemo(
188
+ () => [
189
+ GridCell({
190
+ field: 'id',
191
+ headerName: 'Id',
192
+ }),
193
+ GridInlineTextInput(
194
+ {
195
+ field: 'text1',
196
+ headerName: 'Text input 1',
197
+ },
198
+ {
199
+ editorParams: {
200
+ placeholder: 'Enter some text...',
201
+ onChange: handleInlineEditChange,
202
+ onSave: ({ selectedRowIds, value }) => {
203
+ setRowData(rowData.map((data) => (selectedRowIds.includes(data.id) ? { ...data, text1: value } : data)));
204
+ return true;
205
+ },
206
+ },
207
+ },
208
+ ),
209
+ GridInlineTextInput(
210
+ {
211
+ field: 'text2',
212
+ headerName: 'Text input 2',
213
+ },
214
+ {
215
+ editorParams: {
216
+ placeholder: 'Enter some text...',
217
+ onChange: handleInlineEditChange,
218
+ onSave: ({ selectedRowIds, value }) => {
219
+ setRowData(rowData.map((data) => (selectedRowIds.includes(data.id) ? { ...data, text2: value } : data)));
220
+ return true;
221
+ },
222
+ },
223
+ },
224
+ ),
225
+ ],
226
+ [rowData],
227
+ );
228
+
229
+ return (
230
+ <>
231
+ <Grid
232
+ {...props}
233
+ selectable={true}
234
+ hideSelectColumn={true}
235
+ externalSelectedItems={externalSelectedItems}
236
+ setExternalSelectedItems={setExternalSelectedItems}
237
+ columnDefs={columnDefs}
238
+ rowData={rowData}
239
+ domLayout={'autoHeight'}
240
+ defaultColDef={{ minWidth: 70 }}
241
+ sizeColumns={'auto'}
242
+ singleClickEdit={true}
243
+ enableMultilineBulkEdit={true}
244
+ />
245
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
246
+ <button type="button" data-testid="inline-edit-save" disabled={!isDirty} onClick={() => setIsDirty(false)}>
247
+ Save
248
+ </button>
249
+ </div>
250
+ </>
251
+ );
252
+ };
253
+
254
+ export const _InlineText_ChangeEvents = _InlineText_ChangeEventsTemplate.bind({});
255
+ _InlineText_ChangeEvents.play = async (context) => {
256
+ await waitForGridReady(context);
257
+ const canvas = within(context.canvasElement);
258
+ const saveButtons = canvas.getAllByTestId('inline-edit-save');
259
+ const saveButton = saveButtons[0];
260
+ await expect(saveButton).toBeDisabled();
261
+ const textCells = canvas.getAllByText('a text1');
262
+ await userEvent.click(textCells[0]);
263
+ const input = await screen.findByPlaceholderText('Enter some text...');
264
+ await userEvent.type(input, 'X');
265
+ await expect(saveButton).toBeEnabled();
266
+ };
@@ -256,11 +256,20 @@ KeyboardInteractions.play = async ({ canvasElement }) => {
256
256
  await userEvent.keyboard('{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}{arrowright}');
257
257
 
258
258
  // Test enter post focus
259
- const test = async (fn: () => any, coldIndex: string, rowIndex: string) => {
259
+ const test = async (fn: () => Promise<any>, coldIndex: string, rowIndex: string) => {
260
260
  await userEvent.keyboard('{Enter}');
261
+ await waitFor(() => {
262
+ expect(canvasElement.ownerDocument.querySelector('.szh-menu')).toBeInTheDocument();
263
+ });
261
264
  await wait(1000);
262
- await userEvent.keyboard('{arrowdown}{arrowdown}');
263
- fn();
265
+ for (let i = 0; i < 2; i++) {
266
+ await userEvent.keyboard('{arrowdown}');
267
+ await waitFor(() => {
268
+ const menuItems = canvasElement.ownerDocument.querySelectorAll('[role="menuitem"]');
269
+ expect(menuItems[i]).toHaveClass('szh-menu__item--hover');
270
+ });
271
+ }
272
+ await fn();
264
273
 
265
274
  await waitFor(() => {
266
275
  const activeCell = canvasElement.ownerDocument.activeElement;
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
 
3
3
  export const useDeferredPromise = <T>() => {
4
- const promiseResolve = useRef<((value: T | PromiseLike<T>) => void) | undefined>();
5
- const promiseReject = useRef<() => void>();
4
+ const promiseResolve = useRef<(value: T | PromiseLike<T>) => void>(undefined);
5
+ const promiseReject = useRef<() => void>(undefined);
6
6
 
7
7
  // End promise on unload
8
8
  useEffect(() => {