diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts
index 856ece95c0b02..c69925589221a 100644
--- a/site/e2e/tests/groups/removeMember.spec.ts
+++ b/site/e2e/tests/groups/removeMember.spec.ts
@@ -33,9 +33,8 @@ test("remove member", async ({ page, baseURL }) => {
await expect(page).toHaveTitle(`${group.display_name} - Coder`);
const userRow = page.getByRole("row", { name: member.username });
- await userRow.getByRole("button", { name: "More options" }).click();
-
- const menu = page.locator("#more-options");
+ await userRow.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
await menu.getByText("Remove").click({ timeout: 1_000 });
await expect(page.getByText("Member removed successfully.")).toBeVisible();
diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts
index 08768d4bbae11..14741bdf38e00 100644
--- a/site/e2e/tests/organizationGroups.spec.ts
+++ b/site/e2e/tests/organizationGroups.spec.ts
@@ -79,8 +79,10 @@ test("create group", async ({ page }) => {
await expect(page.getByText("No users found")).toBeVisible();
// Remove someone from the group
- await addedRow.getByLabel("More options").click();
- await page.getByText("Remove").click();
+ await addedRow.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
+ await menu.getByText("Remove").click();
+
await expect(addedRow).not.toBeVisible();
// Delete the group
diff --git a/site/e2e/tests/organizationMembers.spec.ts b/site/e2e/tests/organizationMembers.spec.ts
index 51c3491ae3d62..639e6428edfb5 100644
--- a/site/e2e/tests/organizationMembers.spec.ts
+++ b/site/e2e/tests/organizationMembers.spec.ts
@@ -39,8 +39,9 @@ test("add and remove organization member", async ({ page }) => {
await expect(addedRow.getByText("+1 more")).toBeVisible();
// Remove them from the org
- await addedRow.getByLabel("More options").click();
- await page.getByText("Remove").click(); // Click the "Remove" option
+ await addedRow.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
+ await menu.getByText("Remove").click();
await page.getByRole("button", { name: "Remove" }).click(); // Click "Remove" in the confirmation dialog
await expect(addedRow).not.toBeVisible();
});
diff --git a/site/e2e/tests/organizations/customRoles/customRoles.spec.ts b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts
index 1e1e518e96399..1f55e87de8bab 100644
--- a/site/e2e/tests/organizations/customRoles/customRoles.spec.ts
+++ b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts
@@ -37,8 +37,8 @@ test.describe("CustomRolesPage", () => {
await expect(roleRow.getByText(customRole.display_name)).toBeVisible();
await expect(roleRow.getByText("organization_member")).toBeVisible();
- await roleRow.getByRole("button", { name: "More options" }).click();
- const menu = page.locator("#more-options");
+ await roleRow.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
await menu.getByText("Edit").click();
await expect(page).toHaveURL(
@@ -118,7 +118,7 @@ test.describe("CustomRolesPage", () => {
// Verify that the more menu (three dots) is not present for built-in roles
await expect(
- roleRow.getByRole("button", { name: "More options" }),
+ roleRow.getByRole("button", { name: "Open menu" }),
).not.toBeVisible();
await deleteOrganization(org.name);
@@ -175,9 +175,9 @@ test.describe("CustomRolesPage", () => {
await page.goto(`/organizations/${org.name}/roles`);
const roleRow = page.getByTestId(`role-${customRole.name}`);
- await roleRow.getByRole("button", { name: "More options" }).click();
+ await roleRow.getByRole("button", { name: "Open menu" }).click();
- const menu = page.locator("#more-options");
+ const menu = page.getByRole("menu");
await menu.getByText("Delete…").click();
const input = page.getByRole("textbox");
diff --git a/site/e2e/tests/updateTemplate.spec.ts b/site/e2e/tests/updateTemplate.spec.ts
index e0bfac03cf036..43dd392443ea2 100644
--- a/site/e2e/tests/updateTemplate.spec.ts
+++ b/site/e2e/tests/updateTemplate.spec.ts
@@ -53,8 +53,10 @@ test("add and remove a group", async ({ page }) => {
await expect(row).toBeVisible();
// Now remove the group
- await row.getByLabel("More options").click();
- await page.getByText("Remove").click();
+ await row.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
+ await menu.getByText("Remove").click();
+
await expect(page.getByText("Group removed successfully!")).toBeVisible();
await expect(row).not.toBeVisible();
});
diff --git a/site/e2e/tests/users/removeUser.spec.ts b/site/e2e/tests/users/removeUser.spec.ts
index c44d64b39c13c..92aa3efaa803a 100644
--- a/site/e2e/tests/users/removeUser.spec.ts
+++ b/site/e2e/tests/users/removeUser.spec.ts
@@ -17,9 +17,9 @@ test("remove user", async ({ page, baseURL }) => {
await expect(page).toHaveTitle("Users - Coder");
const userRow = page.getByRole("row", { name: user.email });
- await userRow.getByRole("button", { name: "More options" }).click();
- const menu = page.locator("#more-options");
- await menu.getByText("Delete").click();
+ await userRow.getByRole("button", { name: "Open menu" }).click();
+ const menu = page.getByRole("menu");
+ await menu.getByText("Delete…").click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name of the user to delete").fill(user.username);
diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx
index c37f9f0146047..e56fd7cbe4343 100644
--- a/site/src/components/DropdownMenu/DropdownMenu.tsx
+++ b/site/src/components/DropdownMenu/DropdownMenu.tsx
@@ -196,7 +196,7 @@ export const DropdownMenuSeparator = forwardRef<
>(({ className, ...props }, ref) => (
));
diff --git a/site/src/components/MoreMenu/MoreMenu.stories.tsx b/site/src/components/MoreMenu/MoreMenu.stories.tsx
deleted file mode 100644
index e7a9968b01414..0000000000000
--- a/site/src/components/MoreMenu/MoreMenu.stories.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import GrassIcon from "@mui/icons-material/Grass";
-import KitesurfingIcon from "@mui/icons-material/Kitesurfing";
-import { action } from "@storybook/addon-actions";
-import type { Meta, StoryObj } from "@storybook/react";
-import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
-import {
- MoreMenu,
- MoreMenuContent,
- MoreMenuItem,
- MoreMenuTrigger,
- ThreeDotsButton,
-} from "./MoreMenu";
-
-const meta: Meta = {
- title: "components/MoreMenu",
- component: MoreMenu,
-};
-
-export default meta;
-type Story = StoryObj;
-
-const Example: Story = {
- args: {
- children: (
- <>
-
-
-
-
-
-
- Touch grass
-
-
-
- Touch water
-
-
- >
- ),
- },
- play: async ({ canvasElement, step }) => {
- const canvas = within(canvasElement);
-
- await step("Open menu", async () => {
- await userEvent.click(
- canvas.getByRole("button", { name: "More options" }),
- );
- await waitFor(() =>
- Promise.all([
- expect(screen.getByText(/touch grass/i)).toBeInTheDocument(),
- expect(screen.getByText(/touch water/i)).toBeInTheDocument(),
- ]),
- );
- });
- },
-};
-
-export { Example as MoreMenu };
diff --git a/site/src/components/MoreMenu/MoreMenu.tsx b/site/src/components/MoreMenu/MoreMenu.tsx
deleted file mode 100644
index 8ba7864fc5e5d..0000000000000
--- a/site/src/components/MoreMenu/MoreMenu.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined";
-import IconButton, { type IconButtonProps } from "@mui/material/IconButton";
-import Menu, { type MenuProps } from "@mui/material/Menu";
-import MenuItem, { type MenuItemProps } from "@mui/material/MenuItem";
-import {
- type FC,
- type HTMLProps,
- type PropsWithChildren,
- type ReactElement,
- cloneElement,
- createContext,
- forwardRef,
- useContext,
- useRef,
- useState,
-} from "react";
-
-type MoreMenuContextValue = {
- triggerRef: React.RefObject;
- close: () => void;
- open: () => void;
- isOpen: boolean;
-};
-
-const MoreMenuContext = createContext(
- undefined,
-);
-
-export const MoreMenu: FC = ({ children }) => {
- const triggerRef = useRef(null);
- const [isOpen, setIsOpen] = useState(false);
-
- const close = () => {
- setIsOpen(false);
- };
-
- const open = () => {
- setIsOpen(true);
- };
-
- return (
-
- {children}
-
- );
-};
-
-const useMoreMenuContext = () => {
- const ctx = useContext(MoreMenuContext);
-
- if (!ctx) {
- throw new Error("useMoreMenuContext must be used inside of MoreMenu");
- }
-
- return ctx;
-};
-
-export const MoreMenuTrigger: FC> = ({
- children,
- ...props
-}) => {
- const menu = useMoreMenuContext();
-
- return cloneElement(children as ReactElement, {
- "aria-haspopup": "true",
- ...props,
- ref: menu.triggerRef,
- onClick: menu.open,
- });
-};
-
-export const ThreeDotsButton = forwardRef(
- (props, ref) => {
- return (
-
-
-
- );
- },
-);
-
-export const MoreMenuContent: FC> = (
- props,
-) => {
- const menu = useMoreMenuContext();
-
- return (
-
- );
-};
-
-interface MoreMenuItemProps extends MenuItemProps {
- closeOnClick?: boolean;
- danger?: boolean;
-}
-
-export const MoreMenuItem: FC = ({
- closeOnClick = true,
- danger = false,
- ...menuItemProps
-}) => {
- const menu = useMoreMenuContext();
-
- return (
-