-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Combobox / Command not showing Command.Items correctly when source data changes #1562
Comments
A reproduction would help I am not able to reproduce this with the information you provided. Here is what I am trying to work off of but everything works fine. <script lang="ts">
import Check from 'lucide-svelte/icons/check';
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
import { tick } from 'svelte';
import * as Command from '$lib/components/ui/command/index.js';
import * as Popover from '$lib/components/ui/popover/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
const frameworks = [
{
value: 'sveltekit',
label: 'SvelteKit'
},
{
value: 'next.js',
label: 'Next.js'
},
{
value: 'nuxt.js',
label: 'Nuxt.js'
},
{
value: 'remix',
label: 'Remix'
},
{
value: 'astro',
label: 'Astro'
}
];
let open = $state(false);
let value = $state('');
let triggerRef = $state<HTMLButtonElement>(null!);
const selectedValue = $derived(frameworks.find((f) => f.value === value)?.label);
// We want to refocus the trigger button when the user selects
// an item from the list so users can continue navigating the
// rest of the form with the keyboard.
function closeAndFocusTrigger() {
open = false;
tick().then(() => {
triggerRef.focus();
});
}
const newItem = () => {
frameworks.push({ label: 'SolidJs', value: 'solid.js' });
};
</script>
<main class="flex h-svh flex-col place-items-center justify-center gap-4">
<Popover.Root bind:open>
<Popover.Trigger bind:ref={triggerRef}>
{#snippet child({ props })}
<Button
variant="outline"
class="w-[200px] justify-between"
{...props}
role="combobox"
aria-expanded={open}
>
{selectedValue || 'Select a framework...'}
<ChevronsUpDown class="opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[200px] p-0">
<Command.Root>
<Command.Input placeholder="Search framework..." />
<Command.List>
<Command.Empty>No framework found.</Command.Empty>
<Command.Group>
{#each frameworks as framework}
<Command.Item
value={framework.value}
onSelect={() => {
value = framework.value;
closeAndFocusTrigger();
}}
>
<Check class={cn(value !== framework.value && 'text-transparent')} />
{framework.label}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
<Button onclick={newItem}>Change items</Button>
</main>
|
<script lang="ts">
import Check from 'lucide-svelte/icons/check';
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
import X from 'lucide-svelte/icons/x';
import Loader2 from 'lucide-svelte/icons/loader-2';
import { tick } from 'svelte';
import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
type ScenarioItem = {
value: number;
label: string;
};
let {
noResultsText = 'Nessun risultato trovato',
path = '/api/frameworks',
placeholder = 'Cerca...',
loadingText = 'Caricamento...',
values = $bindable([] as number[])
} = $props();
let open = $state(false);
let loading = $state(false);
let options = $state<ScenarioItem[]>([]);
let searchTerm = $state('');
let selectedValues = $state<number[]>(values);
let debounceTimer: number;
$effect(() => {
values = selectedValues;
});
const selectedLabels = $derived(
options
.filter((f) => selectedValues.includes(f.value))
.map((f) => f.label)
);
async function fetchOptions(search: string) {
const params = new URLSearchParams({ search });
const response = await fetch(`${path}?${params}`);
if (!response.ok) throw new Error('Failed to fetch options');
return response.json();
}
async function handleSearch(value: string) {
searchTerm = value;
window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(async () => {
try {
console.log(searchTerm);
loading = true;
options = await fetchOptions(value);
console.log('options: ', options);
} catch (error) {
console.log('error');
console.error('Error fetching options:', error);
options = [];
} finally {
loading = false;
}
}, 300);
}
function toggleSelection(option: ScenarioItem) {
if (selectedValues.includes(option.value)) {
selectedValues = selectedValues.filter(v => v !== option.value);
} else {
selectedValues = [...selectedValues, option.value];
if (!options.some(o => o.value === option.value)) {
options = [...options, option];
}
}
}
function removeValue(valueToRemove: number) {
selectedValues = selectedValues.filter(v => v !== valueToRemove);
}
</script>
<Popover.Root bind:open>
<Popover.Trigger class="w-full">
<Button
variant="outline"
class="w-full h-auto justify-between"
role="combobox"
aria-expanded={open}
>
<div class="flex flex-wrap gap-1">
{#if selectedLabels.length > 0}
{#each selectedLabels as label, i (label)}
<div class="bg-secondary text-secondary-foreground flex items-center gap-1 rounded px-1 py-0.5">
<span class="text-sm">{label}</span>
<Button
type="button"
class="hover:bg-secondary/80 rounded-sm"
onclick={(e) =>{e.stopPropagation();
const optionToRemove = options.find(f => f.label === label)?.value;
if(optionToRemove){removeValue(optionToRemove)}}}
>
<X class="size-3" />
</Button>
</div>
{/each}
{:else}
<span class="text-muted-foreground">{placeholder}</span>
{/if}
</div>
<ChevronsUpDown class="opacity-50 shrink-0" />
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Command.Root>
<Command.Input
{placeholder}
oninput={(e)=>{handleSearch(e.currentTarget.value.length>0?e.currentTarget.value:'')}}
value={searchTerm}
/>
<Command.List>
{JSON.stringify(options)}
rendering list
<Command.Group>
{#each options as option}
<Command.Item>
{option.label}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root> I believe I am experiencing the same issue. In this code, the options are updated based on the handleSearch() call. Although I see the correct result in the JSON.stringify output, the Command.Item component exhibits strange behavior. When I replace Command.Item with a simple <div>, the list renders correctly. |
@davide-bleggi modifying your example a bit I am able to reproduce. It seems to be something with the search rendering behavior in the Reproduction: <script lang="ts">
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
import X from 'lucide-svelte/icons/x';
import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
type ScenarioItem = {
value: number;
label: string;
};
let { placeholder = 'Search...', values = $bindable([] as number[]) } = $props();
let open = $state(false);
let options = $state<ScenarioItem[]>([]);
let searchTerm = $state('');
let selectedValues = $state<number[]>(values);
let debounceTimer: number;
$effect(() => {
values = selectedValues;
});
const selectedLabels = $derived(
options.filter((f) => selectedValues.includes(f.value)).map((f) => f.label)
);
async function fetchOptions(): Promise<ScenarioItem[]> {
return [
{ label: 'test1', value: 1 },
{ label: 'test2', value: 2 },
{ label: 'test3', value: 3 }
];
}
async function handleSearch(value: string) {
searchTerm = value;
window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(async () => {
try {
console.log(searchTerm);
options = await fetchOptions();
console.log('options: ', options);
} catch (error) {
console.log('error');
console.error('Error fetching options:', error);
options = [];
}
}, 300);
}
function removeValue(valueToRemove: number) {
selectedValues = selectedValues.filter((v) => v !== valueToRemove);
}
</script>
<Popover.Root bind:open>
<Popover.Trigger class="w-full">
<Button
variant="outline"
class="h-auto w-full justify-between"
role="combobox"
aria-expanded={open}
>
<div class="flex flex-wrap gap-1">
{#if selectedLabels.length > 0}
{#each selectedLabels as label, i (label)}
<div
class="flex items-center gap-1 rounded bg-secondary px-1 py-0.5 text-secondary-foreground"
>
<span class="text-sm">{label}</span>
<Button
type="button"
class="rounded-sm hover:bg-secondary/80"
onclick={(e) => {
e.stopPropagation();
const optionToRemove = options.find((f) => f.label === label)?.value;
if (optionToRemove) {
removeValue(optionToRemove);
}
}}
>
<X class="size-3" />
</Button>
</div>
{/each}
{:else}
<span class="text-muted-foreground">{placeholder}</span>
{/if}
</div>
<ChevronsUpDown class="shrink-0 opacity-50" />
</Button>
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Command.Root>
<Command.Input
{placeholder}
oninput={(e) => {
handleSearch(e.currentTarget.value.length > 0 ? e.currentTarget.value : '');
}}
value={searchTerm}
/>
<Command.List>
<Command.Empty>No options found.</Command.Empty>
{JSON.stringify(options)}
<Command.Group>
{#each options as option (option.value)}
<Command.Item>
{option.label}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
|
For now a workaround for this can be setting |
Yeah, this does the trick! Thanks a lot! 😊 |
Describe the bug
Using the demo code for Combobox, if you change the values contained within the 'frameworks' array (by doing a fetch after typing some characters for example), the Command.Items will not display correctly. Oddly, you can see it make space for them but the text won't display even if I try changing the text colors. My actual use case is a stock search which waits for at least one letter to be typed before searching for a list of stock symbols starting with that letter. I can't get anything to display unless the data in the #each block is present when the component mounts.
Reproduction
I tried the above repo link but it builds a Svelte 4 site, which won't work. This should be very easy to replicate, just add a setInterval to add a frameworks object every 5 secs or something.
Logs
No response
System Info
Severity
annoyance
The text was updated successfully, but these errors were encountered: