Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

refactor: show icons for multi-select parameter options#18594

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
aslilac wants to merge1 commit intomain
base:main
Choose a base branch
Loading
fromlilac/multi-select-icons
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 10 additions & 20 deletionsdocs/about/contributing/frontend.md
View file
Open in desktop

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

🚫[linkspector]reported byreviewdog 🐶
Cannot reachhttps://stackoverflow.com/questions/69711888/react-testing-library-getbyrole-is-performing-extremely-slowly Status: 403

-<https://stackoverflow.com/questions/69711888/react-testing-library-getbyrole-is-performing-extremely-slowly>

Original file line numberDiff line numberDiff line change
Expand Up@@ -259,18 +259,16 @@ We use [Formik](https://formik.org/docs) for forms along with

## Testing

We use three types of testing in our app: **End-to-end (E2E)**, **Integration**
We use three types of testing in our app: **End-to-end (E2E)**, **Integration/Unit**
and **Visual Testing**.

### End-to-End (E2E)
### End-to-End (E2E) – Playwright

These are useful for testing complete flows like "Create a user", "Import
template", etc. We use [Playwright](https://playwright.dev/). If you only need
to test if the page is being rendered correctly, you should consider using the
**Visual Testing** approach.
template", etc. We use [Playwright](https://playwright.dev/). These tests run against a full Coder instance, backed by a database, and allows you to make sure that features work properly all the way through the stack. "End to end", so to speak.

For scenarios where you need to be authenticated, you can use
`test.use({ storageState: getStatePath("authState") })`.
For scenarios where you need to be authenticated as a certain user, you can use
`login` helper. Passing it some user credentials will log out of any other user account, and will attempt to login using those credentials.

For ease of debugging, it's possible to run a Playwright test in headful mode
running a Playwright server on your local machine, and executing the test inside
Expand All@@ -289,22 +287,14 @@ local machine and forward the necessary ports to your workspace. At the end of
the script, you will land _inside_ your workspace with environment variables set
so you can simply execute the test (`pnpm run playwright:test`).

### Integration
### Integration/Unit – Jest

Test user interactions like "Click in a button shows a dialog", "Submit the form
sends the correct data", etc. For this, we use [Jest](https://jestjs.io/) and
[react-testing-library](https://testing-library.com/docs/react-testing-library/intro/).
If the test involves routing checks like redirects or maybe checking the info on
another page, you should probably consider using the **E2E** approach.
We use Jest mostly for testing code that does _not_ pertain to React. Functions and classes that contain notable app logic, and which are well abstracted from React should have accompanying tests. If the logic is tightly coupled to a React component, a Storybook test or an E2E test may be a better option depending on the scenario.

### Visualtesting
### VisualTesting – Storybook

We use visual tests to test components without user interaction like testing if
a page/component is rendered correctly depending on some parameters, if a button
is showing a spinner, if `loading` props are passed correctly, etc. This should
always be your first option since it is way easier to maintain. For this, we use
[Storybook](https://storybook.js.org/) and
[Chromatic](https://www.chromatic.com/).
We use Storybook for testing all of our React code. For static components, you simply add a story that renders the components with the props that you would like to test, and Storybook will record snapshots of it to ensure that it isn't changed unintentionally. If you would like to test an interaction with the component, then you can add an interaction test by specifying a `play` function for the story. For stories with an interaction test, a snapshot will be recorded of the end state of the component. We use
[Chromatic](https://www.chromatic.com/) to manage and compare snapshots in CI.

To learn more about testing components that fetch API data, refer to the
[**Where to fetch data**](#where-to-fetch-data) section.
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,3 +98,184 @@ export const ClearAllComboboxItems: Story = {
);
},
};

export const WithIcons: Story = {
args: {
placeholder: "Select technology",
emptyIndicator: (
<p className="text-center text-md text-content-primary">
All technologies selected
</p>
),
options: [
{
label: "Docker",
value: "docker",
icon: "/icon/docker.png",
},
{
label: "Kubernetes",
value: "kubernetes",
icon: "/icon/k8s.png",
},
{
label: "VS Code",
value: "vscode",
icon: "/icon/code.svg",
},
{
label: "JetBrains",
value: "jetbrains",
icon: "/icon/intellij.svg",
},
{
label: "Jupyter",
value: "jupyter",
icon: "/icon/jupyter.svg",
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Open the combobox
await userEvent.click(canvas.getByPlaceholderText("Select technology"));

// Verify that Docker option has an icon
const dockerOption = canvas.getByRole("option", { name: /Docker/i });
const dockerIcon = dockerOption.querySelector("img");
await expect(dockerIcon).toBeInTheDocument();
await expect(dockerIcon).toHaveAttribute("src", "/icon/docker.png");

// Select Docker and verify icon appears in badge
await userEvent.click(dockerOption);

// Find the Docker badge
const dockerBadge = canvas
.getByText("Docker")
.closest('[role="button"]')?.parentElement;
const badgeIcon = dockerBadge?.querySelector("img");
await expect(badgeIcon).toBeInTheDocument();
await expect(badgeIcon).toHaveAttribute("src", "/icon/docker.png");
},
};

export const MixedWithAndWithoutIcons: Story = {
args: {
placeholder: "Select resource",
options: [
{
label: "CPU",
value: "cpu",
icon: "/icon/memory.svg",
},
{
label: "Memory",
value: "memory",
icon: "/icon/memory.svg",
},
{
label: "Storage",
value: "storage",
},
{
label: "Network",
value: "network",
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Open the combobox
await userEvent.click(canvas.getByPlaceholderText("Select resource"));

// Verify that CPU option has an icon
const cpuOption = canvas.getByRole("option", { name: /CPU/i });
const cpuIcon = cpuOption.querySelector("img");
await expect(cpuIcon).toBeInTheDocument();

// Verify that Storage option does not have an icon
const storageOption = canvas.getByRole("option", { name: /Storage/i });
const storageIcon = storageOption.querySelector("img");
await expect(storageIcon).not.toBeInTheDocument();

// Select both and verify badges
await userEvent.click(cpuOption);
await userEvent.click(storageOption);

// CPU badge should have icon
const cpuBadge = canvas
.getByText("CPU")
.closest('[role="button"]')?.parentElement;
const cpuBadgeIcon = cpuBadge?.querySelector("img");
await expect(cpuBadgeIcon).toBeInTheDocument();

// Storage badge should not have icon
const storageBadge = canvas
.getByText("Storage")
.closest('[role="button"]')?.parentElement;
const storageBadgeIcon = storageBadge?.querySelector("img");
await expect(storageBadgeIcon).not.toBeInTheDocument();
},
};

export const WithGroupedIcons: Story = {
args: {
placeholder: "Select tools",
groupBy: "category",
options: [
{
label: "Docker",
value: "docker",
icon: "/icon/docker.png",
category: "Containers",
},
{
label: "Kubernetes",
value: "kubernetes",
icon: "/icon/k8s.png",
category: "Containers",
},
{
label: "VS Code",
value: "vscode",
icon: "/icon/code.svg",
category: "IDEs",
},
{
label: "JetBrains",
value: "jetbrains",
icon: "/icon/intellij.svg",
category: "IDEs",
},
{
label: "Zed",
value: "zed",
icon: "/icon/zed.svg",
category: "IDEs",
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Open the combobox
await userEvent.click(canvas.getByPlaceholderText("Select tools"));

// Verify grouped options still have icons
const dockerOption = canvas.getByRole("option", { name: /Docker/i });
const dockerIcon = dockerOption.querySelector("img");
await expect(dockerIcon).toBeInTheDocument();
await expect(dockerIcon).toHaveAttribute("src", "/icon/docker.png");

const vscodeOption = canvas.getByRole("option", { name: /VS Code/i });
const vscodeIcon = vscodeOption.querySelector("img");
await expect(vscodeIcon).toBeInTheDocument();
await expect(vscodeIcon).toHaveAttribute("src", "/icon/code.svg");

// Verify grouping headers are present
await expect(canvas.getByText("Containers")).toBeInTheDocument();
await expect(canvas.getByText("IDEs")).toBeInTheDocument();
},
};
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@
* This component is based on multiple-selector
* @see {@link https://shadcnui-expansions.typeart.cc/docs/multiple-selector}
*/
import { useTheme } from "@emotion/react";
import { Command as CommandPrimitive, useCommandState } from "cmdk";
import { Badge } from "components/Badge/Badge";
import {
Expand All@@ -25,11 +26,13 @@ import {
useRef,
useState,
} from "react";
import { getExternalImageStylesFromUrl } from "theme/externalImages";
import { cn } from "utils/cn";

export interface Option {
value: string;
label: string;
icon?: string;
disable?: boolean;
/** fixed option that can't be removed. */
fixed?: boolean;
Expand DownExpand Up@@ -204,6 +207,7 @@ export const MultiSelectCombobox = forwardRef<
const [onScrollbar, setOnScrollbar] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const theme = useTheme();

const [selected, setSelected] = useState<Option[]>(
arrayDefaultOptions ?? [],
Expand DownExpand Up@@ -487,7 +491,20 @@ export const MultiSelectCombobox = forwardRef<
data-fixed={option.fixed}
data-disabled={disabled || undefined}
>
{option.label}
<div className="flex items-center gap-1">
{option.icon && (
<img
src={option.icon}
alt=""
className="size-icon-xs"
css={getExternalImageStylesFromUrl(
theme.externalImages,
option.icon,
)}
/>
)}
{option.label}
</div>
<button
type="button"
data-testid="clear-option-button"
Expand DownExpand Up@@ -639,7 +656,20 @@ export const MultiSelectCombobox = forwardRef<
"cursor-default text-content-disabled",
)}
>
{option.label}
<div className="flex items-center gap-2">
{option.icon && (
<img
src={option.icon}
alt=""
className="size-icon-sm"
css={getExternalImageStylesFromUrl(
theme.externalImages,
option.icon,
)}
/>
)}
{option.label}
</div>
</CommandItem>
);
})}
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp