@linzjs/step-ag-grid 32.2.0 → 32.4.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.2.0",
5
+ "version": "32.4.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -28,6 +28,7 @@ import {
28
28
  ValueFormatterParams,
29
29
  } from 'ag-grid-community';
30
30
  import { AgGridReact } from 'ag-grid-react';
31
+ import isChromatic from 'chromatic/isChromatic';
31
32
  import clsx from 'clsx';
32
33
  import { defer, delay, difference, isEmpty, last, omit, xorBy } from 'lodash-es';
33
34
  import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@@ -487,6 +488,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
487
488
  'GridCell-readonly': (ccp: CellClassParams<TData>) =>
488
489
  !suppressReadOnlyStyle && !editable(ccp as unknown as EditableCallbackParams<TData>),
489
490
  },
491
+ width: colDef.minWidth || 120,
490
492
  };
491
493
  }
492
494
  },
@@ -1083,7 +1085,9 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
1083
1085
  const onGridSizeChanged = useCallback(
1084
1086
  (event: GridSizeChangedEvent<TData>) => {
1085
1087
  if (sizeColumns === 'fit' || (['auto', 'auto-skip-headers'].includes(sizeColumns) && hasSetContentSize.current)) {
1086
- event.api.sizeColumnsToFit();
1088
+ if (!isChromatic()) {
1089
+ event.api.sizeColumnsToFit();
1090
+ }
1087
1091
  }
1088
1092
  },
1089
1093
  [sizeColumns],
@@ -2,7 +2,11 @@ import { ReactElement, useCallback, useContext, useEffect } from 'react';
2
2
 
3
3
  import { GridSubComponentContext } from '../../contexts/GridSubComponentContext';
4
4
  import { TextInputFormatted } from '../../lui/TextInputFormatted';
5
- import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
5
+ import {
6
+ TextInputValidator,
7
+ textInputValidatorPropsToSummary,
8
+ TextInputValidatorProps,
9
+ } from '../../utils/textValidator';
6
10
  import { CellEditorCommon } from '../GridCell';
7
11
  import { GridBaseRow } from '../types';
8
12
 
@@ -41,6 +45,7 @@ export const GridFormSubComponentTextInput = <TData extends GridBaseRow>(
41
45
  placeholder={props.placeholder}
42
46
  style={{ width: '100%' }}
43
47
  allowTabToSave={true}
48
+ validationSummary={textInputValidatorPropsToSummary(props)}
44
49
  />
45
50
  );
46
51
  };
@@ -2,7 +2,11 @@ import { useCallback, useMemo, useState } from 'react';
2
2
 
3
3
  import { useGridPopoverContext } from '../../contexts/GridPopoverContext';
4
4
  import { TextInputFormatted } from '../../lui/TextInputFormatted';
5
- import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
5
+ import {
6
+ TextInputValidator,
7
+ textInputValidatorPropsToSummary,
8
+ TextInputValidatorProps,
9
+ } from '../../utils/textValidator';
6
10
  import { CellEditorCommon } from '../GridCell';
7
11
  import { useGridPopoverHook } from '../GridPopoverHook';
8
12
  import { GridBaseRow } from '../types';
@@ -63,6 +67,7 @@ export const GridFormTextInput = <TData extends GridBaseRow>(props: GridFormText
63
67
  style={{ width: props.width ?? 240 }}
64
68
  placeholder={props.placeholder ?? 'Type here'}
65
69
  helpText={helpText}
70
+ validationSummary={textInputValidatorPropsToSummary(props)}
66
71
  />
67
72
  </div>,
68
73
  );
@@ -18,7 +18,9 @@ const ButtonCellRenderer = <TData extends GridBaseRow>(props: ICellRendererParam
18
18
  };
19
19
  api.addEventListener('cellFocused', checkFocus);
20
20
  return () => {
21
- api.removeEventListener('cellFocused', checkFocus);
21
+ if (!api.isDestroyed()) {
22
+ api.removeEventListener('cellFocused', checkFocus);
23
+ }
22
24
  };
23
25
  }, [api, column, node.rowIndex]);
24
26
 
@@ -25,7 +25,11 @@ const BooleanCellRenderer = <TData extends GridBaseRow>(props: CustomCellEditorP
25
25
  }
