@linzjs/step-ag-grid 8.4.3 → 9.0.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 +4 -4
- package/dist/index.css +3 -0
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +5 -4
- package/dist/src/utils/textMatcher.d.ts +1 -1
- package/dist/src/utils/textMatcher.test.d.ts +1 -0
- package/dist/step-ag-grid.esm.js +464 -689
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +42 -42
- package/src/components/Grid.tsx +29 -27
- package/src/components/gridForm/GridFormDropDown.tsx +2 -2
- package/src/components/gridForm/GridFormMultiSelect.tsx +12 -6
- package/src/components/gridForm/GridFormSubComponentTextArea.tsx +0 -1
- package/src/components/gridForm/GridFormSubComponentTextInput.tsx +0 -1
- package/src/lui/TextAreaInput.tsx +1 -1
- package/src/lui/TextInputFormatted.tsx +1 -1
- package/src/react-menu3/components/ControlledMenu.tsx +19 -6
- package/src/stories/grid/GridKeyboardInteractions.stories.tsx +261 -0
- package/src/stories/grid/{GridPopoutBearing.stories.tsx → GridPopoverEditBearing.stories.tsx} +4 -4
- package/src/stories/grid/{GridPopoutEditDropDown.stories.tsx → GridPopoverEditDropDown.stories.tsx} +0 -0
- package/src/stories/grid/{GridPopoutEditMultiSelect.stories.tsx → GridPopoverEditMultiSelect.stories.tsx} +0 -0
- package/src/stories/grid/gridFormInteraction/GridFormDropDownInteraction.stories.tsx +10 -1
- package/src/stories/grid/gridFormInteraction/GridFormEditBearingCorrectionInteraction.stories.tsx +1 -1
- package/src/stories/grid/gridFormInteraction/GridFormEditBearingInteraction.stories.tsx +1 -1
- package/src/stories/grid/gridFormInteraction/GridFormMultiSelectInteraction.stories.tsx +153 -0
- package/src/stories/grid/gridFormInteraction/GridFormPopoverMenuInteraction.stories.tsx +1 -1
- package/src/stories/grid/gridFormInteraction/GridFormTextAreaInteraction.stories.tsx +1 -1
- package/src/stories/grid/gridFormInteraction/GridFormTextInputInteraction.stories.tsx +1 -1
- package/src/stories/react-menu/ReactMenu.stories.tsx +66 -3
- package/src/styles/Grid.scss +3 -0
- package/src/utils/testUtil.ts +1 -0
- package/src/utils/textMatcher.test.ts +38 -0
- package/src/utils/textMatcher.ts +9 -2
|
@@ -24,7 +24,7 @@ const Template: ComponentStory<typeof GridFormEditBearing> = (props) => {
|
|
|
24
24
|
return (
|
|
25
25
|
<div className={"react-menu-inline-test"}>
|
|
26
26
|
<GridContextProvider>
|
|
27
|
-
<h6 ref={anchorRef}>Test</h6>
|
|
27
|
+
<h6 ref={anchorRef}>Interaction Test</h6>
|
|
28
28
|
<GridPopoverContext.Provider
|
|
29
29
|
value={
|
|
30
30
|
{
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import "@linzjs/lui/dist/scss/base.scss";
|
|
2
|
+
import "@linzjs/lui/dist/fonts";
|
|
3
|
+
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/client/preview/types-6-3";
|
|
6
|
+
import {
|
|
7
|
+
GridFormMultiSelect,
|
|
8
|
+
GridFormMultiSelectProps,
|
|
9
|
+
GridFormMultiSelectSaveProps,
|
|
10
|
+
MultiSelectOption,
|
|
11
|
+
} from "../../../components/gridForm/GridFormMultiSelect";
|
|
12
|
+
import { GridContextProvider } from "../../../contexts/GridContextProvider";
|
|
13
|
+
import { GridPopoverContext, GridPopoverContextType } from "contexts/GridPopoverContext";
|
|
14
|
+
import { GridFormSubComponentTextInput } from "../../../components/gridForm/GridFormSubComponentTextInput";
|
|
15
|
+
import { userEvent, within } from "@storybook/testing-library";
|
|
16
|
+
import { expect, jest } from "@storybook/jest";
|
|
17
|
+
import { wait } from "../../../utils/util";
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
title: "GridForm / Interactions",
|
|
21
|
+
component: GridFormMultiSelect,
|
|
22
|
+
args: {},
|
|
23
|
+
} as ComponentMeta<typeof GridFormMultiSelect>;
|
|
24
|
+
|
|
25
|
+
const updateValue = jest
|
|
26
|
+
.fn<void, [saveFn: (selectedRows: any[]) => Promise<boolean>, _tabDirection: 1 | 0 | -1]>()
|
|
27
|
+
.mockImplementation((saveFn: (selectedRows: any[]) => Promise<boolean>, _tabDirection: 1 | 0 | -1) => saveFn([]));
|
|
28
|
+
|
|
29
|
+
const onSave = jest.fn<Promise<boolean>, [GridFormMultiSelectSaveProps<any>]>().mockImplementation(async () => true);
|
|
30
|
+
|
|
31
|
+
const options = Object.freeze([
|
|
32
|
+
{ label: "Zero", value: 0 },
|
|
33
|
+
{ label: "One", value: 1 },
|
|
34
|
+
{
|
|
35
|
+
label: "Sub component",
|
|
36
|
+
value: 2,
|
|
37
|
+
subComponent: () => (
|
|
38
|
+
<GridFormSubComponentTextInput placeholder={"Text input"} maxLength={5} required defaultValue={""} />
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
{ label: "Other", value: 3 },
|
|
42
|
+
]) as MultiSelectOption[];
|
|
43
|
+
|
|
44
|
+
const Template: ComponentStory<typeof GridFormMultiSelect> = (props) => {
|
|
45
|
+
const config: GridFormMultiSelectProps<any> = {
|
|
46
|
+
filtered: true,
|
|
47
|
+
onSave,
|
|
48
|
+
options,
|
|
49
|
+
};
|
|
50
|
+
const anchorRef = useRef<HTMLHeadingElement>(null);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={"react-menu-inline-test"}>
|
|
54
|
+
<GridContextProvider>
|
|
55
|
+
<div>
|
|
56
|
+
<h6 ref={anchorRef}>Interaction test</h6>
|
|
57
|
+
<GridPopoverContext.Provider
|
|
58
|
+
value={
|
|
59
|
+
{
|
|
60
|
+
anchorRef: anchorRef,
|
|
61
|
+
updateValue,
|
|
62
|
+
data: { value: "" },
|
|
63
|
+
value: "",
|
|
64
|
+
field: "value",
|
|
65
|
+
selectedRows: [],
|
|
66
|
+
} as any as GridPopoverContextType<any>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<GridFormMultiSelect {...props} {...config} />
|
|
70
|
+
</GridPopoverContext.Provider>
|
|
71
|
+
</div>
|
|
72
|
+
</GridContextProvider>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const GridFormMultiSelectInteractions_ = Template.bind({});
|
|
78
|
+
GridFormMultiSelectInteractions_.play = async ({ canvasElement }) => {
|
|
79
|
+
const canvas = within(canvasElement);
|
|
80
|
+
|
|
81
|
+
const getOption = (name: RegExp | string) => canvas.findByRole("menuitem", { name });
|
|
82
|
+
|
|
83
|
+
// Check enabled menu handles click
|
|
84
|
+
const zeroMenuOption = await getOption(/Zero/);
|
|
85
|
+
expect(zeroMenuOption).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
userEvent.click(zeroMenuOption);
|
|
88
|
+
userEvent.keyboard("{Tab}");
|
|
89
|
+
expect(updateValue).toHaveBeenCalled();
|
|
90
|
+
expect(onSave).toHaveBeenCalledWith({ selectedOptions: [options[0]], selectedRows: [] });
|
|
91
|
+
|
|
92
|
+
// Check sub menu works
|
|
93
|
+
const subTextInput = await getOption(/Sub component/);
|
|
94
|
+
expect(subTextInput).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
expect(canvas.queryByPlaceholderText("Text input")).not.toBeInTheDocument();
|
|
97
|
+
|
|
98
|
+
userEvent.click(subTextInput);
|
|
99
|
+
const textInput = await canvas.findByPlaceholderText("Text input");
|
|
100
|
+
expect(textInput).toBeInTheDocument();
|
|
101
|
+
expect(await canvas.findByText("Must not be empty")).toBeInTheDocument();
|
|
102
|
+
|
|
103
|
+
userEvent.click(textInput);
|
|
104
|
+
userEvent.type(textInput, "Hello");
|
|
105
|
+
expect(await canvas.findByText("Press enter or tab to save")).toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
// Test tab to save
|
|
108
|
+
updateValue.mockClear();
|
|
109
|
+
onSave.mockClear();
|
|
110
|
+
userEvent.tab();
|
|
111
|
+
expect(updateValue).toHaveBeenCalledWith(expect.anything(), 1); // 1 = Tab
|
|
112
|
+
expect(onSave).toHaveBeenCalledWith({
|
|
113
|
+
selectedRows: [],
|
|
114
|
+
selectedOptions: [
|
|
115
|
+
{ label: "Zero", value: 0, checked: true },
|
|
116
|
+
{ label: "Sub component", value: 2, checked: true, subValue: "Hello", subComponent: expect.anything() },
|
|
117
|
+
],
|
|
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
|
+
|
|
127
|
+
// Test escape to not save
|
|
128
|
+
updateValue.mockClear();
|
|
129
|
+
onSave.mockClear();
|
|
130
|
+
userEvent.type(textInput, "{Escape}");
|
|
131
|
+
expect(updateValue).not.toHaveBeenCalled();
|
|
132
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
133
|
+
|
|
134
|
+
// Test invalid value doesn't save
|
|
135
|
+
updateValue.mockClear();
|
|
136
|
+
onSave.mockClear();
|
|
137
|
+
userEvent.clear(textInput);
|
|
138
|
+
userEvent.type(textInput, "{Enter}");
|
|
139
|
+
expect(updateValue).not.toHaveBeenCalled();
|
|
140
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
141
|
+
|
|
142
|
+
// Test filter
|
|
143
|
+
const filterText = await canvas.findByPlaceholderText("Filter...");
|
|
144
|
+
userEvent.type(filterText, "o");
|
|
145
|
+
await wait(500);
|
|
146
|
+
expect(canvas.queryByText("One")).toBeInTheDocument();
|
|
147
|
+
expect(canvas.queryByText("Other")).toBeInTheDocument();
|
|
148
|
+
userEvent.type(filterText, "n");
|
|
149
|
+
expect(canvas.queryByText("One")).toBeInTheDocument();
|
|
150
|
+
expect(canvas.queryByText("Zero")).not.toBeInTheDocument();
|
|
151
|
+
expect(canvas.queryByText("Sub component")).not.toBeInTheDocument();
|
|
152
|
+
expect(canvas.queryByText("Other")).not.toBeInTheDocument();
|
|
153
|
+
};
|
|
@@ -42,7 +42,7 @@ const Template: ComponentStory<typeof GridFormPopoverMenu> = (props) => {
|
|
|
42
42
|
return (
|
|
43
43
|
<div className={"react-menu-inline-test"}>
|
|
44
44
|
<GridContextProvider>
|
|
45
|
-
<h6 ref={anchorRef}>Test</h6>
|
|
45
|
+
<h6 ref={anchorRef}>Interaction Test</h6>
|
|
46
46
|
<GridPopoverContext.Provider
|
|
47
47
|
value={
|
|
48
48
|
{
|
|
@@ -23,7 +23,7 @@ const Template: ComponentStory<typeof GridFormTextArea> = (props) => {
|
|
|
23
23
|
return (
|
|
24
24
|
<div className={"react-menu-inline-test"}>
|
|
25
25
|
<GridContextProvider>
|
|
26
|
-
<h6 ref={anchorRef}>Test</h6>
|
|
26
|
+
<h6 ref={anchorRef}>Interaction Test</h6>
|
|
27
27
|
<GridPopoverContext.Provider
|
|
28
28
|
value={
|
|
29
29
|
{
|
|
@@ -23,7 +23,7 @@ const Template: ComponentStory<typeof GridFormTextInput> = (props) => {
|
|
|
23
23
|
return (
|
|
24
24
|
<div className={"react-menu-inline-test"}>
|
|
25
25
|
<GridContextProvider>
|
|
26
|
-
<h6 ref={anchorRef}>Test</h6>
|
|
26
|
+
<h6 ref={anchorRef}>Interaction Test</h6>
|
|
27
27
|
<GridPopoverContext.Provider
|
|
28
28
|
value={
|
|
29
29
|
{
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/client/preview/types-6-3";
|
|
2
2
|
import { Grid } from "../../components/Grid";
|
|
3
|
-
import { Menu,
|
|
3
|
+
import { Menu, MenuButton, MenuDivider, MenuItem, SubMenu } from "../../react-menu3";
|
|
4
|
+
import { userEvent, within } from "@storybook/testing-library";
|
|
5
|
+
import { wait } from "../../utils/util";
|
|
6
|
+
import { expect, jest } from "@storybook/jest";
|
|
4
7
|
|
|
5
8
|
export default {
|
|
6
9
|
title: "Components / React-menu",
|
|
@@ -11,11 +14,14 @@ export default {
|
|
|
11
14
|
},
|
|
12
15
|
} as ComponentMeta<typeof Grid>;
|
|
13
16
|
|
|
17
|
+
const menuItemClickAction = jest.fn();
|
|
18
|
+
const newFileAction = jest.fn();
|
|
19
|
+
|
|
14
20
|
const ReactMenuTemplate: ComponentStory<typeof Grid> = () => {
|
|
15
21
|
return (
|
|
16
22
|
<>
|
|
17
|
-
<Menu menuButton={<MenuButton>Open menu</MenuButton>}>
|
|
18
|
-
<MenuItem>New File</MenuItem>
|
|
23
|
+
<Menu menuButton={<MenuButton>Open menu</MenuButton>} onItemClick={menuItemClickAction}>
|
|
24
|
+
<MenuItem onClick={newFileAction}>New File</MenuItem>
|
|
19
25
|
<MenuItem>Save</MenuItem>
|
|
20
26
|
<SubMenu label="Edit">
|
|
21
27
|
<MenuItem>Cut</MenuItem>
|
|
@@ -31,3 +37,60 @@ const ReactMenuTemplate: ComponentStory<typeof Grid> = () => {
|
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
export const ReactMenuControlled = ReactMenuTemplate.bind({});
|
|
40
|
+
ReactMenuControlled.play = async ({ canvasElement }) => {
|
|
41
|
+
const canvas = within(canvasElement);
|
|
42
|
+
|
|
43
|
+
const keyboard = async (key: string) => {
|
|
44
|
+
userEvent.keyboard(key);
|
|
45
|
+
await wait(100); // Wait for debounce
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const menuButton = await canvas.findByRole("button");
|
|
49
|
+
expect(menuButton).toBeInTheDocument();
|
|
50
|
+
|
|
51
|
+
const openMenu = async () => {
|
|
52
|
+
await wait(500); // Wait for debounce
|
|
53
|
+
userEvent.click(menuButton);
|
|
54
|
+
expect(await canvas.findByRole("menuitem", { name: "New File" })).toBeInTheDocument();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Check menu closes on click outside
|
|
58
|
+
await openMenu();
|
|
59
|
+
userEvent.click(menuButton.parentElement as Element);
|
|
60
|
+
expect(canvas.queryByRole("menuitem", { name: "New File" })).not.toBeInTheDocument();
|
|
61
|
+
|
|
62
|
+
// Test arrow down/up
|
|
63
|
+
await openMenu();
|
|
64
|
+
await keyboard("{arrowdown}");
|
|
65
|
+
await keyboard("{arrowdown}");
|
|
66
|
+
await keyboard("{arrowdown}");
|
|
67
|
+
await keyboard("{arrowdown}");
|
|
68
|
+
await keyboard("{arrowdown}");
|
|
69
|
+
expect(document.activeElement?.innerHTML).toBe("Exit");
|
|
70
|
+
|
|
71
|
+
await keyboard("{arrowup}");
|
|
72
|
+
expect(document.activeElement?.innerHTML).toBe("Print...");
|
|
73
|
+
|
|
74
|
+
// Escape close
|
|
75
|
+
userEvent.type(menuButton.parentElement as Element, "{Escape}");
|
|
76
|
+
expect(canvas.queryByRole("menuitem", { name: "New File" })).not.toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
// Test enter to select
|
|
79
|
+
await openMenu();
|
|
80
|
+
await keyboard("{arrowdown}");
|
|
81
|
+
await keyboard("{enter}");
|
|
82
|
+
expect(menuItemClickAction).toHaveBeenCalled();
|
|
83
|
+
expect(newFileAction).toHaveBeenCalled();
|
|
84
|
+
|
|
85
|
+
menuItemClickAction.mockClear();
|
|
86
|
+
newFileAction.mockClear();
|
|
87
|
+
|
|
88
|
+
// Test tab to select
|
|
89
|
+
await openMenu();
|
|
90
|
+
await keyboard("{arrowdown}");
|
|
91
|
+
await keyboard("{Tab}");
|
|
92
|
+
expect(menuItemClickAction).toHaveBeenCalled();
|
|
93
|
+
expect(newFileAction).toHaveBeenCalled();
|
|
94
|
+
|
|
95
|
+
newFileAction.mockClear();
|
|
96
|
+
};
|
package/src/styles/Grid.scss
CHANGED
package/src/utils/testUtil.ts
CHANGED
|
@@ -128,6 +128,7 @@ export const validateMenuOptions = async (
|
|
|
128
128
|
export const clickMenuOption = async (menuOptionText: string | RegExp): Promise<void> => {
|
|
129
129
|
await act(async () => {
|
|
130
130
|
const menuOption = await findMenuOption(menuOptionText);
|
|
131
|
+
// eslint-disable-next-line testing-library/await-async-utils
|
|
131
132
|
userEvent.click(menuOption);
|
|
132
133
|
});
|
|
133
134
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { textMatch } from "./textMatcher";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "L" => L*
|
|
5
|
+
* "=L" => L
|
|
6
|
+
* "*L*" => *L*
|
|
7
|
+
* "*L" => *L
|
|
8
|
+
* "A B" => A* and B*
|
|
9
|
+
* "A B, C" => (A* and B*) or C*
|
|
10
|
+
* "!A" => all values must not match A
|
|
11
|
+
* "=!A" => all values must not match exactly A
|
|
12
|
+
* Returns true if there's a text match.
|
|
13
|
+
*/
|
|
14
|
+
describe("textMatch", () => {
|
|
15
|
+
test("textMatch", () => {
|
|
16
|
+
const validate = [
|
|
17
|
+
{
|
|
18
|
+
value: "",
|
|
19
|
+
matched: [""],
|
|
20
|
+
unmatched: ["a", "a*", "*a*"],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "two words",
|
|
24
|
+
matched: ["", "*wo", "*or*", "tw", "two", "tw wo", "tw, rr", "=!tw"],
|
|
25
|
+
unmatched: ["ds", "o", "=tw", "tw rr", "!two", "!tw*"],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
validate.forEach((v) => {
|
|
30
|
+
for (const filter of v.matched) {
|
|
31
|
+
expect(textMatch(v.value, filter), `Must match text: ${v.value} filter: ${filter}`).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
for (const filter of v.unmatched) {
|
|
34
|
+
expect(textMatch(v.value, filter), `Mustn't match text: ${v.value} filter: ${filter}`).toBe(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/utils/textMatcher.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isMatch } from "matcher";
|
|
|
5
5
|
* Text matching with wildcards and multiple matchers.
|
|
6
6
|
*
|
|
7
7
|
* "L" => L*
|
|
8
|
-
* "L
|
|
8
|
+
* "=L" => L
|
|
9
9
|
* "*L*" => *L*
|
|
10
10
|
* "*L" => *L
|
|
11
11
|
* "A B" => A* and B*
|
|
@@ -19,8 +19,15 @@ export const textMatch = (text: string | undefined | null, filter: string): bool
|
|
|
19
19
|
const superFilters = filter
|
|
20
20
|
.split(",")
|
|
21
21
|
.map((sf) => sf.trim())
|
|
22
|
-
.filter((sf) => sf)
|
|
22
|
+
.filter((sf) => sf)
|
|
23
|
+
.map((r) =>
|
|
24
|
+
r
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.map((f) => (f.startsWith("=") ? f.slice(1) : f + "*"))
|
|
27
|
+
.join(" "),
|
|
28
|
+
);
|
|
23
29
|
const [negativeFilters, positiveFilters] = partition(superFilters, (superFilters) => superFilters.startsWith("!"));
|
|
30
|
+
|
|
24
31
|
const values = text.replaceAll(",", " ").trim().split(/\s+/);
|
|
25
32
|
return (
|
|
26
33
|
(isEmpty(positiveFilters) || // Not filtered
|