fix(ui): fix role cancel and select dropdown scroll (#11128)

Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Prowler Bot
2026-05-12 13:21:18 +02:00
committed by GitHub
parent 8bfc1d85f5
commit 2fdc480beb
8 changed files with 177 additions and 10 deletions
+9
View File
@@ -2,6 +2,15 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.26.1] (Prowler 5.26.1)
### 🐞 Fixed
- Role form Cancel buttons now return to Roles [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125)
- Shared select dropdowns stay constrained and scrollable inside modals [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125)
---
## [1.26.0] (Prowler v5.26.0)
### 🚀 Added
@@ -1,10 +1,15 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AddRoleForm } from "./add-role-form";
const routerMocks = vi.hoisted(() => ({
push: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
useRouter: () => routerMocks,
}));
vi.mock("@/actions/roles/roles", () => ({
@@ -73,6 +78,7 @@ vi.mock("@/components/ui", () => ({
describe("AddRoleForm", () => {
afterEach(() => {
routerMocks.push.mockClear();
vi.unstubAllEnvs();
});
@@ -99,4 +105,16 @@ describe("AddRoleForm", () => {
expect(screen.queryByText("Manage Alerts")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Billing")).not.toBeInTheDocument();
});
it("navigates back to roles when cancel is clicked", async () => {
// Given
const user = userEvent.setup();
render(<AddRoleForm groups={[]} />);
// When
await user.click(screen.getByRole("button", { name: /cancel/i }));
// Then
expect(routerMocks.push).toHaveBeenCalledWith("/roles");
});
});
@@ -252,7 +252,11 @@ export const AddRoleForm = ({
)}
</div>
)}
<FormButtons submitText="Add Role" isDisabled={isLoading} />
<FormButtons
submitText="Add Role"
isDisabled={isLoading}
onCancel={() => router.push("/roles")}
/>
</form>
</Form>
);
@@ -271,7 +271,11 @@ export const EditRoleForm = ({
)}
</div>
)}
<FormButtons submitText="Update Role" isDisabled={isLoading} />
<FormButtons
submitText="Update Role"
isDisabled={isLoading}
onCancel={() => router.push("/roles")}
/>
</form>
</Form>
);
@@ -239,6 +239,46 @@ describe("MultiSelect", () => {
expect(screen.getByRole("dialog")).toHaveClass("max-w-[24rem]");
});
it("keeps long option lists scrollable inside the dropdown", async () => {
// Given
const user = userEvent.setup();
render(
<MultiSelect values={[]} onValuesChange={() => {}}>
<MultiSelectTrigger>
<MultiSelectValue placeholder="Select accounts" />
</MultiSelectTrigger>
<MultiSelectContent search={false}>
{Array.from({ length: 20 }, (_, index) => (
<MultiSelectItem key={index} value={`account-${index}`}>
Account {index}
</MultiSelectItem>
))}
</MultiSelectContent>
</MultiSelect>,
);
// When
await user.click(screen.getByRole("combobox"));
// Then
const list = screen
.getByRole("dialog")
.querySelector('[data-slot="command-list"]');
expect(screen.getByRole("dialog")).toHaveStyle({
maxHeight:
"min(360px, var(--radix-popover-content-available-height, 360px))",
});
expect(list).toHaveClass("minimal-scrollbar");
expect(list).toHaveStyle({
maxHeight:
"min(300px, var(--radix-popover-content-available-height, 300px))",
});
expect(list).toHaveClass("overflow-y-auto");
expect(list).toHaveClass("overscroll-contain");
});
it("keeps the legacy clear-all behavior by default", async () => {
const user = userEvent.setup();
const onValuesChange = vi.fn();
+17 -3
View File
@@ -10,6 +10,7 @@ import {
useEffect,
useRef,
useState,
type WheelEvent,
} from "react";
import { Badge } from "@/components/shadcn/badge/badge";
@@ -49,6 +50,10 @@ type MultiSelectContextType = {
};
const MultiSelectContext = createContext<MultiSelectContextType | null>(null);
const stopWheelPropagation = (event: WheelEvent<HTMLElement>) => {
event.stopPropagation();
};
export function MultiSelect({
children,
values,
@@ -335,12 +340,16 @@ export function MultiSelectContent({
<PopoverContent
align="start"
data-slot="multiselect-content"
style={{
maxHeight:
"min(360px, var(--radix-popover-content-available-height, 360px))",
}}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 rounded-lg border p-0",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 overflow-hidden rounded-lg border p-0",
widthClasses,
)}
>
<Command {...props} className="rounded-lg">
<Command {...props} className="max-h-[inherit] rounded-lg">
{canSearch ? (
<CommandInput
placeholder={
@@ -354,7 +363,12 @@ export function MultiSelectContent({
)}
<CommandList
ref={listRef}
className="minimal-scrollbar max-h-[300px] overflow-x-hidden overflow-y-auto p-3"
onWheelCapture={stopWheelPropagation}
style={{
maxHeight:
"min(300px, var(--radix-popover-content-available-height, 300px))",
}}
className="minimal-scrollbar overflow-x-hidden overflow-y-auto overscroll-contain p-3"
>
{canSearch && (
<CommandEmpty className="text-bg-button-secondary py-6 text-center text-sm">
@@ -0,0 +1,63 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./select";
Object.defineProperty(HTMLElement.prototype, "hasPointerCapture", {
writable: true,
configurable: true,
value: () => false,
});
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
writable: true,
configurable: true,
value: () => {},
});
describe("Select", () => {
it("keeps long option lists scrollable inside the dropdown", async () => {
// Given
const user = userEvent.setup();
render(
<Select defaultValue="option-1">
<SelectTrigger aria-label="Options">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 20 }, (_, index) => (
<SelectItem key={index} value={`option-${index}`}>
Option {index}
</SelectItem>
))}
</SelectContent>
</Select>,
);
// When
await user.click(screen.getByRole("combobox", { name: /options/i }));
// Then
const viewport = screen
.getByRole("listbox")
.querySelector('[data-slot="select-viewport"]');
expect(screen.getByRole("listbox")).toHaveStyle({
maxHeight: "var(--radix-select-content-available-height)",
});
expect(viewport).toHaveClass("minimal-scrollbar");
expect(viewport).toHaveStyle({
maxHeight:
"min(300px, var(--radix-select-content-available-height, 300px))",
});
expect(viewport).toHaveClass("overflow-y-auto");
expect(viewport).toHaveClass("overscroll-contain");
});
});
+19 -4
View File
@@ -2,10 +2,14 @@
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { ComponentProps } from "react";
import { ComponentProps, type WheelEvent } from "react";
import { cn } from "@/lib/utils";
const stopWheelPropagation = (event: WheelEvent<HTMLElement>) => {
event.stopPropagation();
};
function Select({
allowDeselect = false,
...props
@@ -83,6 +87,7 @@ function SelectContent({
position = "popper",
align = "start",
width = "default",
style,
...props
}: ComponentProps<typeof SelectPrimitive.Content> & {
width?: "default" | "wide";
@@ -97,22 +102,32 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-lg border",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
widthClasses,
className,
)}
style={{
maxHeight: "var(--radix-select-content-available-height)",
...style,
}}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-slot="select-viewport"
onWheelCapture={stopWheelPropagation}
style={{
maxHeight:
"min(300px, var(--radix-select-content-available-height, 300px))",
}}
className={cn(
"flex flex-col gap-1 p-3",
"minimal-scrollbar flex flex-col gap-1 overflow-x-hidden overflow-y-auto overscroll-contain p-3",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
"w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}