@linzjs/step-ag-grid 14.7.0 → 14.8.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": "14.7.0",
5
+ "version": "14.8.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -108,7 +108,7 @@
108
108
  "@types/react": "^17.0.60",
109
109
  "@types/react-dom": "^17.0.20",
110
110
  "@types/uuid": "^9.0.1",
111
- "@typescript-eslint/parser": "^5.59.8",
111
+ "@typescript-eslint/parser": "^5.60.0",
112
112
  "babel-jest": "^29.5.0",
113
113
  "babel-preset-react-app": "^10.0.1",
114
114
  "chromatic": "^6.18.0",
@@ -4,7 +4,7 @@ import { GridOptions } from "ag-grid-community/dist/lib/entities/gridOptions";
4
4
  import { CellEvent, GridReadyEvent, SelectionChangedEvent } from "ag-grid-community/dist/lib/events";
5
5
  import { AgGridReact } from "ag-grid-react";
6
6
  import clsx from "clsx";
7
- import { difference, isEmpty, last, omit, xorBy } from "lodash-es";
7
+ import { defer, difference, isEmpty, last, omit, xorBy } from "lodash-es";
8
8
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
9
9
 
10
10
  import { GridContext } from "../contexts/GridContext";
@@ -121,6 +121,15 @@ export const Grid = ({
121
121
  needsAutoSize.current = true;
122
122
  return;
123
123
  }
124
+
125
+ const headerCellCount = gridDivRef.current?.getElementsByClassName("ag-header-cell-label")?.length;
126
+ if (headerCellCount != params.columnDefs.length) {
127
+ // Don't resize grids until all the columns are visible
128
+ // as `autoSizeColumns` will fail silently in this case
129
+ needsAutoSize.current = true;
130
+ return;
131
+ }
132
+
124
133
  const skipHeader = sizeColumns === "auto-skip-headers" && !isEmpty(params.rowData);
125
134
  if (sizeColumns === "auto" || skipHeader) {
126
135
  const result = autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current });
@@ -491,11 +500,13 @@ export const Grid = ({
491
500
  if (!isEmpty(colIdsEdited.current)) {
492
501
  const skipHeader = sizeColumns === "auto-skip-headers";
493
502
  if (sizeColumns === "auto" || skipHeader) {
494
- autoSizeColumns({
495
- skipHeader,
496
- userSizedColIds: userSizedColIds.current,
497
- colIds: colIdsEdited.current,
498
- });
503
+ defer(() =>
504
+ autoSizeColumns({
505
+ skipHeader,
506
+ userSizedColIds: userSizedColIds.current,
507
+ colIds: colIdsEdited.current,
508
+ }),
509
+ );
499
510
  }
500
511
  colIdsEdited.current.clear();
501
512
  }
@@ -0,0 +1,167 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ import { LuiCheckboxInput } from "@linzjs/lui";
4
+
5
+ import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
6
+ import { MenuItem } from "../../react-menu3";
7
+ import { ClickEvent } from "../../react-menu3/types";
8
+ import { GridBaseRow } from "../Grid";
9
+ import { CellEditorCommon } from "../GridCell";
10
+ import { GridIcon } from "../GridIcon";
11
+ import { useGridPopoverHook } from "../GridPopoverHook";
12
+
13
+ export interface MultiSelectGridOption {
14
+ value: any;
15
+ label?: string;
16
+ checked?: boolean | "partial";
17
+ canSelectPartial?: boolean;
18
+ warning?: string | undefined;
19
+ }
20
+
21
+ export interface GridFormMultiSelectGridSaveProps<RowType extends GridBaseRow> {
22
+ selectedRows: RowType[];
23
+ addValues: any[];
24
+ removeValues: any[];
25
+ }
26
+
27
+ export interface GridFormMultiSelectGridProps<RowType extends GridBaseRow> extends CellEditorCommon {
28
+ className?: string | undefined;
29
+ noOptionsMessage?: string;
30
+ onSave?: (props: GridFormMultiSelectGridSaveProps<RowType>) => Promise<boolean>;
31
+ options:
32
+ | MultiSelectGridOption[]
33
+ | ((selectedRows: RowType[]) => Promise<MultiSelectGridOption[]> | MultiSelectGridOption[]);
34
+ invalid?: (props: GridFormMultiSelectGridSaveProps<RowType>) => boolean;
35
+ maxRowCount?: number;
36
+ }
37
+
38
+ export const GridFormMultiSelectGrid = <RowType extends GridBaseRow>(
39
+ props: GridFormMultiSelectGridProps<RowType>,
40
+ ): JSX.Element => {
41
+ const { selectedRows } = useGridPopoverContext<RowType>();
42
+ const optionsInitialising = useRef(false);
43
+
44
+ const [initialValues, setInitialValues] = useState<MultiSelectGridOption[]>();
45
+ const [options, setOptions] = useState<MultiSelectGridOption[]>();
46
+
47
+ const genSave = useCallback(
48
+ (selectedRows: RowType[]): GridFormMultiSelectGridSaveProps<RowType> => {
49
+ if (!options) return { selectedRows, addValues: [], removeValues: [] };
50
+
51
+ const preChecked = (initialValues ?? []).filter((o) => o.checked).map((o) => o.value);
52
+ const postNotChecked = options.filter((o) => o.checked === false).map((o) => o.value);
53
+ const removeValues = preChecked.filter((p) => postNotChecked.some((c) => c === p));
54
+
55
+ const preNotChecked = (initialValues ?? []).filter((o) => o.checked !== true).map((o) => o.value);
56
+ const postChecked = options.filter((o) => o.checked === true).map((o) => o.value);
57
+ const addValues = preNotChecked.filter((p) => postChecked.some((c) => c === p));
58
+
59
+ return { selectedRows, addValues, removeValues };
60
+ },
61
+ [initialValues, options],
62
+ );
63
+
64
+ const save = useCallback(
65
+ async (selectedRows: RowType[]): Promise<boolean> => {
66
+ if (!options || !props.onSave) return true;
67
+ // Any changes to save?
68
+ if (JSON.stringify(initialValues) === JSON.stringify(options)) return true;
69
+ if (!props.onSave) return true;
70
+ return await props.onSave(genSave(selectedRows));
71
+ },
72
+ [genSave, initialValues, options, props],
73
+ );
74
+
75
+ const invalid = useCallback(() => {
76
+ if (!options) return true;
77
+ return props.invalid && props.invalid(genSave(selectedRows));
78
+ }, [genSave, options, props, selectedRows]);
79
+
80
+ const { popoverWrapper } = useGridPopoverHook({
81
+ className: props.className,
82
+ save,
83
+ invalid,
84
+ });
85
+
86
+ // Load up options list if it's async function
87
+ useEffect(() => {
88
+ if (options || optionsInitialising.current) return;
89
+ optionsInitialising.current = true;
90
+
91
+ (async () => {
92
+ const optionsList = typeof props.options === "function" ? await props.options(selectedRows) : props.options;
93
+ setInitialValues(JSON.parse(JSON.stringify(optionsList)));
94
+ setOptions(optionsList);
95
+ optionsInitialising.current = false;
96
+ })();
97
+ }, [options, props, selectedRows]);
98
+
99
+ const toggleValue = useCallback(
100
+ (item: MultiSelectGridOption) => {
101
+ if (options) {
102
+ item.checked = item.checked === true && item.canSelectPartial ? "partial" : !item.checked;
103
+ setOptions([...options]);
104
+ }
105
+ },
106
+ [options, setOptions],
107
+ );
108
+
109
+ return popoverWrapper(
110
+ <>
111
+ <div className={"Grid-popoverContainer"}>
112
+ <div
113
+ style={{
114
+ display: "grid",
115
+ gridAutoFlow: "column",
116
+ gridTemplateRows: `repeat(${props.maxRowCount ?? 10}, auto)`,
117
+ maxWidth: "calc(min(100vw,500px))",
118
+ overflowX: "auto",
119
+ }}
120
+ >
121
+ {options &&
122
+ options.map((o) => (
123
+ <>
124
+ <MenuItem
125
+ key={o.label}
126
+ onClick={(e: ClickEvent) => {
127
+ // Global react-menu MenuItem handler handles tabs
128
+ if (e.key !== "Tab" && e.key !== "Enter") {
129
+ e.keepOpen = true;
130
+ toggleValue(o);
131
+ }
132
+ }}
133
+ >
134
+ <LuiCheckboxInput
135
+ isChecked={!!o.checked ?? false}
136
+ isIndeterminate={o.checked === "partial"}
137
+ value={`${o.value}`}
138
+ label={
139
+ <>
140
+ {o.warning && <GridIcon icon={"ic_warning_outline"} title={o.warning} />}
141
+ <span
142
+ className={"GridMultiSelectGrid-Label"}
143
+ style={{ fontWeight: o.label?.endsWith("0") ? "bold" : "regular" }}
144
+ >
145
+ {o.label ?? (o.value == null ? `<${o.value}>` : `${o.value}`)}
146
+ </span>
147
+ </>
148
+ }
149
+ inputProps={{
150
+ onClick: (e) => {
151
+ // Click is handled by MenuItem onClick
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+ },
155
+ }}
156
+ onChange={() => {
157
+ /*Do nothing, change handled by menuItem*/
158
+ }}
159
+ />
160
+ </MenuItem>
161
+ </>
162
+ ))}
163
+ </div>
164
+ </div>
165
+ </>,
166
+ );
167
+ };
@@ -2,6 +2,7 @@ export * from "./GridFormDropDown";
2
2
  export * from "./GridFormEditBearing";
3
3
  export * from "./GridFormMessage";
4
4
  export * from "./GridFormMultiSelect";
5
+ export * from "./GridFormMultiSelectGrid";
5
6
  export * from "./GridFormPopoverMenu";
6
7
  export * from "./GridFormSubComponentTextArea";
7
8
  export * from "./GridFormSubComponentTextInput";
@@ -0,0 +1,17 @@
1
+ import { GridBaseRow } from "../Grid";
2
+ import { ColDefT, GenericCellEditorProps, GridCell } from "../GridCell";
3
+ import { GridFormMultiSelectGrid, GridFormMultiSelectGridProps } from "../gridForm/GridFormMultiSelectGrid";
4
+ import { GenericCellColDef } from "../gridRender/GridRenderGenericCell";
5
+
6
+ export const GridPopoutEditMultiSelectGrid = <RowType extends GridBaseRow>(
7
+ colDef: GenericCellColDef<RowType>,
8
+ props: GenericCellEditorProps<GridFormMultiSelectGridProps<RowType>>,
9
+ ): ColDefT<RowType> =>
10
+ GridCell(colDef, {
11
+ editor: GridFormMultiSelectGrid,
12
+ ...props,
13
+ editorParams: {
14
+ className: "GridMultiSelect-containerMedium",
15
+ ...(props.editorParams as GridFormMultiSelectGridProps<RowType>),
16
+ },
17
+ });
@@ -1,4 +1,5 @@
1
1
  export * from "./GridPopoutEditMultiSelect";
2
+ export * from "./GridPopoutEditMultiSelectGrid";
2
3
  export * from "./GridPopoverMenu";
3
4
  export * from "./GridPopoverEditBearing";
4
5
  export * from "./GridPopoverEditBearing";
@@ -2,6 +2,7 @@ import { debounce } from "lodash-es";
2
2
  import { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react";
3
3
  import { flushSync } from "react-dom";
4
4
 
5
+ import { findParentWithClass } from "../../utils/util";
5
6
  import { HoverItemContext } from "../contexts/HoverItemContext";
6
7
  import { MenuListContext } from "../contexts/MenuListContext";
7
8
  import { MenuListItemContext } from "../contexts/MenuListItemContext";
@@ -88,6 +89,32 @@ export const MenuList = ({
88
89
  const closeTransition = getTransition(transition, "close");
89
90
  const scrollNodes = scrollNodesRef.current;
90
91
 
92
+ /**
93
+ * Given the menu-item we are on, find the closest or furthest next menu-item horizontally and focus it.
94
+ * @return true if element found else false.
95
+ */
96
+ const focusSideways = (menuItemEl: HTMLElement | null, direction: 1 | -1): boolean => {
97
+ if (!menuItemEl) return false;
98
+
99
+ let [best, worst]: { i: number; d: number }[] = [];
100
+ findParentWithClass("szh-menu", menuItemEl)
101
+ ?.querySelectorAll<HTMLLIElement>(".szh-menu__item")
102
+ ?.forEach((item, i) => {
103
+ // Must be at same height as currently focused menu-item
104
+ if (item.offsetTop === menuItemEl.offsetTop) {
105
+ // Find the least positive distance and most negative distance (for wrap around)
106
+ const d = (item.offsetLeft - menuItemEl.offsetLeft) * direction;
107
+ if (d > 0 && (!best || d < best.d)) best = { i, d };
108
+ if (d < 0 && (!worst || d < worst.d)) worst = { i, d };
109
+ }
110
+ });
111
+ const r = best ?? worst;
112
+ if (!r) return false;
113
+
114
+ dispatch(HoverActionTypes.SET_INDEX, undefined, r["i"]);
115
+ return true;
116
+ };
117
+
91
118
  const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
92
119
  const elementTarget = e.target instanceof HTMLElement ? (e.target as HTMLElement) : null;
93
120
  const isTextInputTarget =
@@ -115,6 +142,18 @@ export const MenuList = ({
115
142
  dispatch(HoverActionTypes.INCREASE, hoverItem, 0);
116
143
  break;
117
144
 
145
+ case Keys.LEFT:
146
+ if (!focusSideways(elementTarget, -1)) {
147
+ return; // Unhandled
148
+ }
149
+ break;
150
+
151
+ case Keys.RIGHT:
152
+ if (!focusSideways(elementTarget, 1)) {
153
+ return; // Unhandled
154
+ }
155
+ break;
156
+
118
157
  // prevent browser from scrolling the page when SPACE is pressed
119
158
  case Keys.SPACE:
120
159
  // Don't preventDefault on children of FocusableItem
@@ -1,3 +1,8 @@
1
1
  import { createContext } from "react";
2
2
 
3
- export const HoverItemContext = createContext(undefined);
3
+ export const HoverItemContext = createContext<
4
+ | HTMLDivElement
5
+ | HTMLLIElement
6
+ | ((prevItem: HTMLDivElement | HTMLLIElement) => HTMLDivElement | HTMLLIElement | undefined)
7
+ | undefined
8
+ >(undefined);
@@ -1,19 +1,25 @@
1
+ import { defer } from "lodash-es";
1
2
  import { MutableRefObject, useCallback, useRef, useState } from "react";
2
3
 
3
4
  import { FocusPosition } from "../types";
4
5
  import { HoverActionTypes, focusFirstInput, indexOfNode } from "../utils";
5
6
 
6
7
  export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObject<any> | undefined) => {
7
- const [hoverItem, setHoverItem] = useState<any>();
8
+ const [hoverItem, setHoverItem] = useState<
9
+ | HTMLDivElement
10
+ | HTMLLIElement
11
+ | ((prevItem: HTMLDivElement | HTMLLIElement) => HTMLDivElement | HTMLLIElement | undefined)
12
+ | undefined
13
+ >();
8
14
  const stateRef = useRef({
9
- items: [] as any[],
15
+ items: [] as (HTMLDivElement | HTMLLIElement)[],
10
16
  hoverIndex: -1,
11
17
  sorted: false,
12
18
  });
13
19
  const mutableState = stateRef.current;
14
20
 
15
21
  const updateItems = useCallback(
16
- (item?: any, isMounted?: boolean) => {
22
+ (item?: HTMLDivElement | HTMLLIElement, isMounted?: boolean) => {
17
23
  const { items } = mutableState;
18
24
  if (!item) {
19
25
  mutableState.items = [];
@@ -45,8 +51,11 @@ export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObj
45
51
  mutableState.sorted = true;
46
52
  };
47
53
 
48
- let index = -1,
49
- newItem = undefined;
54
+ let index = -1;
55
+ let newItem: HTMLDivElement | HTMLLIElement | undefined = undefined;
56
+ let newItemFn:
57
+ | ((prevItem: HTMLDivElement | HTMLLIElement) => HTMLDivElement | HTMLLIElement | undefined)
58
+ | undefined = undefined;
50
59
  switch (actionType) {
51
60
  case HoverActionTypes.RESET:
52
61
  break;
@@ -56,7 +65,7 @@ export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObj
56
65
  break;
57
66
 
58
67
  case HoverActionTypes.UNSET:
59
- newItem = (prevItem: any) => (prevItem === item ? undefined : prevItem);
68
+ newItemFn = (prevItem: HTMLDivElement | HTMLLIElement) => (prevItem === item ? undefined : prevItem);
60
69
  break;
61
70
 
62
71
  case HoverActionTypes.FIRST:
@@ -76,6 +85,9 @@ export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObj
76
85
  sortItems();
77
86
  index = nextIndex;
78
87
  newItem = items[index];
88
+ defer(() =>
89
+ (newItem as HTMLElement).scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }),
90
+ );
79
91
  break;
80
92
 
81
93
  case HoverActionTypes.INCREASE:
@@ -104,8 +116,8 @@ export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObj
104
116
  throw new Error(`[React-Menu] Unknown hover action type: ${actionType}`);
105
117
  }
106
118
 
107
- if (!newItem) index = -1;
108
- setHoverItem(newItem);
119
+ if (!newItem && !newItemFn) index = -1;
120
+ setHoverItem(newItem ?? newItemFn);
109
121
  mutableState.hoverIndex = index;
110
122
  },
111
123
  [menuRef, mutableState],
@@ -5,7 +5,7 @@ import { HoverItemContext } from "../contexts/HoverItemContext";
5
5
  export interface withHoveringResultProps {
6
6
  isHovering?: boolean;
7
7
  externalRef?: MutableRefObject<any>;
8
- menuItemRef?: MutableRefObject<any>;
8
+ menuItemRef?: MutableRefObject<HTMLLIElement>;
9
9
  }
10
10
 
11
11
  export const withHovering = <T,>(name: string, WrappedComponent: (props: T) => JSX.Element) => {
@@ -0,0 +1,109 @@
1
+ import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/client/preview/types-6-3";
2
+ import { countBy, mergeWith, pull, range, union } from "lodash-es";
3
+ import { useMemo, useState } from "react";
4
+
5
+ import "@linzjs/lui/dist/fonts";
6
+ import "@linzjs/lui/dist/scss/base.scss";
7
+
8
+ import { ColDefT, Grid, GridCell, GridContextProvider, GridProps, GridUpdatingContextProvider } from "../..";
9
+ import { MultiSelectGridOption } from "../../components/gridForm/GridFormMultiSelectGrid";
10
+ import { GridPopoutEditMultiSelectGrid } from "../../components/gridPopoverEdit/GridPopoutEditMultiSelectGrid";
11
+ import "../../styles/GridTheme.scss";
12
+ import "../../styles/index.scss";
13
+
14
+ export default {
15
+ title: "Components / Grids",
16
+ component: Grid,
17
+ args: {
18
+ quickFilterValue: "",
19
+ selectable: true,
20
+ },
21
+ decorators: [
22
+ (Story) => (
23
+ <div style={{ width: 1024, height: 400 }}>
24
+ <GridUpdatingContextProvider>
25
+ <GridContextProvider>
26
+ <Story />
27
+ </GridContextProvider>
28
+ </GridUpdatingContextProvider>
29
+ </div>
30
+ ),
31
+ ],
32
+ } as ComponentMeta<typeof Grid>;
33
+
34
+ interface ITestRow {
35
+ id: number;
36
+ position: number[] | null;
37
+ position2: string | null;
38
+ }
39
+
40
+ const GridEditMultiSelectGridTemplate: ComponentStory<typeof Grid> = (props: GridProps) => {
41
+ const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
42
+
43
+ const columnDefs: ColDefT<ITestRow>[] = useMemo(() => {
44
+ return [
45
+ GridCell({
46
+ field: "id",
47
+ headerName: "Id",
48
+ }),
49
+ GridPopoutEditMultiSelectGrid(
50
+ {
51
+ field: "position",
52
+ headerName: "Position",
53
+ valueFormatter: ({ value }) => {
54
+ if (value == null) return "";
55
+ return value.join(", ");
56
+ },
57
+ },
58
+ {
59
+ multiEdit: true,
60
+ editorParams: {
61
+ className: "GridMultiSelect-containerUnlimited",
62
+ options: (selectedRows) => {
63
+ const counts: Record<number, number> = mergeWith(
64
+ {},
65
+ ...selectedRows.map((row) => countBy(row.position)),
66
+ (a: number | undefined, b: number | undefined) => (a ?? 0) + (b ?? 0),
67
+ );
68
+ return range(50024, 50067).map((value): MultiSelectGridOption => {
69
+ const checked = counts[value] == selectedRows.length ? true : counts[value] > 0 ? "partial" : false;
70
+ return {
71
+ value: value,
72
+ label: `${value}`,
73
+ checked,
74
+ canSelectPartial: checked === "partial",
75
+ };
76
+ });
77
+ },
78
+ onSave: async ({ selectedRows, addValues, removeValues }) => {
79
+ selectedRows.forEach((row) => {
80
+ row.position = union(pull(row.position ?? [], ...removeValues), addValues).sort();
81
+ });
82
+
83
+ return true;
84
+ },
85
+ },
86
+ },
87
+ ),
88
+ ];
89
+ }, []);
90
+
91
+ const [rowData] = useState([
92
+ { id: 1000, position: [50024, 50025], position2: "lot1" },
93
+ { id: 1001, position: [50025, 50026], position2: "lot2" },
94
+ ] as ITestRow[]);
95
+
96
+ return (
97
+ <Grid
98
+ {...props}
99
+ animateRows={true}
100
+ externalSelectedItems={externalSelectedItems}
101
+ setExternalSelectedItems={setExternalSelectedItems}
102
+ columnDefs={columnDefs}
103
+ rowData={rowData}
104
+ domLayout={"autoHeight"}
105
+ />
106
+ );
107
+ };
108
+
109
+ export const EditMultiSelectGrid = GridEditMultiSelectGridTemplate.bind({});
@@ -0,0 +1,126 @@
1
+ import { expect, jest } from "@storybook/jest";
2
+ import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/client/preview/types-6-3";
3
+ import { userEvent, waitFor, within } from "@storybook/testing-library";
4
+ import { GridPopoverContext, GridPopoverContextType } from "contexts/GridPopoverContext";
5
+ import { useRef } from "react";
6
+
7
+ import "@linzjs/lui/dist/fonts";
8
+ import "@linzjs/lui/dist/scss/base.scss";
9
+
10
+ import {
11
+ GridContextProvider,
12
+ GridFormMultiSelectGrid,
13
+ GridFormMultiSelectGridProps,
14
+ GridFormMultiSelectGridSaveProps,
15
+ MultiSelectGridOption,
16
+ } from "../../..";
17
+
18
+ export default {
19
+ title: "GridForm / Interactions",
20
+ component: GridFormMultiSelectGrid,
21
+ args: {},
22
+ } as ComponentMeta<typeof GridFormMultiSelectGrid>;
23
+
24
+ const updateValue = jest
25
+ .fn<void, [saveFn: (selectedRows: any[]) => Promise<boolean>, _tabDirection: 1 | 0 | -1]>()
26
+ .mockImplementation((saveFn: (selectedRows: any[]) => Promise<boolean>, _tabDirection: 1 | 0 | -1) => saveFn([]));
27
+
28
+ const onSave = jest
29
+ .fn<Promise<boolean>, [GridFormMultiSelectGridSaveProps<any>]>()
30
+ .mockImplementation(async () => true);
31
+ const onSelectFilter = jest.fn();
32
+
33
+ let options: MultiSelectGridOption[] = [];
34
+ const Template: ComponentStory<typeof GridFormMultiSelectGrid> = (props) => {
35
+ options = [
36
+ { label: "Zero", value: 0 },
37
+ { label: "One", value: 1 },
38
+ { label: "Two", value: 2 },
39
+ { label: "Three", value: 3 },
40
+ { label: "Four", value: 4 },
41
+ { label: "Five", value: 5 },
42
+ ];
43
+ const config: GridFormMultiSelectGridProps<any> = {
44
+ onSave,
45
+ options,
46
+ maxRowCount: 3,
47
+ };
48
+ const anchorRef = useRef<HTMLHeadingElement>(null);
49
+
50
+ return (
51
+ <div className={"react-menu-inline-test"}>
52
+ <GridContextProvider>
53
+ <div>
54
+ <h6 ref={anchorRef}>Interaction test</h6>
55
+ <GridPopoverContext.Provider
56
+ value={
57
+ {
58
+ anchorRef: anchorRef,
59
+ updateValue,
60
+ data: { value: "" },
61
+ value: "",
62
+ field: "value",
63
+ selectedRows: [],
64
+ } as any as GridPopoverContextType<any>
65
+ }
66
+ >
67
+ <GridFormMultiSelectGrid {...props} {...config} />
68
+ </GridPopoverContext.Provider>
69
+ </div>
70
+ </GridContextProvider>
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export const GridFormMultiSelectGridInteractions_ = Template.bind({});
76
+ GridFormMultiSelectGridInteractions_.play = async ({ canvasElement }) => {
77
+ updateValue.mockClear();
78
+ onSave.mockClear();
79
+ onSelectFilter.mockClear();
80
+
81
+ const canvas = within(canvasElement);
82
+
83
+ const getOption = (name: RegExp | string) => canvas.findByRole("menuitem", { name });
84
+
85
+ // Check enabled menu handles click
86
+ const zeroMenuOption = await getOption(/Zero/);
87
+ expect(zeroMenuOption).toBeInTheDocument();
88
+
89
+ userEvent.click(zeroMenuOption);
90
+ userEvent.keyboard("{Tab}");
91
+ expect(updateValue).toHaveBeenCalled();
92
+ await waitFor(() => expect(onSave).toHaveBeenCalledWith({ selectedRows: [], addValues: [0], removeValues: [] }));
93
+
94
+ const check = (label: string) => {
95
+ const activeCell = canvasElement.ownerDocument.activeElement as HTMLElement | null;
96
+ expect(activeCell?.innerText).toBe(label);
97
+ };
98
+
99
+ // Test left/right arrow
100
+ userEvent.keyboard("{ArrowRight}");
101
+ check("Three");
102
+ userEvent.keyboard("{ArrowRight}");
103
+ check("Zero");
104
+ userEvent.keyboard("{ArrowLeft}");
105
+ check("Three");
106
+ userEvent.keyboard("{ArrowLeft}");
107
+ check("Zero");
108
+
109
+ // Test tab to save
110
+ updateValue.mockClear();
111
+ onSave.mockClear();
112
+ userEvent.tab();
113
+ expect(updateValue).toHaveBeenCalledWith(expect.anything(), 1); // 1 = Tab
114
+ expect(onSave).toHaveBeenCalledWith({
115
+ selectedRows: [],
116
+ addValues: [0],
117
+ removeValues: [],
118
+ });
119
+
120
+ // Test shift+tab to save
121
+ updateValue.mockClear();
122
+ onSave.mockClear();
123
+ userEvent.tab({ shift: true });
124
+ expect(updateValue).toHaveBeenCalledWith(expect.anything(), -1); // -1 = Shift + tab
125
+ expect(onSave).toHaveBeenCalled();
126
+ };
@@ -89,6 +89,8 @@ ReactMenuControlled.play = async ({ canvasElement }) => {
89
89
  // Test tab to select
90
90
  await openMenu();
91
91
  await keyboard("{arrowdown}");
92
+ // For some reason arrow down takes a little time to activate
93
+ await wait(10);
92
94
  await keyboard("{Tab}");
93
95
  expect(menuItemClickAction).toHaveBeenCalled();
94
96
  expect(newFileAction).toHaveBeenCalled();
@@ -0,0 +1,9 @@
1
+ .GridMultiSelect-noOptions {
2
+ justify-content: center;
3
+ }
4
+
5
+ .GridMultiSelectGrid-Label {
6
+ text-overflow: ellipsis;
7
+ white-space: nowrap;
8
+ overflow: hidden;
9
+ }
@@ -6,6 +6,7 @@
6
6
  @use "./GridFormDropDown";
7
7
  @use "./GridFormEditBearing";
8
8
  @use "./GridFormMultiSelect";
9
+ @use "./GridFormMultiSelectGrid";
9
10
  @use "./GridFormSubComponentTextInput";
10
11
  @use "./GridIcon";
11
12
  @use "./GridLoadableCell";