Skip to content
Snippets Groups Projects
Unverified Commit 277fc1df authored by github-automation-metabase's avatar github-automation-metabase Committed by GitHub
Browse files

Add generic components for summarize and breakout (#50039) (#50148)


Co-authored-by: default avatarOisin Coveney <oisin@metabase.com>
parent a25b52f5
Branches
Tags
No related merge requests found
Showing
with 338 additions and 1 deletion
import { Badge } from "@mantine/core";
import CS from "metabase/css/core/index.css";
import { ActionIcon, Icon } from "metabase/ui";
type AddBadgeListItemProps = {
name: string;
onClick: () => void;
};
export const AddBadgeListItem = ({ name, onClick }: AddBadgeListItemProps) => (
<Badge
classNames={{
inner: CS.cursorPointer,
}}
bg="var(--mb-color-bg-light)"
tt="capitalize"
size="lg"
variant="transparent"
c="var(--mb-color-text-brand)"
pr="sm"
pl="xs"
leftSection={
<ActionIcon radius="xl" size="sm" className={CS.bgMediumHover}>
<Icon name="add" c="var(--mb-color-text-brand)" size={10} />
</ActionIcon>
}
onClick={onClick}
>
{name}
</Badge>
);
import userEvent from "@testing-library/user-event";
import { render, screen } from "__support__/ui";
import { AddBadgeListItem } from "./AddBadgeListItem";
const setup = () => {
const name = "test badge";
const handleClick = jest.fn();
render(<AddBadgeListItem name={name} onClick={handleClick} />);
return { handleClick };
};
describe("AddBadgeListItem", () => {
it("renders badge with correct name", () => {
setup();
expect(screen.getByText("test badge")).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const { handleClick } = setup();
await userEvent.click(screen.getByText("test badge"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("renders add icon", () => {
setup();
expect(screen.getByLabelText("add icon")).toBeInTheDocument();
});
});
import { useState } from "react";
import { Box, Stack } from "metabase/ui";
import { BadgeList } from "./BadgeList";
export default {
title: "BadgeList",
component: BadgeList,
parameters: {
layout: "fullscreen",
},
};
export const DefaultLayoutBadgeList = {
render() {
const [items, setItems] = useState(
Array.from(Array(5).keys()).map(i => ({
name: `item ${i}`,
item: i,
})),
);
const [selectedItem, setSelectedItem] = useState<{
item?: number;
index?: number;
}>({});
const onSelectItem = (item?: number, index?: number) => {
if (item === selectedItem?.item) {
setSelectedItem({});
} else {
setSelectedItem({ item, index });
}
};
const onAddItem = () =>
setItems(nextItems => [
...nextItems,
{ name: `item ${nextItems.length}`, item: nextItems.length },
]);
const onRemoveItem = (_item?: number, index?: number) => {
if (typeof index === "number") {
setItems(nextItems => [
...nextItems.slice(0, index),
...nextItems.slice(index + 1),
]);
}
};
return (
<Stack>
<BadgeList
items={items}
onSelectItem={onSelectItem}
onAddItem={onAddItem}
onRemoveItem={onRemoveItem}
addButtonLabel="Add another item"
/>
<Box p="md">
{selectedItem?.item
? `The selected element is ${selectedItem.item} at index ${selectedItem.index}`
: "No element has been selected"}
</Box>
</Stack>
);
},
};
import { Group, Paper } from "metabase/ui";
import { AddBadgeListItem } from "./AddBadgeListItem";
import { BadgeListItem } from "./BadgeListItem";
export type BadgeListProps<T> = {
items: {
name: string;
item: T;
}[];
onSelectItem?: (item?: T, index?: number) => void;
onAddItem?: (item?: T) => void;
onRemoveItem?: (item?: T, index?: number) => void;
addButtonLabel?: string;
};
export const BadgeList = <T,>({
items,
onSelectItem,
onAddItem,
onRemoveItem,
addButtonLabel,
}: BadgeListProps<T>) => (
<Paper p="md" w="30rem">
<Group spacing="sm">
{items.map(({ name, item }, index) => (
<BadgeListItem
key={`${name}/${index}`}
onSelectItem={() => onSelectItem?.(item, index)}
onRemoveItem={() => onRemoveItem?.(item, index)}
name={name}
/>
))}
{addButtonLabel && (
<AddBadgeListItem name={addButtonLabel} onClick={() => onAddItem?.()} />
)}
</Group>
</Paper>
);
import userEvent from "@testing-library/user-event";
import { render, screen } from "__support__/ui";
import { BadgeList, type BadgeListProps } from "./BadgeList";
type SetupOpts = Partial<BadgeListProps<{ id: number }>>;
const setup = (opts: SetupOpts = {}) => {
const items = [
{ name: "item1", item: { id: 1 } },
{ name: "item2", item: { id: 2 } },
];
const onSelectItem = jest.fn();
const onAddItem = jest.fn();
const onRemoveItem = jest.fn();
const addButtonLabel =
"addButtonLabel" in opts ? opts.addButtonLabel : "Add new";
render(
<BadgeList
items={items}
onSelectItem={onSelectItem}
onRemoveItem={onRemoveItem}
onAddItem={onAddItem}
addButtonLabel={addButtonLabel}
/>,
);
return {
onSelectItem,
onAddItem,
onRemoveItem,
};
};
describe("BadgeList", () => {
it("renders all items", () => {
setup();
expect(screen.getByText("item1")).toBeInTheDocument();
expect(screen.getByText("item2")).toBeInTheDocument();
});
it("renders add button when label is provided", () => {
setup();
expect(screen.getByText("Add new")).toBeInTheDocument();
});
it("doesn't render add button when label is not provided", () => {
setup({ addButtonLabel: undefined });
expect(screen.queryByText("Add new")).not.toBeInTheDocument();
});
it("calls onSelectItem with correct item when badge is clicked", async () => {
const { onSelectItem } = setup();
await userEvent.click(screen.getByText("item1"));
expect(onSelectItem).toHaveBeenCalledWith({ id: 1 }, 0);
});
it("calls onRemoveItem with correct item when remove button is clicked", async () => {
const { onRemoveItem } = setup();
const removeButtons = screen.getAllByLabelText("close icon");
await userEvent.click(removeButtons[0]);
expect(onRemoveItem).toHaveBeenCalledWith({ id: 1 }, 0);
});
it("calls onAddItem when add button is clicked", async () => {
const { onAddItem } = setup();
await userEvent.click(screen.getByText("Add new"));
expect(onAddItem).toHaveBeenCalled();
});
});
import { Badge } from "@mantine/core";
import CS from "metabase/css/core/index.css";
import { ActionIcon, Icon } from "metabase/ui";
type BadgeListItemProps = {
onSelectItem?: () => void;
onRemoveItem?: () => void;
name: string;
};
export const BadgeListItem = ({
name,
onRemoveItem,
onSelectItem,
}: BadgeListItemProps) => (
<Badge
size="lg"
tt="capitalize"
variant="light"
bg="var(--mb-color-brand-light)"
c="var(--mb-color-text-brand)"
classNames={{
root: CS.bgLightHover,
inner: CS.cursorPointer,
}}
onClick={onSelectItem}
pr={0}
pl="sm"
rightSection={
<ActionIcon
radius="xl"
size="sm"
ml={0}
onClick={e => {
e.stopPropagation();
onRemoveItem?.();
}}
className={CS.bgMediumHover}
>
<Icon name="close" c="var(--mb-color-text-brand)" size={10} />
</ActionIcon>
}
>
{name}
</Badge>
);
import userEvent from "@testing-library/user-event";
import { render, screen } from "__support__/ui";
import { BadgeListItem } from "./BadgeListItem";
const setup = () => {
const name = "test badge";
const onSelectItem = jest.fn();
const onRemoveItem = jest.fn();
render(
<BadgeListItem
name={name}
onSelectItem={onSelectItem}
onRemoveItem={onRemoveItem}
/>,
);
return { onSelectItem, onRemoveItem };
};
describe("BadgeListItem", () => {
it("renders badge with correct name", () => {
setup();
expect(screen.getByText("test badge")).toBeInTheDocument();
});
it("calls onSelectItem when badge is clicked", async () => {
const { onSelectItem } = setup();
await userEvent.click(screen.getByText("test badge"));
expect(onSelectItem).toHaveBeenCalledTimes(1);
});
it("prevents badge click event when clicking remove button", async () => {
const { onSelectItem, onRemoveItem } = setup();
await userEvent.click(screen.getByLabelText("close icon"));
expect(onRemoveItem).toHaveBeenCalledTimes(1);
expect(onSelectItem).not.toHaveBeenCalled();
});
it("renders close icon", () => {
setup();
expect(screen.getByLabelText("close icon")).toBeInTheDocument();
});
});
export * from "./BadgeList";
......@@ -354,7 +354,8 @@
background-color: var(--mb-color-bg-light);
}
.bgMedium {
.bgMedium,
.bgMediumHover:hover {
background-color: var(--mb-color-bg-medium);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment