Command onValueChange event fires every time the cursor is moved over an command item
Describe the bug
When implementing the Combobox example and adding onValueChange handler to the Command.Root, the event fires every time the cursor is moved over an command item, not when an item is actually selected. I would expect the event only to fire when the selection is actually changed.
Reproduction
See: https://stackblitz.com/edit/github-wuyshrsb?file=src%2Froutes%2F%2Bpage.svelte
<script lang="ts">
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 selected=$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();
});
}
function valueChange(value:string){
console.log(value);
selected=value;
}
</script>
<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="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[200px] p-0">
<Command.Root onValueChange={valueChange}>
<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(
"mr-2 size-4",
value !== framework.value && "text-transparent"
)}
/>
{framework.label}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
{selected}
Logs
System Info
System:
OS: Windows 11 10.0.26100
CPU: (16) x64 AMD Ryzen 7 PRO 4750U with Radeon Graphics
Memory: 14.22 GB / 31.23 GB
Binaries:
Node: 20.15.1 - C:\nvm4w\nodejs\node.EXE
npm: 10.8.2 - C:\nvm4w\nodejs\npm.CMD
pnpm: 10.6.3 - ~\AppData\Local\pnpm\pnpm.CMD
Browsers:
Edge: Chromium (131.0.2903.112)
Internet Explorer: 11.0.26100.1882
npmPackages:
bits-ui: ^1.3.19 => 1.3.19
svelte: latest => 5.27.0
tailwindcss: ^3.4.17 => 3.4.17
Severity
annoyance
I'm not sure if this is the intended behavior in bits-ui or not.
But I normally don't use onValueChange on the Root element. Normally I would use onSelect on each individual item and handle it there.
From the api reference
- Change Handler To perform additional logic on state changes, use the onValueChange prop. This approach is useful when you need to execute side effects when the value changes.
<script lang="ts">
import { Command } from "bits-ui";
</script>
<Command.Root
onValueChange={(value) => {
// do something with the new value
console.log(value);
}}
>
<!-- ... -->
</Command.Root>
Reading this i got the impression that binding to the Root is encouraged.
Yeah again I am not sure if it's the intended behavior but it I would encourage anyone to use onSelect on each item because I haven't really ever needed to use onValueChange.
Also bits-ui is at 1.0.0 so no need to go to next.bits-ui.com.
Fair enough. next.bits-ui.com is where the href on the api reference button is pointing to on https://next.shadcn-svelte.com/docs/components/command
Fixed in bits-ui@latest.