26
26
  };
27
27
  api.addEventListener('cellFocused', checkFocus);
28
- return () => void api.removeEventListener('cellFocused', checkFocus);
28
+ return () => {
29
+ if (!api.isDestroyed()) {
30
+ api.removeEventListener('cellFocused', checkFocus);
31
+ }
32
+ };
29
33
  }, [api, column, node.rowIndex]);
30
34
 
31
35
  const isDisabled = !fnOrVar(colDef?.editable, props);
@@ -9,6 +9,7 @@ import {
9
9
  ProcessCellForExportParams,
10
10
  RowNode,
11
11
  } from 'ag-grid-community';
12
+ import isChromatic from 'chromatic/isChromatic';
12
13
  import debounce from 'debounce-promise';
13
14
  import { compact, defer, delay, difference, filter, isEmpty, last, pull, remove, sortBy, sumBy } from 'lodash-es';
14
15
  import { PropsWithChildren, ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@@ -463,6 +464,13 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
463
464
  return null;
464
465
  }
465
466
 
467
+ // Column widths are measured from rendered content, which is non-deterministic across
468
+ // Chromatic's environment. Skip measuring/resizing so snapshots stay stable at their
469
+ // configured widths, but still resolve with a width so callers don't retry forever.
470
+ if (isChromatic()) {
471
+ return { width: sumBy(gridApi.getColumnState(), (colState) => (colState.hide ? 0 : (colState.width ?? 0))) };
472
+ }
473
+
466
474
  try {
467
475
  const colIdsSet = colIds instanceof Set ? colIds : new Set(colIds);
468
476
 
@@ -530,7 +538,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
530
538
  */
531
539
  const sizeColumnsToFit = useCallback(
532
540
  (paramsOrGridWidth?: ISizeColumnsToFitParams): void => {
533
- if (gridApi && !gridApi?.isDestroyed()) {
541
+ if (gridApi && !gridApi?.isDestroyed() && !isChromatic()) {
534
542
  gridApi.sizeColumnsToFit(paramsOrGridWidth);
535
543
  }
536
544
  },
@@ -654,13 +662,13 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
654
662
  const nextColumn = focusedCell?.column;
655
663
  const nextColDef = nextColumn?.getColDef();
656
664
  const rowNode = focusedCell && gridApi.getDisplayedRowAtIndex(focusedCell?.rowIndex);
657
- const popupable = String(nextColDef?.cellClass).indexOf("GridCellEditableWithNoPopup") === -1
658
- return (
659
- !!(rowNode && nextColumn && nextColDef) &&
665
+ const popupable = String(nextColDef?.cellClass).indexOf('GridCellEditableWithNoPopup') === -1;
666
+ return !!(rowNode && nextColumn && nextColDef) &&
660
667
  nextColumn.isCellEditable(rowNode) &&
661
668
  !nextColDef.cellEditorParams?.preventAutoEdit &&
662
669
  !nextColDef.cellRendererParams?.editAction
663
- ) ? popupable : null
670
+ ? popupable
671
+ : null;
664
672
  };
665
673
 
666
674
  // Just in case I've missed something, we don't want the loop to hang everything
@@ -698,7 +706,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
698
706
 
699
707
  // checkbox cells are editable but don't have a popup
700
708
  // we need to end bulk editing and just select the cell
701
- const editable = focusedCellIsEditable()
709
+ const editable = focusedCellIsEditable();
702
710
  if (editable !== null) {
703
711
  const focusedCell = gridApi.getFocusedCell();
704
712
  if (focusedCell) {
@@ -708,7 +716,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
708
716
  return false;
709
717
  }
710
718
  if (editable) {
711
- await startCellEditing({rowId, colId: focusedCell.column.getColId()});
719
+ await startCellEditing({ rowId, colId: focusedCell.column.getColId() });
712
720
  // Continue bulk edit
713
721
  return true;
714
722
  } else {
@@ -0,0 +1,22 @@
1
+ import { cleanup, render, screen } from '@testing-library/react';
2
+ import { afterEach, describe, expect, test } from 'vitest';
3
+
4
+ import { TextInputFormatted } from './TextInputFormatted';
5
+
6
+ describe('TextInputFormatted', () => {
7
+ afterEach(() => {
8
+ cleanup();
9
+ });
10
+
11
+ test('sets data-validationsummary attribute from validationSummary prop', () => {
12
+ render(<TextInputFormatted value={''} onChange={() => {}} validationSummary={'required,maxLength=10'} />);
13
+
14
+ expect(screen.getByRole('textbox').getAttribute('data-validationsummary')).toBe('required,maxLength=10');
15
+ });
16
+
17
+ test('omits data-validationsummary attribute when validationSummary prop is not set', () => {
18
+ render(<TextInputFormatted value={''} onChange={() => {}} />);
19
+
20
+ expect(screen.getByRole('textbox').hasAttribute('data-validationsummary')).toBe(false);
21
+ });
22
+ });
@@ -15,6 +15,7 @@ export interface LuiTextInputProps extends DetailedHTMLProps<InputHTMLAttributes
15
15
  error?: ReactElement | string | boolean | null;
16
16
  formatted?: string;
17
17
  allowTabToSave?: boolean;
18
+ validationSummary?: string;
18
19
  }
19
20
 
20
21
  export const TextInputFormatted = (props: LuiTextInputProps): ReactElement => {
@@ -32,13 +33,22 @@ export const TextInputFormatted = (props: LuiTextInputProps): ReactElement => {
32
33
  type={'text'}
33
34
  spellCheck={true}
34
35
  defaultValue={props.value}
35
- {...omit(props, ['error', 'value', 'helpText', 'formatted', 'className', 'allowTabToSave'])}
36
+ {...omit(props, [
37
+ 'error',
38
+ 'value',
39
+ 'helpText',
40
+ 'formatted',
41
+ 'className',
42
+ 'allowTabToSave',
43
+ 'validationSummary',
44
+ ])}
36
45
  className={'LuiTextInput-input'}
37
46
  onMouseEnter={(e) => {
38
47
  e.currentTarget.focus();
39
48
  props.onMouseEnter?.(e);
40
49
  }}
41
50
  data-allowtabtosave={props.allowTabToSave}
51
+ data-validationsummary={props.validationSummary}
42
52
  />
43
53
  <span className={'LuiTextInput-formatted'}>{props.formatted}</span>
44
54
  </span>
@@ -12,7 +12,7 @@ import {
12
12
  GridUpdatingContextProvider,
13
13
  primitiveToSelectOption,
14
14
  } from '../..';
15
- import { expect, userEvent, waitFor } from 'storybook/test';
15
+ import { expect, userEvent, fireEvent, waitFor } from 'storybook/test';
16
16
 
17
17
  import { waitForGridReady } from '../../utils/__tests__/storybookTestUtil';
18
18
 
@@ -132,23 +132,24 @@ export const _EditBoolean = GridPopoutEditBooleanTemplate.bind({});
132
132
  _EditBoolean.play = waitForGridReady;
133
133
 
134
134
  export const _EditBooleanRetainsFocusAfterToggle = GridPopoutEditBooleanTemplate.bind({});
135
- _EditBooleanRetainsFocusAfterToggle.play = async (context) => {
136
- await waitForGridReady(context);
137
- const { canvasElement } = context;
135
+ _EditBooleanRetainsFocusAfterToggle.play = async ({ canvasElement }) => {
136
+ await waitForGridReady({ canvasElement });
138
137
 
139
138
  // Use boldMultiSelect (no async delay) so the redraw happens immediately
140
139
  const getBoolInput = () =>
141
140
  canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"] input.ag-checkbox-input') as HTMLElement;
142
141
 
143
- await userEvent.click(canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"]') as HTMLElement);
144
- await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
142
+ const button = canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"]') as HTMLElement;
143
+ await expect(button).toBeInTheDocument();
144
+ await fireEvent.click(button);
145
+ await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 1500 });
145
146
 
146
147
  // Space toggles the checkbox, which triggers redrawRows — ag-grid moves focus from the
147
148
  // input to the cell div (role="gridcell"). The shared interval should detect this and restore focus.
148
- await userEvent.keyboard(' ');
149
+ await fireEvent.keyPress(getBoolInput(), { key: 'Space', code: 'KeySpace' });
149
150
 
150
151
  // Re-query the input each retry since redraw replaces the DOM node
151
- await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
152
+ await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 1500 });
152
153
  };
153
154
 
154
155
  const selectRow = async (canvasElement: HTMLElement, rowId: string) => {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  import { GridBaseRow } from '../components/types';
4
- import { TextInputValidator, TextInputValidatorProps } from './textValidator';
4
+ import { textInputValidatorPropsToSummary, TextInputValidator, TextInputValidatorProps } from './textValidator';
5
5
 
6
6
  describe('TextInputValidator', () => {
7
7
  test('number format', () => {
@@ -115,3 +115,47 @@ describe('TextInputValidator', () => {
115
115
  expect(TextInputValidator({ maxBytes: 2 }, 'a–', { id: 0 }, {})).toBe('Must be no longer than 2 bytes');
116
116
  });
117
117
  });
118
+
119
+ describe('textInputValidatorPropsToSummary', () => {
120
+ test('empty props gives empty summary', () => {
121
+ expect(textInputValidatorPropsToSummary({})).toBe('');
122
+ });
123
+
124
+ test('required', () => {
125
+ expect(textInputValidatorPropsToSummary({ required: true })).toBe('required');
126
+ expect(textInputValidatorPropsToSummary({ required: false })).toBe('');
127
+ });
128
+
129
+ test('maxLength and maxBytes', () => {
130
+ expect(textInputValidatorPropsToSummary({ maxLength: 10 })).toBe('maxLength=10');
131
+ expect(textInputValidatorPropsToSummary({ maxBytes: 20 })).toBe('maxBytes=20');
132
+ });
133
+
134
+ test('invalid callback', () => {
135
+ expect(textInputValidatorPropsToSummary({ invalid: () => null })).toBe('invalid');
136
+ });
137
+
138
+ test('numberFormat fields', () => {
139
+ expect(
140
+ textInputValidatorPropsToSummary({
141
+ numberFormat: { precision: 3, scale: 2, gtMin: 0, geMin: 1, ltMax: 100, leMax: 99, notZero: true },
142
+ }),
143
+ ).toBe('precision=3,scale=2,gtMin=0,geMin=1,ltMax=100,leMax=99,notZero');
144
+ });
145
+
146
+ test('numberFormat with no set fields contributes nothing', () => {
147
+ expect(textInputValidatorPropsToSummary({ numberFormat: {} })).toBe('');
148
+ });
149
+
150
+ test('combines multiple props in order', () => {
151
+ expect(
152
+ textInputValidatorPropsToSummary({
153
+ required: true,
154
+ maxLength: 10,
155
+ maxBytes: 20,
156
+ invalid: () => null,
157
+ numberFormat: { scale: 2 },
158
+ }),
159
+ ).toBe('required,maxLength=10,maxBytes=20,invalid,scale=2');
160
+ });
161
+ });
@@ -81,3 +81,24 @@ export const TextInputValidator = <TData extends GridBaseRow>(
81
81
  }
82
82
  return null;
83
83
  };
84
+
85
+ export const textInputValidatorPropsToSummary = <TData extends GridBaseRow>(
86
+ props: TextInputValidatorProps<TData>,
87
+ ): string => {
88
+ const parts: string[] = [];
89
+ if (props.required) parts.push('required');
90
+ if (props.maxLength != null) parts.push(`maxLength=${props.maxLength}`);
91
+ if (props.maxBytes != null) parts.push(`maxBytes=${props.maxBytes}`);
92
+ if (props.invalid) parts.push('invalid');
93
+ const nf = props.numberFormat;
94
+ if (nf) {
95
+ if (nf.precision != null) parts.push(`precision=${nf.precision}`);
96
+ if (nf.scale != null) parts.push(`scale=${nf.scale}`);
97
+ if (nf.gtMin != null) parts.push(`gtMin=${nf.gtMin}`);
98
+ if (nf.geMin != null) parts.push(`geMin=${nf.geMin}`);
99
+ if (nf.ltMax != null) parts.push(`ltMax=${nf.ltMax}`);
100
+ if (nf.leMax != null) parts.push(`leMax=${nf.leMax}`);
101
+ if (nf.notZero) parts.push('notZero');
102
+ }
103
+ return parts.join(',');
104
+ };