@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/README.md +1 -0
- package/dist/index.css +10 -0
- package/dist/src/components/gridForm/GridFormMultiSelectGrid.d.ts +24 -0
- package/dist/src/components/gridForm/index.d.ts +1 -0
- package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelectGrid.d.ts +5 -0
- package/dist/src/components/gridPopoverEdit/index.d.ts +1 -0
- package/dist/src/react-menu3/contexts/HoverItemContext.d.ts +1 -1
- package/dist/src/react-menu3/hooks/useItems.d.ts +2 -2
- package/dist/src/react-menu3/utils/withHovering.d.ts +1 -1
- package/dist/step-ag-grid.esm.js +2381 -2903
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Grid.tsx +17 -6
- package/src/components/gridForm/GridFormMultiSelectGrid.tsx +167 -0
- package/src/components/gridForm/index.ts +1 -0
- package/src/components/gridPopoverEdit/GridPopoutEditMultiSelectGrid.ts +17 -0
- package/src/components/gridPopoverEdit/index.ts +1 -0
- package/src/react-menu3/components/MenuList.tsx +39 -0
- package/src/react-menu3/contexts/HoverItemContext.ts +6 -1
- package/src/react-menu3/hooks/useItems.ts +20 -8
- package/src/react-menu3/utils/withHovering.tsx +1 -1
- package/src/stories/grid/GridPopoverEditMultiSelectGrid.stories.tsx +109 -0
- package/src/stories/grid/gridFormInteraction/GridFormMultiSelectGridInteraction.stories.tsx +126 -0
- package/src/stories/react-menu/ReactMenu.stories.tsx +2 -0
- package/src/styles/GridFormMultiSelectGrid.scss +9 -0
- 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.
|
|
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.
|
|
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",
|
package/src/components/Grid.tsx
CHANGED
|
@@ -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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
});
|
|
@@ -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
|
|
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<
|
|
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
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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();
|