Skip to content
Snippets Groups Projects
Unverified Commit ae9cae6e authored by Oisin Coveney's avatar Oisin Coveney Committed by GitHub
Browse files

Add `ToolbarButton` to start replacing dashboard header buttons (#46628)

parent 6d8c0392
No related branches found
No related tags found
No related merge requests found
import type { ButtonHTMLAttributes, Ref, MouseEvent } from "react";
import { forwardRef } from "react";
import type { ActionIconProps, IconName } from "metabase/ui";
import { ActionIcon, Box, Icon, Tooltip } from "metabase/ui";
export type ToolbarButtonProps = {
icon?: IconName;
"aria-label": string;
tooltipLabel?: string;
visibleOnSmallScreen?: boolean;
isActive?: boolean;
hasBackground?: boolean;
} & ActionIconProps &
ButtonHTMLAttributes<HTMLButtonElement>;
export const ToolbarButton = forwardRef(function ToolbarButton(
{
icon = "unknown",
"aria-label": ariaLabel,
onClick,
tooltipLabel,
visibleOnSmallScreen = true,
isActive = false,
hasBackground = true,
children,
disabled,
...actionIconProps
}: ToolbarButtonProps,
ref: Ref<HTMLButtonElement>,
) {
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (onClick && !disabled) {
onClick?.(e);
}
};
const actionButton = (
<ActionIcon
data-testid="toolbar-button"
data-is-active={isActive}
ref={ref}
display={{
base: visibleOnSmallScreen ? "flex" : "none",
sm: "flex",
}}
size="2rem"
variant="viewHeader"
aria-label={ariaLabel}
onClick={handleButtonClick}
bg={hasBackground ? undefined : "transparent"}
disabled={disabled}
{...actionIconProps}
>
{children ?? (
<Icon
name={icon}
color={isActive ? "var(--mb-color-brand)" : undefined}
/>
)}
</ActionIcon>
);
if (!tooltipLabel) {
return actionButton;
}
return (
<Tooltip label={tooltipLabel}>
<Box>{actionButton}</Box>
</Tooltip>
);
});
import { render, screen } from "@testing-library/react";
import userEvent, {
PointerEventsCheckLevel,
} from "@testing-library/user-event";
import { ToolbarButton, type ToolbarButtonProps } from "./ToolbarButton";
const setup = (props: Partial<ToolbarButtonProps> = {}) => {
const mergedProps: ToolbarButtonProps = {
"aria-label": "Test Button",
icon: "gear",
...props,
};
render(<ToolbarButton {...mergedProps} />);
};
describe("ToolbarButton", () => {
describe("Rendering", () => {
it("renders with default props", () => {
setup();
const button = screen.getByTestId("toolbar-button");
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute("aria-label", "Test Button");
});
it("renders with custom icon", () => {
setup({ icon: "check" });
const button = screen.getByTestId("toolbar-button");
expect(button).toBeInTheDocument();
expect(screen.getByLabelText("check icon")).toBeInTheDocument();
});
it("renders children instead of icon when provided", () => {
setup({ children: <span>Custom Content</span> });
expect(screen.getByText("Custom Content")).toBeInTheDocument();
expect(screen.queryByLabelText("gear icon")).not.toBeInTheDocument();
});
});
describe("Visibility", () => {
it("is visible on small screens by default", () => {
setup();
const button = screen.getByTestId("toolbar-button");
expect(button).toHaveStyle({ display: "flex" });
});
it("can be hidden on small screens", () => {
setup({ visibleOnSmallScreen: false });
const button = screen.getByTestId("toolbar-button");
expect(button).toHaveStyle({ display: "none" });
});
});
describe("Tooltip", () => {
it("renders without tooltip by default", () => {
setup();
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
});
it("renders with tooltip when tooltipLabel is provided", async () => {
setup({ tooltipLabel: "Tooltip Text" });
await userEvent.hover(screen.getByTestId("toolbar-button"));
expect(await screen.findByText("Tooltip Text")).toBeInTheDocument();
});
});
describe("Background", () => {
it("has background by default", () => {
setup();
const button = screen.getByTestId("toolbar-button");
expect(button).not.toHaveStyle({ background: "transparent" });
});
it("can have transparent background", () => {
setup({ hasBackground: false });
const button = screen.getByTestId("toolbar-button");
expect(button).toHaveStyle({ background: "transparent" });
});
});
describe("Interaction", () => {
it("calls onClick when clicked and not disabled", async () => {
const onClick = jest.fn();
setup({ onClick });
const button = screen.getByTestId("toolbar-button");
await userEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
it("does not call onClick when clicked and disabled", async () => {
const onClick = jest.fn();
setup({ onClick, disabled: true });
const button = screen.getByTestId("toolbar-button");
await userEvent.click(button, {
pointerEventsCheck: PointerEventsCheckLevel.Never,
});
expect(onClick).not.toHaveBeenCalled();
});
});
});
export * from "./ToolbarButton";
......@@ -32,12 +32,16 @@ export const getActionIconOverrides =
color: theme.fn.themeColor("text-dark"),
backgroundColor: "transparent",
border: "1px solid transparent",
transition: "background 300ms linear, border 300ms linear",
transition: "all 300ms linear",
"&:hover": {
color: theme.fn.themeColor("brand"),
backgroundColor: theme.fn.themeColor("bg-medium"),
border: "1px solid transparent",
},
"&:disabled, &[data-disabled]": {
color: theme.fn.themeColor("text-light"),
backgroundColor: "transparent",
},
},
}),
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment