@linzjs/step-ag-grid 14.7.1 → 14.9.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 (27) hide show
  1. package/README.md +1 -0
  2. package/dist/index.css +10 -0
  3. package/dist/src/components/Grid.d.ts +1 -0
  4. package/dist/src/components/gridForm/GridFormMultiSelectGrid.d.ts +24 -0
  5. package/dist/src/components/gridForm/index.d.ts +1 -0
  6. package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelectGrid.d.ts +5 -0
  7. package/dist/src/components/gridPopoverEdit/index.d.ts +1 -0
  8. package/dist/src/react-menu3/contexts/HoverItemContext.d.ts +1 -1
  9. package/dist/src/react-menu3/hooks/useItems.d.ts +2 -2
  10. package/dist/src/react-menu3/utils/withHovering.d.ts +1 -1
  11. package/dist/step-ag-grid.esm.js +2376 -2904
  12. package/dist/step-ag-grid.esm.js.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/Grid.tsx +10 -6
  15. package/src/components/gridForm/GridFormMultiSelectGrid.tsx +172 -0
  16. package/src/components/gridForm/index.ts +1 -0
  17. package/src/components/gridPopoverEdit/GridPopoutEditMultiSelectGrid.ts +17 -0
  18. package/src/components/gridPopoverEdit/index.ts +1 -0
  19. package/src/react-menu3/components/MenuList.tsx +39 -0
  20. package/src/react-menu3/contexts/HoverItemContext.ts +6 -1
  21. package/src/react-menu3/hooks/useItems.ts +20 -8
  22. package/src/react-menu3/utils/withHovering.tsx +1 -1
  23. package/src/stories/grid/GridPopoverEditMultiSelectGrid.stories.tsx +109 -0
  24. package/src/stories/grid/gridFormInteraction/GridFormMultiSelectGridInteraction.stories.tsx +126 -0
  25. package/src/stories/react-menu/ReactMenu.stories.tsx +2 -0
  26. package/src/styles/GridFormMultiSelectGrid.scss +9 -0
  27. package/src/styles/index.scss +1 -0
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.1",
5
+ "version": "14.9.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";
@@ -37,6 +37,7 @@ export interface GridProps {
37
37
  noRowsOverlayText?: string;
38
38
  postSortRows?: GridOptions["postSortRows"];
39
39
  animateRows?: boolean;
40
+ rowHeight?: number;
40
41
  rowClassRules?: GridOptions["rowClassRules"];
41
42
  rowSelection?: "single" | "multiple";
42
43
  autoSelectFirstRow?: boolean;
@@ -500,11 +501,13 @@ export const Grid = ({
500
501
  if (!isEmpty(colIdsEdited.current)) {
501
502
  const skipHeader = sizeColumns === "auto-skip-headers";
502
503
  if (sizeColumns === "auto" || skipHeader) {
503
- autoSizeColumns({
504
- skipHeader,
505
- userSizedColIds: userSizedColIds.current,
506
- colIds: colIdsEdited.current,
507
- });
504
+ defer(() =>
505
+ autoSizeColumns({
506
+ skipHeader,
507
+ userSizedColIds: userSizedColIds.current,
508
+ colIds: colIdsEdited.current,
509
+ }),
510
+ );
508
511
  }
509
512
  colIdsEdited.current.clear();
510
513
  }
@@ -562,6 +565,7 @@ export const Grid = ({
562
565
  >
563
566
  <div style={{ flex: 1 }} ref={gridDivRef}>
564
567
  <AgGridReact
568
+ rowHeight={params.rowHeight}
565
569
  animateRows={params.animateRows}
566
570
  rowClassRules={params.rowClassRules}
567
571
  getRowId={(params) => `${params.data.id}`}
@@ -0,0 +1,172 @@
1
+ import { isEqual } from "lodash-es";
2
+ import { ReactElement, useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ import { LuiCheckboxInput } from "@linzjs/lui";
5
+
6
+ import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
7
+ import { MenuItem } from "../../react-menu3";
8
+ import { ClickEvent } from "../../react-menu3/types";
9
+ import { GridBaseRow } from "../Grid";
10
+ import { CellEditorCommon } from "../GridCell";
11
+ import { GridIcon } from "../GridIcon";
12
+ import { useGridPopoverHook } from "../GridPopoverHook";
13
+
14
+ export interface MultiSelectGridOption {
15
+ value: any;
16
+ label?: string | ReactElement;
17
+ checked?: boolean | "partial";
18
+ canSelectPartial?: boolean;
19
+ warning?: string | undefined;
20
+ }
21
+
22
+ export interface GridFormMultiSelectGridSaveProps<RowType extends GridBaseRow> {
23
+ selectedRows: RowType[];
24
+ addValues: any[];
25
+ removeValues: any[];
26
+ }
27
+
28
+ export interface GridFormMultiSelectGridProps<RowType extends GridBaseRow> extends CellEditorCommon {
29
+ className?: string | undefined;
30
+ noOptionsMessage?: string;
31
+ onSave?: (props: GridFormMultiSelectGridSaveProps<RowType>) => Promise<boolean>;
32
+ options:
33
+ | MultiSelectGridOption[]
34
+ | ((selectedRows: RowType[]) => Promise<MultiSelectGridOption[]> | MultiSelectGridOption[]);
35
+ invalid?: (props: GridFormMultiSelectGridSaveProps<RowType>) => boolean;
36
+ maxRowCount?: number;
37
+ }
38
+
39
+ export const GridFormMultiSelectGrid = <RowType extends GridBaseRow>(
40
+ props: GridFormMultiSelectGridProps<RowType>,
41
+ ): ReactElement => {
42
+ const { selectedRows } = useGridPopoverContext<RowType>();
43
+ const optionsInitialising = useRef(false);
44
+
45
+ const [initialValues, setInitialValues] = useState<MultiSelectGridOption[]>();
46
+ const [options, setOptions] = useState<MultiSelectGridOption[]>();
47
+
48
+ const genSave = useCallback(
49
+ (selectedRows: RowType[]): GridFormMultiSelectGridSaveProps<RowType> => {
50
+ if (!options) return { selectedRows, addValues: [], removeValues: [] };
51
+
52
+ const preChecked = (initialValues ?? []).filter((o) => o.checked).map((o) => o.value);
53
+ const postNotChecked = options.filter((o) => o.checked === false).map((o) => o.value);
54
+ const removeValues = preChecked.filter((p) => postNotChecked.some((c) => c === p));
55
+
56
+ const preNotChecked = (initialValues ?? []).filter((o) => o.checked !== true).map((o) => o.value);
57
+ const postChecked = options.filter((o) => o.checked === true).map((o) => o.value);
58
+ const addValues = preNotChecked.filter((p) => postChecked.some((c) => c === p));
59
+
60
+ return { selectedRows, addValues, removeValues };
61
+ },
62
+ [initialValues, options],
63
+ );
64
+
65
+ const save = useCallback(
66
+ async (selectedRows: RowType[]): Promise<boolean> => {
67
+ if (!options || !props.onSave) return true;
68
+ // Any changes to save?
69
+ if (
70
+ isEqual(
71
+ initialValues?.map((o) => o.checked),
72
+ options.map((o) => o.checked),
73
+ )
74
+ ) {
75
+ return true;
76
+ }
77
+ if (!props.onSave) return true;
78
+ return await props.onSave(genSave(selectedRows));
79
+ },
80
+ [genSave, initialValues, options, props],
81
+ );
82
+
83
+ const invalid = useCallback(() => {
84
+ if (!options) return true;
85
+ return props.invalid && props.invalid(genSave(selectedRows));
86
+ }, [genSave, options, props, selectedRows]);
87
+
88
+ const { popoverWrapper } = useGridPopoverHook({
89
+ className: props.className,
90
+ save,
91
+ invalid,
92
+ });
93
+
94
+ // Load up options list if it's async function
95
+ useEffect(() => {
96
+ if (options || optionsInitialising.current) return;
97
+ optionsInitialising.current = true;
98
+
99
+ (async () => {
100
+ const optionsList = typeof props.options === "function" ? await props.options(selectedRows) : props.options;
101
+ setInitialValues(optionsList.map((o) => ({ ...o, label: "" })));
102
+ setOptions(optionsList);
103
+ optionsInitialising.current = false;
104
+ })();
105
+ }, [options, props, selectedRows]);
106
+
107
+ const toggleValue = useCallback(
108
+ (item: MultiSelectGridOption) => {
109
+ if (options) {
110
+ item.checked = item.checked === true && item.canSelectPartial ? "partial" : !item.checked;
111
+ setOptions([...options]);
112
+ }
113
+ },
114
+ [options, setOptions],
115
+ );
116
+
117
+ return popoverWrapper(
118
+ <>
119
+ <div className={"Grid-popoverContainer"}>
120
+ <div
121
+ style={{
122
+ display: "grid",
123
+ gridAutoFlow: "column",
124
+ gridTemplateRows: `repeat(${props.maxRowCount ?? 10}, auto)`,
125
+ maxWidth: "calc(min(100vw,500px))",
126
+ overflowX: "auto",
127
+ }}
128
+ >
129
+ {options &&
130
+ options.map((o) => (
131
+ <>
132
+ <MenuItem
133
+ key={JSON.stringify(o.value)}
134
+ onClick={(e: ClickEvent) => {
135
+ // Global react-menu MenuItem handler handles tabs
136
+ if (e.key !== "Tab" && e.key !== "Enter") {
137
+ e.keepOpen = true;
138
+ toggleValue(o);
139
+ }
140
+ }}
141
+ >
142
+ <LuiCheckboxInput
143
+ isChecked={!!o.checked ?? false}
144
+ isIndeterminate={o.checked === "partial"}
145
+ value={`${o.value}`}
146
+ label={
147
+ <>
148
+ {o.warning && <GridIcon icon={"ic_warning_outline"} title={o.warning} />}
149
+ <span className={"GridMultiSelectGrid-Label"}>
150
+ {o.label ?? (o.value == null ? `<${o.value}>` : `${o.value}`)}
151
+ </span>
152
+ </>
153
+ }
154
+ inputProps={{
155
+ onClick: (e) => {
156
+ // Click is handled by MenuItem onClick
157
+ e.preventDefault();
158
+ e.stopPropagation();
159
+ },
160
+ }}
161
+ onChange={() => {
162
+ /*Do nothing, change handled by menuItem*/
163
+ }}
164
+ />
165
+ </MenuItem>
166
+ </>
167
+ ))}
168
+ </div>
169
+ </div>
170
+ </>,
171
+ );
172
+ };
@@ -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: <div>Five</div>, 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
+ }