diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx index 1600881dbb..93b4c4b701 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx @@ -1,5 +1,13 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, screen, userEvent, waitFor, within } from "storybook/test"; +import type { ComponentProps } from "react"; +import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test"; +import { + Table, + TableBody, + TableCell, + TableRow, +} from "#/components/Table/Table"; +import { useClickableTableRow } from "#/hooks/useClickableTableRow"; import { MockTemplate, MockTemplateVersion, @@ -48,3 +56,62 @@ const Example: Story = { }; export { Example as WorkspaceOutdatedTooltip }; + +// Regression coverage for the `useClickableTableRow` usage on the workspaces +// list. The trigger must stop click + keyboard propagation so the popover +// opens instead of the parent row's onClick swallowing the activation and +// navigating away. +type ClickableRowArgs = ComponentProps & { + onRowClick: () => void; +}; + +export const InsideClickableRow: StoryObj = { + args: { + onRowClick: fn(), + }, + decorators: [ + (Story, { args }) => { + const clickableProps = useClickableTableRow({ + onClick: args.onRowClick, + }); + return ( + + + + + + + + +
+ ); + }, + ], + play: async ({ args, canvasElement, step }) => { + const body = within(canvasElement.ownerDocument.body); + + await step("mouse click opens the popover", async () => { + await userEvent.click(body.getByRole("button", { name: "More info" })); + await waitFor(() => + expect(screen.getByRole("dialog")).toHaveTextContent( + MockTemplateVersion.message, + ), + ); + await userEvent.keyboard("{Escape}"); + }); + + await step("keyboard activation via Space opens the popover", async () => { + body.getByRole("button", { name: "More info" }).focus(); + await userEvent.keyboard(" "); + await waitFor(() => + expect(screen.getByRole("dialog")).toHaveTextContent( + MockTemplateVersion.message, + ), + ); + }); + + await step("the row's onClick was never called", async () => { + expect(args.onRowClick).not.toHaveBeenCalled(); + }); + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 444e23e882..add8a31610 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import Link from "@mui/material/Link"; -import { InfoIcon, RotateCcwIcon } from "lucide-react"; +import { CircleAlertIcon, RotateCcwIcon } from "lucide-react"; import { type FC, type ReactNode, useState } from "react"; import { useQuery } from "react-query"; import { toast } from "sonner"; @@ -35,27 +35,36 @@ export const WorkspaceOutdatedTooltip: FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); + // Stop activation from bubbling to a parent `useClickableTableRow` row, + // which navigates on click, Enter (onKeyDown), and Space (onKeyUp). Radix + // composes its own click handler, so the popover still opens. + const stopPropagation = (event: React.SyntheticEvent) => { + event.stopPropagation(); + }; + return ( {children ? ( - - ({ - color: theme.roles.notice.outline, - })} - size={14} - /> + + {children} ) : ( - - ({ - color: theme.roles.notice.outline, - })} - /> + + Outdated info )}