Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ProjectFilter: React.FC<ProjectFilterProps> = ({
</button>

{isOpen && (
<div className="absolute top-full start-0 z-50 mt-2 max-h-96 w-72 overflow-y-auto rounded-xl border border-border/50 bg-popover overflow-hidden">
<div className="absolute top-full start-0 z-50 mt-2 max-h-96 w-72 overflow-y-auto rounded-xl border border-border/50 bg-popover">
{/* All Projects Option */}
<button
onClick={() => {
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/ui/CustomSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function CustomSelect<T extends string>({
<div
ref={menuRef}
role="listbox"
className="fixed z-[10050] overflow-hidden rounded-2xl border border-border/50 bg-popover shadow-xl shadow-black/[0.08]"
className="fixed z-[10050] flex flex-col overflow-hidden rounded-2xl border border-border/50 bg-popover shadow-xl shadow-black/[0.08]"
style={{
left: menuPosition.left,
width: menuPosition.width,
Expand All @@ -171,7 +171,7 @@ export function CustomSelect<T extends string>({
: { bottom: menuPosition.bottom }),
}}
>
<div className="max-h-full overflow-y-auto py-1.5">
<div className="min-h-0 flex-1 overflow-y-auto py-1.5">
{options.map((option) => {
const isSelected = option.value === value;
return (
Expand Down
70 changes: 70 additions & 0 deletions apps/dashboard/src/components/ui/custom-select.render.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { renderToStaticMarkup } from "react-dom/server";
import { CustomSelect } from "./CustomSelect";

/**
* Regression test for Supabase / database connection dropdown scrolling bug.
*
* Problem:
* When connecting a Supabase database to a project (via "Use in a project" modal),
* the target project dropdown menu could not be scrolled when there were many projects.
* The portal outer container had `overflow-hidden` and `maxHeight`, but lacked
* `flex flex-col`, causing the child `max-h-full` to fail resolving against parent
* indefinite height (`height: auto`). The inner container expanded to full content
* height, preventing `overflow-y-auto` from activating, while the outer container
* clipped the content at `maxHeight` (e.g. 256px).
*
* Fix:
* 1. Outer container has `flex flex-col overflow-hidden` with `maxHeight`.
* 2. Inner container has `min-h-0 flex-1 overflow-y-auto` so flexbox constrains
* the inner container to available height and properly activates vertical scrolling.
*/
describe("CustomSelect — scrollable dropdown structure", () => {
const options = Array.from({ length: 25 }, (_, i) => ({
value: `proj-${i + 1}`,
label: `Project ${i + 1}`,
description: `project-${i + 1}.example.com`,
}));

it("renders trigger button with placeholder when unselected", () => {
const html = renderToStaticMarkup(
<CustomSelect
value=""
options={options}
onChange={() => {}}
placeholder="Select a project…"
/>,
);

expect(html).toContain("Select a project…");
expect(html).toContain('aria-haspopup="listbox"');
expect(html).toContain('aria-expanded="false"');
});

it("renders selected option label and description when value matches", () => {
const html = renderToStaticMarkup(
<CustomSelect
value="proj-3"
options={options}
onChange={() => {}}
placeholder="Select a project…"
/>,
);

expect(html).toContain("Project 3");
expect(html).toContain("project-3.example.com");
});

it("ensures CustomSelect source code uses flex-col on outer menu and min-h-0 flex-1 overflow-y-auto on inner list", () => {
const sourcePath = resolve(__dirname, "CustomSelect.tsx");
const source = readFileSync(sourcePath, "utf-8");

// Outer portal container must be flex flex-col to bound children in flex layout
expect(source).toMatch(/className="[^"]*flex flex-col overflow-hidden[^"]*bg-popover/);

// Inner options list must have min-h-0 flex-1 overflow-y-auto to scroll when content exceeds max-height
expect(source).toMatch(/className="[^"]*min-h-0 flex-1 overflow-y-auto[^"]*"/);
});
});