diff --git a/site/src/components/Combobox/Combobox.stories.tsx b/site/src/components/Combobox/Combobox.stories.tsx index 2207f4e64686f..075a57261e536 100644 --- a/site/src/components/Combobox/Combobox.stories.tsx +++ b/site/src/components/Combobox/Combobox.stories.tsx @@ -103,7 +103,8 @@ export const SearchAndFilter: Story = { screen.queryByRole("option", { name: "Kotlin" }), ).not.toBeInTheDocument(); }); - await userEvent.click(screen.getByRole("option", { name: "Rust" })); + // Accessible name includes both image alt text and text content: "Rust Rust" + await userEvent.click(screen.getByRole("option", { name: "Rust Rust" })); }, }; @@ -137,9 +138,11 @@ export const ClearSelectedOption: Story = { await userEvent.click(canvas.getByRole("button")); // const goOption = screen.getByText("Go"); // First select an option - await userEvent.click(await screen.findByRole("option", { name: "Go" })); + // Accessible name includes both image alt text and text content: "Go Go" + await userEvent.click(await screen.findByRole("option", { name: "Go Go" })); // Then clear it by selecting it again - await userEvent.click(await screen.findByRole("option", { name: "Go" })); + // Accessible name includes both image alt text and text content: "Go Go" + await userEvent.click(await screen.findByRole("option", { name: "Go Go" })); await waitFor(() => expect(canvas.getByRole("button")).toHaveTextContent("Select option"), diff --git a/site/src/components/Combobox/Combobox.tsx b/site/src/components/Combobox/Combobox.tsx index e815ca601f0af..6ef29cade0390 100644 --- a/site/src/components/Combobox/Combobox.tsx +++ b/site/src/components/Combobox/Combobox.tsx @@ -1,4 +1,3 @@ -import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; import { Command, @@ -23,6 +22,7 @@ import { Check, ChevronDown, CornerDownLeft } from "lucide-react"; import { Info } from "lucide-react"; import { type FC, type KeyboardEventHandler, useState } from "react"; import { cn } from "utils/cn"; +import { ExternalImage } from "../ExternalImage/ExternalImage"; interface ComboboxProps { value: string; @@ -69,19 +69,18 @@ export const Combobox: FC = ({ const isOpen = open ?? managedOpen; + const handleOpenChange = (newOpen: boolean) => { + setManagedOpen(newOpen); + onOpenChange?.(newOpen); + }; + return ( - { - setManagedOpen(newOpen); - onOpenChange?.(newOpen); - }} - > + - + = ({ keywords={[option.displayName]} onSelect={(currentValue) => { onSelect(currentValue === value ? "" : currentValue); + // Close the popover after selection + handleOpenChange(false); }} > - {showIcons && ( - - )} + {showIcons && + (option.icon ? ( + + ) : ( + /* Placeholder for missing icon to maintain layout consistency */ +
+ ))} {option.displayName}
{value === option.value && ( diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 4d0e3ff81c95f..44bc21951fcb9 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,5 +1,6 @@ import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; +import { expect, screen, waitFor } from "@storybook/test"; import { within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { chromatic } from "testHelpers/chromatic"; @@ -129,8 +130,8 @@ export const PresetsButNoneSelected: Story = { { ID: "preset-1", Name: "Preset 1", - Description: "", - Icon: "", + Description: "Preset 1 description", + Icon: "/emojis/0031-fe0f-20e3.png", Default: false, Parameters: [ { @@ -143,9 +144,8 @@ export const PresetsButNoneSelected: Story = { { ID: "preset-2", Name: "Preset 2", - Description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse imperdiet ultricies massa, eu dapibus ex fermentum ac.", - Icon: "/emojis/1f60e.png", + Description: "Preset 2 description", + Icon: "/emojis/0032-fe0f-20e3.png", Default: false, Parameters: [ { @@ -165,21 +165,12 @@ export const PresetsButNoneSelected: Story = { }; export const PresetSelected: Story = { - args: PresetsButNoneSelected.args, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click(canvas.getByText("Preset 1")); - }, -}; - -export const PresetSelectedWithHiddenParameters: Story = { args: PresetsButNoneSelected.args, play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Select a preset - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click(canvas.getByText("Preset 1")); + await userEvent.click(canvas.getByRole("button", { name: "None" })); + await userEvent.click(screen.getByText("Preset 1")); }, }; @@ -188,8 +179,8 @@ export const PresetSelectedWithVisibleParameters: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Select a preset - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click(canvas.getByText("Preset 1")); + await userEvent.click(canvas.getByRole("button", { name: "None" })); + await userEvent.click(screen.getByText("Preset 1")); // Toggle off the show preset parameters switch await userEvent.click(canvas.getByLabelText("Show preset parameters")); }, @@ -201,16 +192,12 @@ export const PresetReselected: Story = { const canvas = within(canvasElement); // First selection of Preset 1 - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click( - canvas.getByText("Preset 1", { selector: ".MuiMenuItem-root" }), - ); + await userEvent.click(canvas.getByRole("button", { name: "None" })); + await userEvent.click(screen.getByText("Preset 1")); // Reselect the same preset - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click( - canvas.getByText("Preset 1", { selector: ".MuiMenuItem-root" }), - ); + await userEvent.click(canvas.getByRole("button", { name: "Preset 1" })); + await userEvent.click(canvas.getByText("Preset 1")); }, }; @@ -230,12 +217,11 @@ export const PresetNoneSelected: Story = { const canvas = within(canvasElement); // First select a preset to set the field value - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click(canvas.getByText("Preset 1")); + await userEvent.click(canvas.getByRole("button", { name: "None" })); + await userEvent.click(screen.getByText("Preset 1")); // Then select "None" to unset the field value - await userEvent.click(canvas.getByLabelText("Preset")); - await userEvent.click(canvas.getByText("None")); + await userEvent.click(screen.getByText("None")); // Fill in required fields and submit to test the API call await userEvent.type( @@ -260,8 +246,8 @@ export const PresetsWithDefault: Story = { { ID: "preset-1", Name: "Preset 1", - Icon: "", - Description: "", + Description: "Preset 1 description", + Icon: "/emojis/0031-fe0f-20e3.png", Default: false, Parameters: [ { @@ -274,9 +260,8 @@ export const PresetsWithDefault: Story = { { ID: "preset-2", Name: "Preset 2", - Icon: "/emojis/1f60e.png", - Description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse imperdiet ultricies massa, eu dapibus ex fermentum ac.", + Description: "Preset 2 description", + Icon: "/emojis/0032-fe0f-20e3.png", Default: true, Parameters: [ { @@ -295,6 +280,10 @@ export const PresetsWithDefault: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); + // Should have the default preset listed first + await waitFor(() => + expect(canvas.getByRole("button", { name: "Preset 2 (Default)" })), + ); // Wait for the switch to be available since preset parameters are populated asynchronously await canvas.findByLabelText("Show preset parameters"); // Toggle off the show preset parameters switch diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index bc31e1db42742..8b5d4dabe675c 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -6,7 +6,7 @@ import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; -import { SelectFilter } from "components/Filter/SelectFilter"; +import { Combobox } from "components/Combobox/Combobox"; import { FormFields, FormFooter, @@ -158,16 +158,18 @@ export const CreateWorkspacePageView: FC = ({ ); const [presetOptions, setPresetOptions] = useState([ - { label: "None", value: "" }, + { displayName: "None", value: "undefined", icon: "", description: "" }, ]); const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); // Build options and keep default label/value in sync useEffect(() => { const options = [ - { label: "None", value: "" }, - ...presets.map((p) => ({ - label: p.Default ? `${p.Name} (Default)` : p.Name, - value: p.ID, + { displayName: "None", value: "undefined", icon: "", description: "" }, + ...presets.map((preset) => ({ + displayName: preset.Default ? `${preset.Name} (Default)` : preset.Name, + value: preset.ID, + icon: preset.Icon, + description: preset.Description, })), ]; setPresetOptions(options); @@ -392,12 +394,15 @@ export const CreateWorkspacePageView: FC = ({ - { + placeholder="Select a preset" + onSelect={(value) => { const index = presetOptions.findIndex( - (preset) => preset.value === option?.value, + (preset) => preset.value === value, ); if (index === -1) { return; @@ -405,12 +410,13 @@ export const CreateWorkspacePageView: FC = ({ setSelectedPresetIndex(index); form.setFieldValue( "template_version_preset_id", - // Empty string is equivalent to using None - option?.value === "" ? undefined : option?.value, + // "undefined" string is equivalent to using None option + // Combobox requires a value in order to correctly highlight the None option + presetOptions[index].value === "undefined" + ? undefined + : presetOptions[index].value, ); }} - placeholder="Select a preset" - selectedOption={presetOptions[selectedPresetIndex]} /> {/* Only show the preset parameter visibility toggle if preset parameters are actually being modified, otherwise it has no effect. */} diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 3fb267fb9daac..1feb4a8707f9b 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -215,7 +215,7 @@ export const IdpOrgSyncPageView: FC = ({ )}
-
+
diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 9282bd6bfd2b1..1be01567f6bb3 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -219,7 +219,7 @@ export const IdpGroupSyncForm: FC = ({
-
+
diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index 0825ab4217395..2efbf6f758393 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -159,7 +159,7 @@ export const IdpRoleSyncForm: FC = ({

{form.errors.field}

)}
-
+