Skip to content

Commit e9cef4f

Browse files
committed
Add grid-story with interactive cells and add tests for interactions
1 parent 58a62e3 commit e9cef4f

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { type GridColDef, StackPage, StackSwitch, Toolbar, ToolbarBackButton, ToolbarTitleItem } from "@comet/admin";
2+
import { Favorite } from "@comet/admin-icons";
3+
import { IconButton } 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: "description",
29+
type: "actions",
30+
headerName: "Description",
31+
renderCell: () => <DummyTextInContentOverflow />,
32+
flex: 2,
33+
},
34+
{
35+
field: "visible",
36+
type: "actions",
37+
headerName: "Visible",
38+
width: 120,
39+
renderCell: () => <DummyVisibilitySelect />,
40+
},
41+
{
42+
field: "actions",
43+
type: "actions",
44+
headerName: "",
45+
width: 84,
46+
renderCell: (params) => (
47+
<>
48+
<IconButton color="secondary" aria-label="Add to favorites">
49+
<Favorite />
50+
</IconButton>
51+
<EditIconButton id={params.row.id} />
52+
</>
53+
),
54+
},
55+
];
56+
return <DataGrid checkboxSelection rows={exampleRows} columns={columns} onRowClick={handleDataGridRowClick} />;
57+
};
58+
59+
return (
60+
<StackSwitch>
61+
<StackPage name="grid">
62+
<Grid />
63+
</StackPage>
64+
<StackPage name="edit">
65+
{(id) => {
66+
const row = exampleRows.find((row) => row.id.toString() === id);
67+
if (!row) return null;
68+
69+
return (
70+
<Toolbar>
71+
<ToolbarBackButton />
72+
<ToolbarTitleItem>
73+
Editing {row.firstName} {row.lastName} (ID: {id})
74+
</ToolbarTitleItem>
75+
</Toolbar>
76+
);
77+
}}
78+
</StackPage>
79+
</StackSwitch>
80+
);
81+
};
82+
83+
export const TestClickingEditButton: StoryObj<typeof Story> = {
84+
render: Story,
85+
play: async ({ canvas, userEvent, step }) => {
86+
await step("Click on edit button", async () => {
87+
const gridRowsContainer = canvas.getByRole("rowgroup");
88+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
89+
const firstNameCell = within(firstRow).getAllByRole("gridcell")[1];
90+
const firstName = firstNameCell.textContent;
91+
92+
const editButton = within(firstRow).getByLabelText(/edit/i);
93+
await userEvent.click(editButton);
94+
95+
const toolbarEditText = await within(document.body).findByText(new RegExp(`Editing ${firstName}`));
96+
97+
expect(toolbarEditText).toBeInTheDocument();
98+
expect(firstNameCell).not.toBeInTheDocument();
99+
expect(editButton).not.toBeInTheDocument();
100+
});
101+
},
102+
};
103+
104+
export const TestClickingRow: StoryObj<typeof Story> = {
105+
render: Story,
106+
play: async ({ canvas, userEvent, step }) => {
107+
await step("Click on row (first cell)", async () => {
108+
const gridRowsContainer = canvas.getByRole("rowgroup");
109+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
110+
const firstNameCell = within(firstRow).getAllByRole("gridcell")[1];
111+
const firstName = firstNameCell.textContent;
112+
113+
await userEvent.click(firstNameCell);
114+
115+
const toolbarEditText = await within(document.body).findByText(new RegExp(`Editing ${firstName}`));
116+
117+
expect(toolbarEditText).toBeInTheDocument();
118+
expect(firstNameCell).not.toBeInTheDocument();
119+
});
120+
},
121+
};
122+
123+
export const TestClickingCheckbox: StoryObj<typeof Story> = {
124+
render: Story,
125+
play: async ({ canvas, userEvent, step }) => {
126+
await step("Click on checkbox", async () => {
127+
const gridRowsContainer = canvas.getByRole("rowgroup");
128+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
129+
130+
const inputTypeCheckbox = within(firstRow).getByRole("checkbox");
131+
await userEvent.click(inputTypeCheckbox);
132+
133+
expect(firstRow).toBeInTheDocument();
134+
expect(inputTypeCheckbox).toBeChecked();
135+
136+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
137+
expect(toolbarEditText).toBeNull();
138+
});
139+
},
140+
};
141+
142+
export const TestClickingFavoriteButton: StoryObj<typeof Story> = {
143+
render: Story,
144+
play: async ({ canvas, userEvent, step }) => {
145+
await step("Click on favorite button", async () => {
146+
const gridRowsContainer = canvas.getByRole("rowgroup");
147+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
148+
149+
const favoriteButton = within(firstRow).getByLabelText(/add to favorites/i);
150+
await userEvent.click(favoriteButton);
151+
152+
expect(firstRow).toBeInTheDocument();
153+
154+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
155+
expect(toolbarEditText).toBeNull();
156+
});
157+
},
158+
};
159+
160+
export const TestSettingInvisible: StoryObj<typeof Story> = {
161+
render: Story,
162+
play: async ({ canvas, userEvent, step }) => {
163+
const gridRowsContainer = canvas.getByRole("rowgroup");
164+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
165+
const selectVisibilityButton = within(firstRow).getByLabelText(/select visibility/i);
166+
167+
await step("Open visibility menu", async () => {
168+
await userEvent.click(selectVisibilityButton);
169+
170+
expect(firstRow).toBeInTheDocument();
171+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
172+
expect(toolbarEditText).toBeNull();
173+
});
174+
await step("Click on invisible option", async () => {
175+
const menu = await waitFor(() => within(document.body).getByRole("menu"), { timeout: 4000 });
176+
const invisibleOption = within(menu).getByText(/invisible/i);
177+
await userEvent.click(invisibleOption);
178+
179+
expect(firstRow).toBeInTheDocument();
180+
expect(selectVisibilityButton).toHaveTextContent(/invisible/i);
181+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
182+
expect(toolbarEditText).toBeNull();
183+
});
184+
},
185+
};
186+
187+
export const TestOpeningContentOverflow: StoryObj<typeof Story> = {
188+
render: Story,
189+
play: async ({ canvas, userEvent, step }) => {
190+
const startTextOfContentOverflow = "Ornare Inceptos Egestas Bibendum";
191+
192+
await step("Open visibility menu", async () => {
193+
const gridRowsContainer = canvas.getByRole("rowgroup");
194+
const firstRow = within(gridRowsContainer).getAllByRole("row")[0];
195+
196+
const buttonsInRow = within(firstRow).getAllByRole("button");
197+
const descriptionButton = buttonsInRow.find((button) => button.textContent?.includes(startTextOfContentOverflow));
198+
199+
if (!descriptionButton) {
200+
throw new Error("Description button not found in row");
201+
}
202+
203+
await userEvent.click(descriptionButton);
204+
205+
expect(firstRow).toBeInTheDocument();
206+
const toolbarEditText = within(document.body).queryByText(new RegExp(`Editing`));
207+
expect(toolbarEditText).toBeNull();
208+
209+
const dialog = await waitFor(() => within(document.body).getByRole("dialog"), { timeout: 4000 });
210+
expect(dialog).toBeInTheDocument();
211+
212+
const dialogTitleText = within(dialog).queryByText(new RegExp(`${startTextOfContentOverflow}`));
213+
expect(dialogTitleText).toBeInTheDocument();
214+
});
215+
},
216+
};
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)