mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-21 19:04:19 +00:00
feat(ui): add multiselect selection microinteractions
- Animate selected pills and item feedback in multiselect components - Add checkbox state transitions for provider group selections - Cover shared selection motion with focused unit tests
This commit is contained in:
6 files changed
+339
-15
No files matched your search
@@ -0,0 +1,52 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useState } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Checkbox } from "./checkbox";
|
||||
|
||||
function ControlledCheckbox() {
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
aria-label="Select provider"
|
||||
checked={checked}
|
||||
onCheckedChange={(value) => setChecked(value === true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe("Checkbox", () => {
|
||||
it("animates the background and check mark as one state change", async () => {
|
||||
// Given - A controlled checkbox in the unchecked state
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledCheckbox />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", { name: /select provider/i });
|
||||
const indicator = checkbox.querySelector(
|
||||
"[data-slot='checkbox-indicator']",
|
||||
);
|
||||
|
||||
// When - The user checks the checkbox
|
||||
await user.click(checkbox);
|
||||
|
||||
// Then - The background and check mark transitions use the same timing
|
||||
expect(checkbox).toHaveClass(
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(indicator).toHaveClass(
|
||||
"transition-[opacity,transform]",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"data-[state=checked]:scale-100",
|
||||
"data-[state=checked]:opacity-100",
|
||||
"data-[state=unchecked]:scale-75",
|
||||
"data-[state=unchecked]:opacity-0",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ function Checkbox({
|
||||
checked={indeterminate ? "indeterminate" : checked}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"peer shrink-0 rounded-sm border transition-all outline-none",
|
||||
"peer shrink-0 rounded-sm border transition-colors duration-200 ease-out outline-none motion-reduce:transition-none",
|
||||
sizeStyles.root,
|
||||
// Default state
|
||||
"bg-bg-input-primary border-border-input-primary shadow-[0_1px_2px_0_rgba(0,0,0,0.1)]",
|
||||
@@ -58,8 +58,9 @@ function Checkbox({
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
forceMount
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
className="grid place-content-center text-current transition-[opacity,transform] duration-200 ease-out data-[state=checked]:scale-100 data-[state=checked]:opacity-100 data-[state=indeterminate]:scale-100 data-[state=indeterminate]:opacity-100 data-[state=unchecked]:scale-75 data-[state=unchecked]:opacity-0 motion-reduce:scale-100 motion-reduce:transition-none"
|
||||
>
|
||||
{indeterminate || checked === "indeterminate" ? (
|
||||
<MinusIcon className={sizeStyles.icon} />
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { EnhancedMultiSelect } from "./enhanced-multi-select";
|
||||
|
||||
const options = [
|
||||
{ value: "aws-prod", label: "Production AWS" },
|
||||
{ value: "azure-dev", label: "Development Azure" },
|
||||
];
|
||||
|
||||
beforeAll(() => {
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe = vi.fn();
|
||||
unobserve = vi.fn();
|
||||
disconnect = vi.fn();
|
||||
} as unknown as typeof ResizeObserver;
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
|
||||
configurable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
describe("EnhancedMultiSelect", () => {
|
||||
it("uses visible trigger and chevron open-state motion", () => {
|
||||
render(
|
||||
<EnhancedMultiSelect
|
||||
options={options}
|
||||
onValueChange={() => {}}
|
||||
placeholder="Select providers"
|
||||
aria-label="Select providers"
|
||||
/>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole("combobox", { name: /select providers/i });
|
||||
const icon = trigger.querySelector("svg");
|
||||
|
||||
expect(trigger).toHaveClass(
|
||||
"group",
|
||||
"transition-[background-color,border-color,color,box-shadow]",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(icon).toHaveClass(
|
||||
"transition-transform",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"group-aria-expanded:rotate-180",
|
||||
"motion-reduce:rotate-0",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("animates item selection feedback and checkbox visibility", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<EnhancedMultiSelect
|
||||
options={options}
|
||||
onValueChange={() => {}}
|
||||
placeholder="Select providers"
|
||||
aria-label="Select providers"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("combobox", { name: /select providers/i }),
|
||||
);
|
||||
|
||||
const option = screen.getByRole("option", { name: /production aws/i });
|
||||
const checkbox = option.querySelector("[data-slot='checkbox']");
|
||||
const checkboxIndicator = checkbox?.querySelector(
|
||||
"[data-slot='checkbox-indicator']",
|
||||
);
|
||||
|
||||
expect(option).toHaveClass(
|
||||
"transition-colors",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(checkbox).toHaveClass(
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(checkboxIndicator).toHaveClass(
|
||||
"transition-[opacity,transform]",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"data-[state=checked]:scale-100",
|
||||
"data-[state=checked]:opacity-100",
|
||||
"data-[state=unchecked]:scale-75",
|
||||
"data-[state=unchecked]:opacity-0",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("animates selected pills when values are added to the trigger", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onValueChange = vi.fn();
|
||||
|
||||
render(
|
||||
<EnhancedMultiSelect
|
||||
options={options}
|
||||
onValueChange={onValueChange}
|
||||
placeholder="Select providers"
|
||||
aria-label="Select providers"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("combobox", { name: /select providers/i }),
|
||||
);
|
||||
await user.click(screen.getByRole("option", { name: /production aws/i }));
|
||||
|
||||
const pill = within(
|
||||
screen.getByRole("combobox", { name: /select providers/i }),
|
||||
)
|
||||
.getByText("Production AWS")
|
||||
.closest("[data-slot='enhanced-multiselect-pill']");
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(["aws-prod"]);
|
||||
expect(pill).toHaveClass(
|
||||
"animate-in",
|
||||
"fade-in-0",
|
||||
"zoom-in-95",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:animate-none",
|
||||
"motion-reduce:transform-none",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -183,7 +183,7 @@ export function EnhancedMultiSelect({
|
||||
aria-controls={open ? listboxId : undefined}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
"border-border-input-primary bg-bg-input-primary text-text-neutral-primary data-[placeholder]:text-text-neutral-tertiary [&_svg:not([class*='text-'])]:text-text-neutral-tertiary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-bg-input-primary active:bg-bg-input-primary focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex h-auto min-h-12 w-full items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 focus-visible:ring-offset-1 [&_svg]:pointer-events-auto",
|
||||
"group border-border-input-primary bg-bg-input-primary text-text-neutral-primary data-[placeholder]:text-text-neutral-tertiary [&_svg:not([class*='text-'])]:text-text-neutral-tertiary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-bg-input-primary active:bg-bg-input-primary focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex h-auto min-h-12 w-full items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm shadow-xs transition-[background-color,border-color,color,box-shadow] duration-150 ease-out outline-none focus-visible:ring-1 focus-visible:ring-offset-1 motion-reduce:transition-none [&_svg]:pointer-events-auto",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className,
|
||||
)}
|
||||
@@ -200,7 +200,8 @@ export function EnhancedMultiSelect({
|
||||
<Badge
|
||||
key={value}
|
||||
variant="tag"
|
||||
className="m-1 cursor-default [&>svg]:pointer-events-auto"
|
||||
data-slot="enhanced-multiselect-pill"
|
||||
className="animate-in fade-in-0 zoom-in-95 m-1 cursor-default duration-150 ease-out motion-reduce:transform-none motion-reduce:animate-none motion-reduce:transition-none [&>svg]:pointer-events-auto"
|
||||
>
|
||||
<span className="cursor-default">{option.label}</span>
|
||||
<span
|
||||
@@ -224,7 +225,8 @@ export function EnhancedMultiSelect({
|
||||
{selectedValues.length > maxCount && (
|
||||
<Badge
|
||||
variant="tag"
|
||||
className="m-1 cursor-default [&>svg]:pointer-events-auto"
|
||||
data-slot="enhanced-multiselect-pill"
|
||||
className="animate-in fade-in-0 zoom-in-95 m-1 cursor-default duration-150 ease-out motion-reduce:transform-none motion-reduce:animate-none motion-reduce:transition-none [&>svg]:pointer-events-auto"
|
||||
>
|
||||
{`+ ${selectedValues.length - maxCount} more`}
|
||||
<span
|
||||
@@ -271,7 +273,7 @@ export function EnhancedMultiSelect({
|
||||
className="flex h-full min-h-6"
|
||||
/>
|
||||
<ChevronDown
|
||||
className="text-text-neutral-tertiary mx-2 h-4 cursor-pointer"
|
||||
className="text-text-neutral-tertiary mx-2 h-4 cursor-pointer transition-transform duration-200 ease-out group-aria-expanded:rotate-180 motion-reduce:rotate-0 motion-reduce:transition-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
@@ -281,7 +283,7 @@ export function EnhancedMultiSelect({
|
||||
<span className="text-text-neutral-tertiary mx-3 text-sm">
|
||||
{placeholder}
|
||||
</span>
|
||||
<ChevronDown className="text-text-neutral-tertiary mx-2 h-4 cursor-pointer" />
|
||||
<ChevronDown className="text-text-neutral-tertiary mx-2 h-4 cursor-pointer transition-transform duration-200 ease-out group-aria-expanded:rotate-180 motion-reduce:rotate-0 motion-reduce:transition-none" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
@@ -339,7 +341,7 @@ export function EnhancedMultiSelect({
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={option.disabled}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
"cursor-pointer transition-colors duration-150 ease-out motion-reduce:transition-none",
|
||||
option.disabled && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
disabled={option.disabled}
|
||||
|
||||
@@ -83,6 +83,138 @@ describe("MultiSelect", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses visible trigger and chevron open-state motion", () => {
|
||||
render(
|
||||
<MultiSelect values={[]} onValuesChange={() => {}}>
|
||||
<MultiSelectTrigger>
|
||||
<MultiSelectValue placeholder="Select accounts" />
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole("combobox");
|
||||
const icon = trigger.querySelector("svg");
|
||||
|
||||
expect(trigger).toHaveClass(
|
||||
"transition-[background-color,border-color,color,box-shadow]",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(icon).toHaveClass(
|
||||
"transition-transform",
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"group-aria-expanded:rotate-180",
|
||||
"motion-reduce:rotate-0",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses visible content open and close motion", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<MultiSelect values={[]} onValuesChange={() => {}}>
|
||||
<MultiSelectTrigger>
|
||||
<MultiSelectValue placeholder="Select accounts" />
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
|
||||
const content = document.querySelector("[data-slot='multiselect-content']");
|
||||
|
||||
expect(content).toHaveClass(
|
||||
"duration-200",
|
||||
"ease-out",
|
||||
"data-[state=open]:animate-in",
|
||||
"data-[state=open]:fade-in-0",
|
||||
"data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0",
|
||||
"data-[state=closed]:zoom-out-95",
|
||||
"data-[state=closed]:duration-100",
|
||||
"data-[state=closed]:ease-in",
|
||||
"motion-reduce:animate-none",
|
||||
"motion-reduce:transform-none",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("animates item selection feedback and check visibility", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<MultiSelect defaultValues={[]} onValuesChange={() => {}}>
|
||||
<MultiSelectTrigger>
|
||||
<MultiSelectValue placeholder="Select accounts" />
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
|
||||
const option = screen.getByRole("option", { name: /production aws/i });
|
||||
const checkIcon = option.querySelector("svg");
|
||||
|
||||
expect(option).toHaveClass(
|
||||
"transition-colors",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
expect(checkIcon).toHaveClass(
|
||||
"transition-[opacity,transform]",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("animates selected pills when values are added to the trigger", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<MultiSelect defaultValues={[]} onValuesChange={() => {}}>
|
||||
<MultiSelectTrigger>
|
||||
<MultiSelectValue placeholder="Select accounts" />
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
await user.click(screen.getByRole("option", { name: /production aws/i }));
|
||||
|
||||
const pill = within(screen.getByRole("combobox"))
|
||||
.getByText("Production AWS")
|
||||
.closest("[data-selected-item]");
|
||||
|
||||
expect(pill).toHaveClass(
|
||||
"animate-in",
|
||||
"fade-in-0",
|
||||
"zoom-in-95",
|
||||
"duration-150",
|
||||
"ease-out",
|
||||
"motion-reduce:animate-none",
|
||||
"motion-reduce:transform-none",
|
||||
"motion-reduce:transition-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("filters items without crashing when search is enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -153,15 +153,14 @@ export function MultiSelectTrigger({
|
||||
data-slot="multiselect-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-border-input-primary bg-bg-input-primary text-bg-button-secondary data-[placeholder]:text-bg-button-secondary [&_svg:not([class*='text-'])]:text-bg-button-secondary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex w-full items-center justify-between gap-2 overflow-hidden rounded-lg border px-4 py-3 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[52px] data-[size=sm]:h-10 *:data-[slot=multiselect-value]:line-clamp-1 *:data-[slot=multiselect-value]:flex *:data-[slot=multiselect-value]:items-center *:data-[slot=multiselect-value]:gap-2 dark:focus-visible:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6",
|
||||
"group border-border-input-primary bg-bg-input-primary text-bg-button-secondary data-[placeholder]:text-bg-button-secondary [&_svg:not([class*='text-'])]:text-bg-button-secondary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex w-full items-center justify-between gap-2 overflow-hidden rounded-lg border px-4 py-3 text-sm whitespace-nowrap shadow-xs transition-[background-color,border-color,color,box-shadow] duration-150 ease-out outline-none focus-visible:ring-1 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[52px] data-[size=sm]:h-10 *:data-[slot=multiselect-value]:line-clamp-1 *:data-[slot=multiselect-value]:flex *:data-[slot=multiselect-value]:items-center *:data-[slot=multiselect-value]:gap-2 motion-reduce:transition-none dark:focus-visible:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"text-bg-button-secondary size-6 shrink-0 opacity-70 transition-transform duration-200",
|
||||
open && "rotate-180",
|
||||
"text-bg-button-secondary size-6 shrink-0 opacity-70 transition-transform duration-200 ease-out group-aria-expanded:rotate-180 motion-reduce:rotate-0 motion-reduce:transition-none",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
@@ -267,7 +266,7 @@ export function MultiSelectValue({
|
||||
<Badge
|
||||
variant="tag"
|
||||
data-selected-item
|
||||
className="group flex items-center gap-1.5 px-2 py-1 text-xs font-medium"
|
||||
className="group animate-in fade-in-0 zoom-in-95 flex items-center gap-1.5 px-2 py-1 text-xs font-medium duration-150 ease-out motion-reduce:transform-none motion-reduce:animate-none motion-reduce:transition-none"
|
||||
key={value}
|
||||
onClick={
|
||||
clickToRemove
|
||||
@@ -415,7 +414,7 @@ export function MultiSelectItem({
|
||||
keywords={keywords}
|
||||
data-slot="multiselect-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-bg-button-secondary text-bg-button-secondary my-1 flex w-full cursor-pointer items-center justify-between gap-3 overflow-hidden rounded-lg px-4 py-3 text-sm outline-hidden select-none first:mt-0 last:mb-0 hover:bg-slate-200 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 dark:hover:bg-slate-700/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-bg-button-secondary text-bg-button-secondary my-1 flex w-full cursor-pointer items-center justify-between gap-3 overflow-hidden rounded-lg px-4 py-3 text-sm outline-hidden transition-colors duration-150 ease-out select-none first:mt-0 last:mb-0 hover:bg-slate-200 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 motion-reduce:transition-none dark:hover:bg-slate-700/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
|
||||
isSelected && "bg-slate-100 dark:bg-slate-800/50",
|
||||
disabled && "cursor-not-allowed opacity-50 hover:bg-transparent",
|
||||
className,
|
||||
@@ -431,8 +430,8 @@ export function MultiSelectItem({
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"text-bg-button-secondary size-5 shrink-0",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
"text-bg-button-secondary size-5 shrink-0 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none",
|
||||
isSelected ? "scale-100 opacity-100" : "scale-95 opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
||||
Reference in new issue
Block a user