fix: add scroll support to Select dropdown for long lists

This commit is contained in:
silentoplayz 2026-06-02 07:54:51 -04:00
parent 1a97751e37
commit 3ce299f1c5
2 changed files with 55 additions and 3 deletions

View file

@ -25,6 +25,7 @@
let triggerEl;
let contentEl;
let contentMaxHeight = '';
/** Svelte action: moves the node to document.body */
function portal(node) {
@ -68,6 +69,22 @@
contentEl.style.position = 'fixed';
contentEl.style.zIndex = '9999';
// Find the trigger's nearest vertically-scrollable ancestor to use as boundary
let minTop = 8;
let scrollParent = triggerEl.parentElement;
while (scrollParent && scrollParent !== document.body) {
const style = getComputedStyle(scrollParent);
const oy = style.overflowY;
if (
(oy === 'auto' || oy === 'scroll') &&
scrollParent.scrollHeight > scrollParent.clientHeight
) {
minTop = scrollParent.getBoundingClientRect().top;
break;
}
scrollParent = scrollParent.parentElement;
}
const contentHeight = contentEl.offsetHeight || 0;
const spaceBelow = window.innerHeight - rect.bottom - sideOffset;
const spaceAbove = rect.top - sideOffset;
@ -83,9 +100,14 @@
if (openAbove) {
contentEl.style.bottom = `${window.innerHeight - rect.top + sideOffset}px`;
contentEl.style.top = 'auto';
const maxAbove = rect.top - minTop - sideOffset;
contentMaxHeight = `${maxAbove}px`;
} else {
contentEl.style.top = `${rect.bottom + sideOffset}px`;
const dropdownTop = Math.max(minTop, rect.bottom + sideOffset);
contentEl.style.top = `${dropdownTop}px`;
contentEl.style.bottom = 'auto';
const availableHeight = window.innerHeight - dropdownTop - 8;
contentMaxHeight = `${availableHeight}px`;
}
if (align === 'end') {
@ -187,6 +209,8 @@
bind:this={contentEl}
class={contentClass}
role="menu"
style:max-height={contentMaxHeight}
style:overflow-y="auto"
transition:flyAndScale
on:click={(e) => e.stopPropagation()}
on:pointerdown={(e) => e.stopPropagation()}

View file

@ -38,6 +38,7 @@
let triggerEl;
let contentEl;
let contentMaxHeight = '';
$: selectedLabel = items.find((i) => i.value === value)?.label ?? placeholder;
@ -59,9 +60,29 @@
contentEl.style.position = 'fixed';
contentEl.style.zIndex = '9999';
contentEl.style.top = `${rect.bottom + 4}px`;
contentEl.style.minWidth = `${rect.width}px`;
// Find the trigger's nearest vertically-scrollable ancestor and use its
// top edge as the minimum dropdown position so the dropdown never overlaps
// fixed headers above the scroll area (e.g. workspace nav bar)
let minTop = 8;
let parent = triggerEl.parentElement;
while (parent && parent !== document.body) {
const style = getComputedStyle(parent);
const oy = style.overflowY;
if ((oy === 'auto' || oy === 'scroll') && parent.scrollHeight > parent.clientHeight) {
minTop = parent.getBoundingClientRect().top;
break;
}
parent = parent.parentElement;
}
const dropdownTop = Math.max(minTop, rect.bottom + 4);
contentEl.style.top = `${dropdownTop}px`;
// Constrain dropdown height to available viewport space below the clamped top
const availableHeight = window.innerHeight - dropdownTop - 8;
contentMaxHeight = `${availableHeight}px`;
if (align === 'end') {
contentEl.style.right = `${window.innerWidth - rect.right}px`;
contentEl.style.left = 'auto';
@ -123,7 +144,14 @@
</button>
{#if open}
<div use:portal bind:this={contentEl} class={contentClass} transition:flyAndScale>
<div
use:portal
bind:this={contentEl}
class={contentClass}
style:max-height={contentMaxHeight}
style:overflow-y="auto"
transition:flyAndScale
>
<slot {open} {selectItem}>
{#each items as item}
<button class={itemClass} type="button" on:click={() => selectItem(item)}>