@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.
Files changed (32) hide show
  1. package/README.md +4 -4
  2. package/dist/index.css +3 -0
  3. package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +5 -4
  4. package/dist/src/utils/textMatcher.d.ts +1 -1
  5. package/dist/src/utils/textMatcher.test.d.ts +1 -0
  6. package/dist/step-ag-grid.esm.js +464 -689
  7. package/dist/step-ag-grid.esm.js.map +1 -1
  8. package/package.json +42 -42
  9. package/src/components/Grid.tsx +29 -27
  10. package/src/components/gridForm/GridFormDropDown.tsx +2 -2
  11. package/src/components/gridForm/GridFormMultiSelect.tsx +12 -6
  12. package/src/components/gridForm/GridFormSubComponentTextArea.tsx +0 -1
  13. package/src/components/gridForm/GridFormSubComponentTextInput.tsx +0 -1
  14. package/src/lui/TextAreaInput.tsx +1 -1
  15. package/src/lui/TextInputFormatted.tsx +1 -1
  16. package/src/react-menu3/components/ControlledMenu.tsx +19 -6
  17. package/src/stories/grid/GridKeyboardInteractions.stories.tsx +261 -0
  18. package/src/stories/grid/{GridPopoutBearing.stories.tsx → GridPopoverEditBearing.stories.tsx} +4 -4
  19. package/src/stories/grid/{GridPopoutEditDropDown.stories.tsx → GridPopoverEditDropDown.stories.tsx} +0 -0
  20. package/src/stories/grid/{GridPopoutEditMultiSelect.stories.tsx → GridPopoverEditMultiSelect.stories.tsx} +0 -0
  21. package/src/stories/grid/gridFormInteraction/GridFormDropDownInteraction.stories.tsx +10 -1
  22. package/src/stories/grid/gridFormInteraction/GridFormEditBearingCorrectionInteraction.stories.tsx +1 -1
  23. package/src/stories/grid/gridFormInteraction/GridFormEditBearingInteraction.stories.tsx +1 -1
  24. package/src/stories/grid/gridFormInteraction/GridFormMultiSelectInteraction.stories.tsx +153 -0
  25. package/src/stories/grid/gridFormInteraction/GridFormPopoverMenuInteraction.stories.tsx +1 -1
  26. package/src/stories/grid/gridFormInteraction/GridFormTextAreaInteraction.stories.tsx +1 -1
  27. package/src/stories/grid/gridFormInteraction/GridFormTextInputInteraction.stories.tsx +1 -1
  28. package/src/stories/react-menu/ReactMenu.stories.tsx +66 -3
  29. package/src/styles/Grid.scss +3 -0
  30. package/src/utils/testUtil.ts +1 -0
  31. package/src/utils/textMatcher.test.ts +38 -0
  32. 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, MenuItem, MenuButton, MenuDivider, SubMenu } from "../../react-menu3";
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
+ };
@@ -5,6 +5,8 @@
5
5
  .Grid-container {
6
6
  flex: 1;
7
7
  width: 100%;
8
+ display: flex;
9
+ flex-direction: column;
8
10
  }
9
11
 
10
12
  .Grid-sortIsStale {
@@ -20,6 +22,7 @@
20
22
  .Grid-quickFilter {
21
23
  width: 100%;
22
24
  margin-bottom: 16px;
25
+ flex: 0;
23
26
  }
24
27
 
25
28
  .Grid-quickFilterBox {
@@ -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
+ });
@@ -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*" => 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