mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user