fix: language dropdown overflow in settings modal (#281) (#287)

* fix: language dropdown overflow in settings modal (#281)

Smart-place the Settings language menu: open downward when there is enough space below the trigger, otherwise fall back to opening upward. Expose the available height to CSS via --menu-available-h so the menu is bounded in either direction, scrolls internally, and prevents long labels from overflowing item rows.

* fix(SettingsDialog): address PR review feedback for #281

- Document the 200px threshold used to prefer downward placement.
- Close the language menu on window resize so the placement (computed once at open) cannot become stale.
- Cap the menu max-height at 60vh so it stays comfortable on short viewports (mobile landscape ~400-500px tall).
This commit is contained in:
Sina Mashini 2026-05-02 15:21:26 +03:30 committed by GitHub
parent 0c00f241e7
commit 6805628458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 7 deletions

View file

@ -103,6 +103,15 @@ export function SettingsDialog({
};
}, [languageOpen]);
// Close the language menu on window resize so its placement (computed on
// open) cannot end up stale relative to the new viewport dimensions.
useEffect(() => {
if (!languageOpen) return;
const handleResize = () => setLanguageOpen(false);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [languageOpen]);
const installedCount = useMemo(
() => agents.filter((a) => a.available).length,
[agents],
@ -531,15 +540,23 @@ export function SettingsDialog({
</span>
<Icon name="chevron-down" size={16} />
</button>
{languageOpen && languageMenuRect ? (
{languageOpen && languageMenuRect ? (() => {
const spaceBelow = window.innerHeight - languageMenuRect.bottom;
const spaceAbove = languageMenuRect.top;
// Prefer downward if at least 200px available (enough for ~5 options)
const openDownward = spaceBelow >= spaceAbove || spaceBelow >= 200;
return (
<div
className="settings-language-menu"
role="menu"
style={{
bottom: window.innerHeight - languageMenuRect.top + 6,
top: openDownward ? languageMenuRect.bottom + 6 : undefined,
bottom: openDownward
? undefined
: window.innerHeight - languageMenuRect.top + 6,
left: languageMenuRect.left,
width: languageMenuRect.width,
'--menu-available-h': `${languageMenuRect.top - 6}px`,
'--menu-available-h': `${(openDownward ? spaceBelow : spaceAbove) - 6}px`,
} as React.CSSProperties}
>
{LOCALES.map((code) => {
@ -569,7 +586,8 @@ export function SettingsDialog({
);
})}
</div>
) : null}
);
})() : null}
</div>
</section>
) : null}

View file

@ -1355,9 +1355,12 @@ code {
z-index: 1000;
max-width: calc(100vw - 24px);
/* 360px = 9 locales × ~40px; --menu-available-h is set by JS to the
distance between the trigger and the viewport top, so the menu never
overflows above the viewport regardless of trigger placement. */
max-height: min(360px, var(--menu-available-h, 360px));
distance between the trigger and whichever viewport edge the menu
opens toward, so the menu never overflows the viewport regardless of
placement direction (above or below the trigger). The 60vh cap keeps
the menu from feeling cramped on short viewports (mobile landscape,
~400500px tall). */
max-height: min(360px, 60vh, var(--menu-available-h, 360px));
overflow-y: auto;
overscroll-behavior: contain;
display: flex;
@ -1375,6 +1378,7 @@ code {
align-items: center;
gap: 12px;
width: 100%;
min-height: 40px;
padding: 10px 12px;
border: 0;
border-radius: 9px;