ui/search: add loading state

This commit is contained in:
Johannes Kirschbauer
2025-08-26 17:06:55 +02:00
parent 24c5146763
commit 53e16242b9
2 changed files with 73 additions and 35 deletions

View File

@@ -117,6 +117,27 @@ export const Default: Story = {
}, },
}; };
export const Loading: Story = {
args: {
// Test with lots of modules
loading: true,
options: [],
renderItem: () => <span></span>,
},
render: (args: SearchProps<Module>) => {
return (
<div class="absolute bottom-1/3 w-3/4 px-3">
<Search<Module>
{...args}
onChange={(module) => {
// Go to the module configuration
}}
/>
</div>
);
},
};
type MachineOrTag = type MachineOrTag =
| { | {
value: string; value: string;

View File

@@ -2,9 +2,10 @@ import Icon from "../Icon/Icon";
import { Button } from "../Button/Button"; import { Button } from "../Button/Button";
import styles from "./Search.module.css"; import styles from "./Search.module.css";
import { Combobox } from "@kobalte/core/combobox"; import { Combobox } from "@kobalte/core/combobox";
import { createMemo, createSignal, For, JSX } from "solid-js"; import { createMemo, createSignal, For, JSX, Match, Switch } from "solid-js";
import { createVirtualizer } from "@tanstack/solid-virtual"; import { createVirtualizer } from "@tanstack/solid-virtual";
import { CollectionNode } from "@kobalte/core/*"; import { CollectionNode } from "@kobalte/core/*";
import { Loader } from "../Loader/Loader";
export interface Option { export interface Option {
value: string; value: string;
@@ -15,6 +16,8 @@ export interface SearchProps<T> {
onChange: (value: T | null) => void; onChange: (value: T | null) => void;
options: T[]; options: T[];
renderItem: (item: T) => JSX.Element; renderItem: (item: T) => JSX.Element;
loading?: boolean;
loadingComponent?: JSX.Element;
} }
export function Search<T extends Option>(props: SearchProps<T>) { export function Search<T extends Option>(props: SearchProps<T>) {
// Controlled input value, to allow resetting the input itself // Controlled input value, to allow resetting the input itself
@@ -136,41 +139,55 @@ export function Search<T extends Option>(props: SearchProps<T>) {
setComboboxItems(arr); setComboboxItems(arr);
return ( return (
<div <Switch>
style={{ <Match when={props.loading}>
height: `${virtualizer().getTotalSize()}px`, {props.loadingComponent ?? (
width: "100%", <div class="flex w-full justify-center py-2">
position: "relative", <Loader />
}} </div>
> )}
<For each={virtualizer().getVirtualItems()}> </Match>
{(virtualRow) => { <Match when={!props.loading}>
const item: CollectionNode<T> | undefined = <div
items().getItem(virtualRow.key as string); style={{
height: `${virtualizer().getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
<For each={virtualizer().getVirtualItems()}>
{(virtualRow) => {
const item: CollectionNode<T> | undefined =
items().getItem(virtualRow.key as string);
if (!item) { if (!item) {
console.warn("Item not found for key:", virtualRow.key); console.warn(
return null; "Item not found for key:",
} virtualRow.key,
return ( );
<Combobox.Item return null;
item={item} }
class={styles.searchItem} return (
style={{ <Combobox.Item
position: "absolute", item={item}
top: 0, class={styles.searchItem}
left: 0, style={{
width: "100%", position: "absolute",
height: `${virtualRow.size}px`, top: 0,
transform: `translateY(${virtualRow.start}px)`, left: 0,
}} width: "100%",
> height: `${virtualRow.size}px`,
{props.renderItem(item.rawValue)} transform: `translateY(${virtualRow.start}px)`,
</Combobox.Item> }}
); >
}} {props.renderItem(item.rawValue)}
</For> </Combobox.Item>
</div> );
}}
</For>
</div>
</Match>
</Switch>
); );
}} }}
</Combobox.Listbox> </Combobox.Listbox>