Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

187 lines
4.5 KiB
Svelte

<script lang="ts">
import { fly } from "svelte/transition";
let {
choices,
filtered_indices,
show_options = false,
disabled = false,
selected_indices = [],
active_index = null,
remember_scroll = false,
offset_from_top = 0,
from_top = false,
listbox_id = undefined,
onchange,
onload
}: {
choices: [string, string | number][];
filtered_indices: number[];
show_options: boolean;
disabled?: boolean;
selected_indices?: (string | number)[];
active_index: number | null;
remember_scroll?: boolean;
offset_from_top?: number;
from_top?: boolean;
listbox_id?: string;
onchange?: (index: any) => void;
onload?: () => void;
} = $props();
let distance_from_top = $state(0);
let distance_from_bottom = $state(0);
let input_height = $state(0);
let input_width = $state(0);
let refElement: HTMLDivElement;
let listElement: HTMLUListElement;
let top: string | null = $state(null);
let bottom: string | null = $state(null);
let max_height: number = $state(0);
let innerHeight = $state(0);
let list_scroll_y = 0;
function calculate_window_distance(): void {
const { top: ref_top, bottom: ref_bottom } =
refElement.getBoundingClientRect();
if (from_top) {
distance_from_top = offset_from_top;
} else {
distance_from_top = ref_top;
}
distance_from_bottom = innerHeight - ref_bottom;
}
let scroll_timeout: NodeJS.Timeout | null = null;
function scroll_listener(): void {
if (!show_options) return;
if (scroll_timeout !== null) {
clearTimeout(scroll_timeout);
}
scroll_timeout = setTimeout(() => {
calculate_window_distance();
scroll_timeout = null;
}, 10);
}
function restore_last_scroll(): void {
listElement?.scrollTo?.(0, list_scroll_y);
}
$effect(() => {
if (show_options && refElement) {
if (remember_scroll) {
restore_last_scroll();
} else {
if (listElement && selected_indices.length > 0) {
let elements = listElement.querySelectorAll("li");
for (const element of Array.from(elements)) {
if (
element.getAttribute("data-index") ===
selected_indices[0].toString()
) {
listElement?.scrollTo?.(0, (element as HTMLLIElement).offsetTop);
break;
}
}
}
}
calculate_window_distance();
const rect = refElement.parentElement?.getBoundingClientRect();
input_height = rect?.height || 0;
input_width = rect?.width || 0;
onload?.();
}
if (distance_from_bottom > distance_from_top || from_top) {
top = `${distance_from_top}px`;
max_height = distance_from_bottom;
bottom = null;
} else {
bottom = `${distance_from_bottom + input_height}px`;
max_height = distance_from_top - input_height;
top = null;
}
});
</script>
<svelte:window on:scroll={scroll_listener} bind:innerHeight />
<div class="reference" bind:this={refElement} />
{#if show_options && !disabled}
<ul
class="options"
transition:fly={{ duration: 200, y: 5 }}
onmousedown={(e) => {
e.preventDefault();
onchange?.((e.target as HTMLElement).dataset.index);
}}
onscroll={(e) => (list_scroll_y = e.currentTarget.scrollTop)}
style:top
style:bottom
style:max-height={`calc(${max_height}px - var(--window-padding))`}
style:width={input_width + "px"}
bind:this={listElement}
id={listbox_id}
role="listbox"
>
{#each filtered_indices as index}
<li
class="item"
class:selected={selected_indices.includes(index)}
class:active={index === active_index}
class:bg-gray-100={index === active_index}
class:dark:bg-gray-600={index === active_index}
style:width={input_width + "px"}
data-index={index}
id={listbox_id ? `${listbox_id}-option-${index}` : undefined}
aria-label={choices[index][0]}
data-testid="dropdown-option"
role="option"
aria-selected={selected_indices.includes(index)}
>
<span class:hide={!selected_indices.includes(index)} class="inner-item">
</span>
{choices[index][0]}
</li>
{/each}
</ul>
{/if}
<style>
.options {
--window-padding: var(--size-8);
position: fixed;
z-index: var(--layer-top);
margin-left: 0;
box-shadow: var(--shadow-drop-lg);
border-radius: var(--container-radius);
background: var(--background-fill-primary);
min-width: fit-content;
max-width: inherit;
overflow: auto;
color: var(--body-text-color);
list-style: none;
}
.item {
display: flex;
cursor: pointer;
padding: var(--size-2);
word-break: break-word;
}
.item:hover,
.active {
background: var(--background-fill-secondary);
}
.inner-item {
padding-right: var(--size-1);
}
.hide {
visibility: hidden;
}
</style>