From 0028fea0e5341dcec537f0ff4b58aa220aa398b7 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 3 Aug 2026 19:09:59 +1000 Subject: [PATCH] refactor(site): migrate VS Code button menus off MUI (#27730) Replace MUI `Menu`/`MenuItem` (and the Emotion `css` prop) in `VSCodeDesktopButton` and `VSCodeDevContainerButton` with the shared shadcn `DropdownMenu`. The variant selector keeps the split-button layout, sizes the menu to the button group via an `inline-flex` anchor (so width is not the full parent), and uses `collisionPadding` so the menu stays inset from the viewport edge. --- .../VSCodeDesktopButton.tsx | 121 +++++++++--------- .../VSCodeDevContainerButton.tsx | 121 +++++++++--------- 2 files changed, 118 insertions(+), 124 deletions(-) diff --git a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx index e12f7e5dda..100523a326 100644 --- a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx +++ b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx @@ -1,9 +1,13 @@ -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import { type FC, useRef, useState } from "react"; +import { type FC, useId, useRef, useState } from "react"; import { API } from "#/api/api"; import type { DisplayApp } from "#/api/typesGenerated"; import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "#/components/DropdownMenu/DropdownMenu"; import { VSCodeIcon } from "#/components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon"; import { getVSCodeHref } from "#/modules/apps/apps"; @@ -22,81 +26,74 @@ type VSCodeVariant = "vscode" | "vscode-insiders"; const VARIANT_KEY = "vscode-variant"; +const isVSCodeVariant = (value: string | null): value is VSCodeVariant => { + return value === "vscode" || value === "vscode-insiders"; +}; + export const VSCodeDesktopButton: FC = (props) => { const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false); - const previousVariant = localStorage.getItem(VARIANT_KEY); const [variant, setVariant] = useState(() => { - if (!previousVariant) { - return "vscode"; - } - return previousVariant as VSCodeVariant; + const previousVariant = localStorage.getItem(VARIANT_KEY); + return isVSCodeVariant(previousVariant) ? previousVariant : "vscode"; }); const menuAnchorRef = useRef(null); + const menuContentId = useId(); - const selectVariant = (variant: VSCodeVariant) => { - localStorage.setItem(VARIANT_KEY, variant); - setVariant(variant); - setIsVariantMenuOpen(false); + const selectVariant = (nextVariant: VSCodeVariant) => { + localStorage.setItem(VARIANT_KEY, nextVariant); + setVariant(nextVariant); }; const includesVSCodeDesktop = props.displayApps.includes("vscode"); const includesVSCodeInsiders = props.displayApps.includes("vscode_insiders"); return includesVSCodeDesktop && includesVSCodeInsiders ? ( - <> -
- {variant === "vscode" ? ( - - ) : ( - - )} +
+ {variant === "vscode" ? ( + + ) : ( + + )} - { - setIsVariantMenuOpen(true); - }} - size="icon-lg" - > - - -
- - setIsVariantMenuOpen(false)} - css={{ - "& .MuiMenu-paper": { - width: menuAnchorRef.current?.clientWidth, - }, - }} + onOpenChange={setIsVariantMenuOpen} > - { - selectVariant("vscode"); - }} + + + + + + + - - {DisplayAppNameMap.vscode} - - { - selectVariant("vscode-insiders"); - }} - > - - {DisplayAppNameMap.vscode_insiders} - - - + { + selectVariant("vscode"); + }} + > + + {DisplayAppNameMap.vscode} + + { + selectVariant("vscode-insiders"); + }} + > + + {DisplayAppNameMap.vscode_insiders} + + + +
) : includesVSCodeDesktop ? ( ) : ( diff --git a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx index 85ba95659a..cac0c33b39 100644 --- a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx +++ b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx @@ -1,9 +1,13 @@ -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import { type FC, useRef, useState } from "react"; +import { type FC, useId, useRef, useState } from "react"; import { API } from "#/api/api"; import type { DisplayApp } from "#/api/typesGenerated"; import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "#/components/DropdownMenu/DropdownMenu"; import { VSCodeIcon } from "#/components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon"; import { AgentButton } from "../AgentButton"; @@ -24,83 +28,76 @@ type VSCodeVariant = "vscode" | "vscode-insiders"; const VARIANT_KEY = "vscode-variant"; +const isVSCodeVariant = (value: string | null): value is VSCodeVariant => { + return value === "vscode" || value === "vscode-insiders"; +}; + export const VSCodeDevContainerButton: FC = ( props, ) => { const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false); - const previousVariant = localStorage.getItem(VARIANT_KEY); const [variant, setVariant] = useState(() => { - if (!previousVariant) { - return "vscode"; - } - return previousVariant as VSCodeVariant; + const previousVariant = localStorage.getItem(VARIANT_KEY); + return isVSCodeVariant(previousVariant) ? previousVariant : "vscode"; }); const menuAnchorRef = useRef(null); + const menuContentId = useId(); - const selectVariant = (variant: VSCodeVariant) => { - localStorage.setItem(VARIANT_KEY, variant); - setVariant(variant); - setIsVariantMenuOpen(false); + const selectVariant = (nextVariant: VSCodeVariant) => { + localStorage.setItem(VARIANT_KEY, nextVariant); + setVariant(nextVariant); }; const includesVSCodeDesktop = props.displayApps.includes("vscode"); const includesVSCodeInsiders = props.displayApps.includes("vscode_insiders"); return includesVSCodeDesktop && includesVSCodeInsiders ? ( - <> -
- {variant === "vscode" ? ( - - ) : ( - - )} +
+ {variant === "vscode" ? ( + + ) : ( + + )} - { - setIsVariantMenuOpen(true); - }} - size="icon-lg" - > - - -
- - setIsVariantMenuOpen(false)} - css={{ - "& .MuiMenu-paper": { - width: menuAnchorRef.current?.clientWidth, - }, - }} + onOpenChange={setIsVariantMenuOpen} > - { - selectVariant("vscode"); - }} + + + + + + + - - {DisplayAppNameMap.vscode} - - { - selectVariant("vscode-insiders"); - }} - > - - {DisplayAppNameMap.vscode_insiders} - - - + { + selectVariant("vscode"); + }} + > + + {DisplayAppNameMap.vscode} + + { + selectVariant("vscode-insiders"); + }} + > + + {DisplayAppNameMap.vscode_insiders} + + + +
) : includesVSCodeDesktop ? ( ) : includesVSCodeInsiders ? (