Skip to content

Commit a3f5dbd

Browse files
committed
Add grid-story with interactive cells and add tests for interactions
1 parent 79eb595 commit a3f5dbd

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { type GridColDef, StackPage, StackSwitch, Toolbar, ToolbarBackButton, ToolbarTitleItem } from "@comet/admin";
2+
import { Favorite } from "@comet/admin-icons";
3+
import { IconButton, InputBase } from "@mui/material";
4+
import { DataGrid } from "@mui/x-data-grid";
5+
import { type StoryObj } from "@storybook/react-webpack5";
6+
import { expect, waitFor, within } from "storybook/test";
7+
8+
import { exampleColumns, exampleRows } from "../../helpers/ExampleDataGrid";
9+
import { stackRouteDecorator } from "../../helpers/storyDecorators";
10+
import { storyRouterDecorator } from "../../story-router.decorator";
11+
import { DummyTextInContentOverflow } from "./utils/DummyTextInContentOverflow";
12+
import { DummyVisibilitySelect } from "./utils/DummyVisibilitySelect";
13+
import { useEditGridRow } from "./utils/useEditGridRow";
14+
15+
export default {
16+
title: "@comet/admin/data-grid/Clickable Rows With Interactive Cells",
17+
decorators: [stackRouteDecorator(), storyRouterDecorator()],
18+
};
19+
20+
export const Story = () => {
21+
// Grid must be a separate component to make sure the stackSwitchApi context is correct
22+
const Grid = () => {
23+
const { handleDataGridRowClick, EditIconButton } = useEditGridRow();
24+
25+
const columns: GridColDef[] = [
26+
...exampleColumns,
27+
{
28+
field: "input",
29+
type: "actions",
30+
headerName: "Input",
31+
renderCell: () => <InputBase aria-label="Text Input" />,
32+
flex: 1,
33+
},
34+
{
35+
field: "description",
36+
type: "actions",
37+
headerName: "Description",
38+
renderCell: () => <DummyTextInContentOverflow />,
39+
flex: 2,
40+
},
41+
{
42+
field: "visible",
43+
type: "actions",
44+
headerName: "Visible",
45+
width: 120,
46+
renderCell: () => <DummyVisibilitySelect />,
47+
},
48+
{
49+
field: "actions",
50+
type: "actions",
51+
headerName: "",
52+
width: 84,
53+
renderCell: (params) => (
54+
<>
55+
<IconButton color="secondary" aria-label="Add to favorites">
56+
<Favorite />
57+
</IconButton>
58+
<EditIconButton id={params.row.id} />
59+
</>
60+
),
61+
},
62+
];
63+
return <DataGrid checkboxSelection rows={exampleRows} columns={columns} onRowClick={handleDataGridRowClick} />;
64+
};
65+
66+
return (
67+
<StackSwitch>
68+
<StackPage name="grid">
69+
<Grid />
70+
</StackPage>
71+
<StackPage name="edit">
72+
{(id) => {
73+
const row = exampleRows.find((row) => row.id.toString() === id);
74+
if (!row) return null;
75+
76+
return (
77+
<Toolbar>
78+
<ToolbarBackButton />
79+
<ToolbarTitleItem>
80+
Editing {row.firstName} {row.lastName} (ID: {id})
81+
</ToolbarTitleItem>
82+
</Toolbar>
83+
);
84+
}}
85+
</StackPage>
86+
</StackSwitch>
87+
);
88+
};
89+
90+
export const TestClickingEditButton: StoryObj<typeof Story> = {
91+
render: Story,
92+
play: async ({ canvas, userEvent, step }) => {
93+
await step("Click on edit button", async () => {
94+
const gridRowsContainer = canvas.getByRole("rowgroup");
95+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
96+
const firstNameCell = within(firstRow).getAllByRole("gridcell")[1];
97+
const firstName = firstNameCell.textContent;
98+
99+
const editButton = within(firstRow).getByLabelText(/edit/i);
100+
await userEvent.click(editButton);
101+
102+
const toolbarEditText = await within(document.body).findByText(new RegExp(`Editing ${firstName}`));
103+
104+
expect(toolbarEditText).toBeInTheDocument();
105+
expect(firstNameCell).not.toBeInTheDocument();
106+
expect(editButton).not.toBeInTheDocument();
107+
});
108+
},
109+
};
110+
111+
export const TestClickingRow: StoryObj<typeof Story> = {
112+
render: Story,
113+
play: async ({ canvas, userEvent, step }) => {
114+
await step("Click on row (first cell)", async () => {
115+
const gridRowsContainer = canvas.getByRole("rowgroup");
116+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
117+
const firstNameCell = within(firstRow).getAllByRole("gridcell")[1];
118+
const firstName = firstNameCell.textContent;
119+
120+
await userEvent.click(firstNameCell);
121+
122+
const toolbarEditText = await within(document.body).findByText(new RegExp(`Editing ${firstName}`));
123+
124+
expect(toolbarEditText).toBeInTheDocument();
125+
expect(firstNameCell).not.toBeInTheDocument();
126+
});
127+
},
128+
};
129+
130+
export const TestClickingCheckbox: StoryObj<typeof Story> = {
131+
render: Story,
132+
play: async ({ canvas, userEvent, step }) => {
133+
await step("Click on checkbox", async () => {
134+
const gridRowsContainer = canvas.getByRole("rowgroup");
135+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
136+
137+
const inputTypeCheckbox = within(firstRow).getByRole("checkbox");
138+
await userEvent.click(inputTypeCheckbox);
139+
140+
expect(firstRow).toBeInTheDocument();
141+
expect(inputTypeCheckbox).toBeChecked();
142+
143+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
144+
expect(toolbarEditText).toBeNull();
145+
});
146+
},
147+
};
148+
149+
export const TestClickingFavoriteButton: StoryObj<typeof Story> = {
150+
render: Story,
151+
play: async ({ canvas, userEvent, step }) => {
152+
await step("Click on favorite button", async () => {
153+
const gridRowsContainer = canvas.getByRole("rowgroup");
154+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
155+
156+
const favoriteButton = within(firstRow).getByLabelText(/add to favorites/i);
157+
await userEvent.click(favoriteButton);
158+
159+
expect(firstRow).toBeInTheDocument();
160+
161+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
162+
expect(toolbarEditText).toBeNull();
163+
});
164+
},
165+
};
166+
167+
export const TestSettingInvisible: StoryObj<typeof Story> = {
168+
render: Story,
169+
play: async ({ canvas, userEvent, step }) => {
170+
const gridRowsContainer = canvas.getByRole("rowgroup");
171+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
172+
const selectVisibilityButton = within(firstRow).getByLabelText(/select visibility/i);
173+
174+
await step("Open visibility menu", async () => {
175+
await userEvent.click(selectVisibilityButton);
176+
177+
expect(firstRow).toBeInTheDocument();
178+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
179+
expect(toolbarEditText).toBeNull();
180+
});
181+
await step("Click on invisible option", async () => {
182+
const menu = await waitFor(() => within(document.body).getByRole("menu"), { timeout: 4000 });
183+
const invisibleOption = within(menu).getByText(/invisible/i);
184+
await userEvent.click(invisibleOption);
185+
186+
expect(firstRow).toBeInTheDocument();
187+
expect(selectVisibilityButton).toHaveTextContent(/invisible/i);
188+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
189+
expect(toolbarEditText).toBeNull();
190+
});
191+
},
192+
};
193+
194+
export const TestOpeningContentOverflow: StoryObj<typeof Story> = {
195+
render: Story,
196+
play: async ({ canvas, userEvent, step }) => {
197+
const startTextOfContentOverflow = "Ornare Inceptos Egestas Bibendum";
198+
199+
await step("Open visibility menu", async () => {
200+
const gridRowsContainer = canvas.getByRole("rowgroup");
201+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
202+
203+
const buttonsInRow = within(firstRow).getAllByRole("button");
204+
const descriptionButton = buttonsInRow.find((button) => button.textContent?.includes(startTextOfContentOverflow));
205+
206+
if (!descriptionButton) {
207+
throw new Error("Description button not found in row");
208+
}
209+
210+
await userEvent.click(descriptionButton);
211+
212+
expect(firstRow).toBeInTheDocument();
213+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
214+
expect(toolbarEditText).toBeNull();
215+
216+
const dialog = await waitFor(() => within(document.body).getByRole("dialog"), { timeout: 4000 });
217+
expect(dialog).toBeInTheDocument();
218+
219+
const dialogTitleText = within(dialog).queryByText(new RegExp(`${startTextOfContentOverflow}`));
220+
expect(dialogTitleText).toBeInTheDocument();
221+
});
222+
},
223+
};
224+
225+
export const TestInteractingWithInput: StoryObj<typeof Story> = {
226+
render: Story,
227+
play: async ({ canvas, userEvent, step }) => {
228+
const gridRowsContainer = canvas.getByRole("rowgroup");
229+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
230+
const inputWrapper = within(firstRow).getByLabelText(/text input/i);
231+
const input = within(inputWrapper).getByRole("textbox");
232+
233+
await step("Click on input", async () => {
234+
await userEvent.click(inputWrapper);
235+
expect(input).toHaveFocus();
236+
expect(firstRow).toBeInTheDocument();
237+
238+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
239+
expect(toolbarEditText).toBeNull();
240+
});
241+
await step("Type in input", async () => {
242+
await userEvent.type(input, "Hello");
243+
expect(input).toHaveValue("Hello");
244+
expect(firstRow).toBeInTheDocument();
245+
246+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
247+
expect(toolbarEditText).toBeNull();
248+
});
249+
},
250+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ContentOverflow } from "@comet/admin";
2+
import { Typography } from "@mui/material";
3+
4+
export const DummyTextInContentOverflow = () => {
5+
const paragraphsCount = 5;
6+
return (
7+
<ContentOverflow>
8+
<Typography fontWeight={600} gutterBottom>
9+
Ornare Inceptos Egestas Bibendum
10+
</Typography>
11+
{Array.from({ length: paragraphsCount }).map((_, index) => (
12+
<Typography key={index} variant="body2" gutterBottom={index !== paragraphsCount - 1}>
13+
Curabitur blandit tempus porttitor. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras mattis consectetur purus sit amet
14+
fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
15+
</Typography>
16+
))}
17+
</ContentOverflow>
18+
);
19+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ChevronDown, Invisible, Visible } from "@comet/admin-icons";
2+
import { Chip, ListItemIcon, ListItemText, Menu, MenuItem } from "@mui/material";
3+
import { useState } from "react";
4+
5+
export const DummyVisibilitySelect = () => {
6+
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
7+
const [visible, setVisible] = useState(true);
8+
9+
const handleMenuItemClick = (visible: boolean) => {
10+
setVisible(visible);
11+
setAnchorEl(null);
12+
};
13+
14+
return (
15+
<>
16+
<Chip
17+
component="button"
18+
aria-label="Select visibility"
19+
color={visible ? "success" : "default"}
20+
icon={<ChevronDown />}
21+
label={visible ? "Visible" : "Invisible"}
22+
onClick={(event) => setAnchorEl(event.currentTarget)}
23+
/>
24+
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}>
25+
<MenuItem onClick={() => handleMenuItemClick(true)}>
26+
<ListItemIcon>
27+
<Visible />
28+
</ListItemIcon>
29+
<ListItemText primary="Visible" />
30+
</MenuItem>
31+
<MenuItem onClick={() => handleMenuItemClick(false)}>
32+
<ListItemIcon>
33+
<Invisible />
34+
</ListItemIcon>
35+
<ListItemText primary="Invisible" />
36+
</MenuItem>
37+
</Menu>
38+
</>
39+
);
40+
};

0 commit comments

Comments
 (0)