diff --git a/e2e/selectPopupLayout.spec.ts b/e2e/selectPopupLayout.spec.ts new file mode 100644 index 000000000..81a941d34 --- /dev/null +++ b/e2e/selectPopupLayout.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from "./fixtures"; + +for (const [id, key, name] of [ + ["languages.node", "languages.node.interpreter", "Node interpreter"], + ["languages.java", "languages.java.jdk", "Project JDK"], + ["languages.php", "languages.php.interpreter", "PHP interpreter"], +]) { + test(`${name} dropdown keeps long custom paths inside the viewport`, async ({ workbench: page }) => { + await page.keyboard.press("Meta+,"); + await expect(page.getByRole("dialog", { name: "Settings", exact: true })).toBeVisible(); + const value = "/project/" + "long-path/".repeat(30) + "runtime"; + await page.evaluate( + async ({ id, key, value }) => { + const path = "/src/features/settings/model/staging.ts"; + const { selectPage, stage } = await import(/* @vite-ignore */ path); + stage(id, "project", key, value); + selectPage(id); + }, + { id, key, value }, + ); + const combo = page.getByRole("button", { name, exact: true }); + for (const width of [1000, 640, 520]) { + await page.setViewportSize({ width, height: 760 }); + await combo.click(); + const list = page.getByRole("listbox", { name, exact: true }); + await expect(list).toBeVisible(); + await expect + .poll(() => + list.evaluate((list) => { + const popup = list.closest("[data-popup]")!; + const r = popup.getBoundingClientRect(); + return r.left >= 0 && r.right <= innerWidth && list.scrollWidth <= list.clientWidth + 1; + }), + ) + .toBe(true); + const option = list.getByRole("option").filter({ hasText: value }); + await expect(option).toBeVisible(); + await expect(option.locator("[title]").first()).toHaveAttribute("title", value); + await option.click(); + await expect(list).toHaveCount(0); + await expect(combo).toHaveText(value); + } + }); +} diff --git a/src/ui/Popup.tsx b/src/ui/Popup.tsx index 31bde957f..8c4910bc0 100644 --- a/src/ui/Popup.tsx +++ b/src/ui/Popup.tsx @@ -96,9 +96,11 @@ function PopupInner({ shift({ padding: 6 }), sizeMw({ padding: 6, - apply({ rects, elements, availableHeight }) { + apply({ rects, elements, availableHeight, availableWidth }) { elements.floating.style.maxHeight = `${Math.max(120, availableHeight)}px`; - if (matchWidth) elements.floating.style.minWidth = `${rects.reference.width}px`; + const width = Math.max(0, availableWidth); + elements.floating.style.maxWidth = `${width}px`; + if (matchWidth) elements.floating.style.minWidth = `${Math.min(rects.reference.width, width)}px`; }, }), ], diff --git a/src/ui/Select.module.css b/src/ui/Select.module.css index 06ce2e544..c3e155739 100644 --- a/src/ui/Select.module.css +++ b/src/ui/Select.module.css @@ -91,7 +91,8 @@ overflow: hidden; } .optionLabel { - flex: 1 0 auto; + flex: 1 1 auto; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/src/ui/Select.tsx b/src/ui/Select.tsx index 5e1cf117c..554fe9c33 100644 --- a/src/ui/Select.tsx +++ b/src/ui/Select.tsx @@ -268,7 +268,9 @@ export function Select({ > {o.value === value && } {o.icon && } - {given(o.label)} + + {given(o.label)} + {o.detail && ( {given(o.detail)}