From 2fdc480beb6a4c2bee1650b1191d72bdf2eb7131 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Tue, 12 May 2026 13:21:18 +0200 Subject: [PATCH] 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 --- ui/CHANGELOG.md | 9 +++ .../workflow/forms/add-role-form.test.tsx | 20 +++++- .../roles/workflow/forms/add-role-form.tsx | 6 +- .../roles/workflow/forms/edit-role-form.tsx | 6 +- .../shadcn/select/multiselect.test.tsx | 40 ++++++++++++ ui/components/shadcn/select/multiselect.tsx | 20 +++++- ui/components/shadcn/select/select.test.tsx | 63 +++++++++++++++++++ ui/components/shadcn/select/select.tsx | 23 +++++-- 8 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 ui/components/shadcn/select/select.test.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index b2947b6d8e..89970264c0 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -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 diff --git a/ui/components/roles/workflow/forms/add-role-form.test.tsx b/ui/components/roles/workflow/forms/add-role-form.test.tsx index b7686635a4..80a9c523b3 100644 --- a/ui/components/roles/workflow/forms/add-role-form.test.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.test.tsx @@ -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(); + + // When + await user.click(screen.getByRole("button", { name: /cancel/i })); + + // Then + expect(routerMocks.push).toHaveBeenCalledWith("/roles"); + }); }); diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx index ccd32bf031..2ae5db944d 100644 --- a/ui/components/roles/workflow/forms/add-role-form.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.tsx @@ -252,7 +252,11 @@ export const AddRoleForm = ({ )} )} - + router.push("/roles")} + /> ); diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx index 4c881d6f98..e3e1a3bde8 100644 --- a/ui/components/roles/workflow/forms/edit-role-form.tsx +++ b/ui/components/roles/workflow/forms/edit-role-form.tsx @@ -271,7 +271,11 @@ export const EditRoleForm = ({ )} )} - + router.push("/roles")} + /> ); diff --git a/ui/components/shadcn/select/multiselect.test.tsx b/ui/components/shadcn/select/multiselect.test.tsx index 18a8821be6..6f649d34d5 100644 --- a/ui/components/shadcn/select/multiselect.test.tsx +++ b/ui/components/shadcn/select/multiselect.test.tsx @@ -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( + {}}> + + + + + {Array.from({ length: 20 }, (_, index) => ( + + Account {index} + + ))} + + , + ); + + // 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(); diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx index caf39437ca..a9e1544935 100644 --- a/ui/components/shadcn/select/multiselect.tsx +++ b/ui/components/shadcn/select/multiselect.tsx @@ -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(null); +const stopWheelPropagation = (event: WheelEvent) => { + event.stopPropagation(); +}; + export function MultiSelect({ children, values, @@ -335,12 +340,16 @@ export function MultiSelectContent({ - + {canSearch ? ( {canSearch && ( diff --git a/ui/components/shadcn/select/select.test.tsx b/ui/components/shadcn/select/select.test.tsx new file mode 100644 index 0000000000..10fe8ec490 --- /dev/null +++ b/ui/components/shadcn/select/select.test.tsx @@ -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( + , + ); + + // 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"); + }); +}); diff --git a/ui/components/shadcn/select/select.tsx b/ui/components/shadcn/select/select.tsx index 73bdfbae28..06face8201 100644 --- a/ui/components/shadcn/select/select.tsx +++ b/ui/components/shadcn/select/select.tsx @@ -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) => { + event.stopPropagation(); +}; + function Select({ allowDeselect = false, ...props @@ -83,6 +87,7 @@ function SelectContent({ position = "popper", align = "start", width = "default", + style, ...props }: ComponentProps & { width?: "default" | "wide"; @@ -97,22 +102,32 @@ function SelectContent({ {children}