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
30 changes: 27 additions & 3 deletions packages/ui/src/lib/components/select/Select.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,36 @@
),
);

// Rendering every option eagerly freezes the app when the list is huge
// (e.g. selecting a target branch in a repo with tens of thousands of
// remote branches), so render in chunks that grow as the list scrolls.
const RENDER_CHUNK = 100;
const RENDER_MORE_THRESHOLD = 200; // px left to scroll before rendering more
let renderLimit = $state(RENDER_CHUNK);
const renderedOptions = $derived(filteredOptions.slice(0, renderLimit));

let lastSearchValue = untrack(() => searchValue);
$effect(() => {
if (searchValue !== lastSearchValue) {
lastSearchValue = searchValue;
renderLimit = RENDER_CHUNK;
}
});

function renderMoreOnScroll(e: Event) {
if (renderLimit >= filteredOptions.length) return;
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLElement;
if (scrollHeight - scrollTop - clientHeight < RENDER_MORE_THRESHOLD) {
renderLimit += RENDER_CHUNK;
}
}

// Group options by separators
const groupedOptions = $derived.by(() => {
const groups: SelectItem<T>[][] = [];
let currentGroup: SelectItem<T>[] = [];

for (const option of filteredOptions) {
for (const option of renderedOptions) {
if (option.separator) {
if (currentGroup.length > 0) {
groups.push(currentGroup);
Expand All @@ -118,7 +142,7 @@

// Flatten grouped options for navigation while preserving order
const selectableOptions = $derived.by(
() => filteredOptions.filter((item) => !item.separator) as SelectItem<T>[],
() => renderedOptions.filter((item) => !item.separator) as SelectItem<T>[],
);

// Auto-highlight first option when search results change, reset when search is cleared
Expand Down Expand Up @@ -379,7 +403,7 @@
tabindex="-1"
onkeydown={(ev: KeyboardEvent) => handleKeyDown(ev)}
>
<ScrollableContainer whenToShow="scroll">
<ScrollableContainer whenToShow="scroll" onscroll={renderMoreOnScroll}>
{#if searchable && options.length > 5}
<SearchItem bind:this={searchItemEl} bind:searchValue />
{/if}
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/stories/components/Select.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@
argTypes: {},
});

const hugeOptions = Array.from({ length: 25000 }, (_, i) => ({
value: `branch-${i}`,
label: `origin/feature/branch-${i}`,
}));

let selectedItem = $state<string>("1");
let selectedWithIcon = $state<string>("js");
let selectedWithEmoji = $state<string>("happy");
let selectedLongOption = $state<string>();
let selectedWithSeparators = $state<string>("new");
let selectedHugeOption = $state<string>();
let customBtnOpen = $state(false);
</script>

Expand Down Expand Up @@ -111,6 +117,27 @@
{/snippet}
</Story>

<Story name="Huge List">
{#snippet template(_args)}
<div class="wrap">
<Select
searchable
options={hugeOptions}
value={selectedHugeOption}
onselect={(value: string) => {
selectedHugeOption = value;
}}
>
{#snippet itemSnippet({ item, highlighted })}
<SelectItem selected={item.value === selectedHugeOption} {highlighted}>
{item.label}
</SelectItem>
{/snippet}
</Select>
</div>
{/snippet}
</Story>

<Story name="Custom button">
{#snippet template(args)}
<div class="wrap">
Expand Down
Loading