From 87685d2baf29efdc1856b19a45e89d24aeaefc51 Mon Sep 17 00:00:00 2001 From: MaryWylde Date: Mon, 14 Sep 2026 20:10:38 +0200 Subject: [PATCH] fix(library): let long shelf names breathe in the Move To menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The portaled menu was pinned to its trigger's width, and the Move To trigger is a compact fit-content pill — so shelf names were crushed into a ~90px column, and a name with no break opportunity ("Crime/Detective") walked straight out of the menu and over the modal behind it. The portaled menu now sizes to its content: min-width keeps it at least as wide as the trigger, max-width keeps it inside the viewport and off the far edge of the screen. Option labels break long tokens instead of escaping the box, and the trigger truncates its own label so a chosen shelf name can't push the pill past the modal. Co-Authored-By: Claude Opus 5 --- .../molecules/Dropdown/Dropdown.module.scss | 20 +++++++++++++++++++ .../library/molecules/Dropdown/Dropdown.tsx | 18 ++++++++++++++--- .../ObjectOverviewModal.module.scss | 3 +++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/components/library/molecules/Dropdown/Dropdown.module.scss b/src/components/library/molecules/Dropdown/Dropdown.module.scss index 6ae6fdc6..283cac88 100644 --- a/src/components/library/molecules/Dropdown/Dropdown.module.scss +++ b/src/components/library/molecules/Dropdown/Dropdown.module.scss @@ -42,6 +42,12 @@ .text { flex: 1; + // The trigger is a fixed-height row, so a long selected label has to + // truncate — wrapping would spill out of the bottom of the control. + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; text-align: left; } @@ -98,6 +104,12 @@ position: fixed; right: auto; z-index: 1500; + // `.menu` is `width: 100%`, which under `position: fixed` resolves against + // the viewport. Size to the longest option instead; the inline min-width + // keeps the menu at least as wide as the trigger (compact pill triggers + // would otherwise crush option labels into a one-word-per-line column) and + // the inline max-width keeps it inside the viewport. + width: max-content; } .optionWrapper { @@ -138,6 +150,14 @@ } } +// Shelf and library names can be a single long token ("Crime/Detective"), and +// the menu is `overflow: visible` so an unbroken one paints over whatever sits +// behind the dropdown. Break it instead. +.optionLabel { + min-width: 0; + overflow-wrap: anywhere; +} + .subArrow { flex-shrink: 0; transform: rotate(0deg); diff --git a/src/components/library/molecules/Dropdown/Dropdown.tsx b/src/components/library/molecules/Dropdown/Dropdown.tsx index 80330446..647272b1 100644 --- a/src/components/library/molecules/Dropdown/Dropdown.tsx +++ b/src/components/library/molecules/Dropdown/Dropdown.tsx @@ -112,7 +112,11 @@ export function Dropdown(props: DropdownProps): JSX.Element { ? { top: menuPos.top, left: menuPos.left, - width: menuPos.width, + // Never narrower than the trigger, never wider than the room + // left of the viewport's right edge, and capped so a long shelf + // name can't stretch the menu across the whole screen. + minWidth: menuPos.width, + maxWidth: `min(320px, calc(100vw - ${menuPos.left + 8}px))`, transform: menuPos.placement === 'top' ? 'translateY(-100%)' : undefined, } @@ -167,7 +171,12 @@ export function Dropdown(props: DropdownProps): JSX.Element { ) : ( - {option.label} + + {option.label} + )} {hasSubOptions ? ( handleSubSelect(sub.value))} aria-label={`Select ${sub.label}`} > - + {sub.label} {value === sub.value && ( diff --git a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.module.scss b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.module.scss index 6469c808..4bdc0fb5 100644 --- a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.module.scss +++ b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.module.scss @@ -363,6 +363,9 @@ // the Shelf settings-trigger override). Compact pill, not a full-width field. .moveToTrigger { width: fit-content !important; + // The pill hugs its label, so a long shelf name would push it past the + // modal's edge; cap it and let the trigger's own ellipsis do the rest. + max-width: 100%; padding: 7px 12px !important; background: var(--white) !important; border: 1px solid var(--beige) !important;