cmdk icon indicating copy to clipboard operation
cmdk copied to clipboard

[Bug] The List scrolls to a stale top item when search changes

Open zamuka opened this issue 2 months ago • 2 comments

When you change the search search string and this change makes more search results then the list scrolls to the previous top item instead of the current one.

I suppose it happens because store.setState('search', props.value) is followed by scrollSelectedIntoView... and inside getSelectedItem search for [aria-selected="true"] in not yet updated dom.

When the search string is changed from cc to cccc - the new top item is "Cascade Interactive" When the search is changed back from cccc to cc - the new top item is "Echo Networks" and it is selected BUT List scrolls to Cascade Interactive

https://github.com/user-attachments/assets/f7837b05-dbc1-44ab-b9ea-49cf5549a6b5

here is a test data

const organizations = [ { id: 1, name: "Acme Innovations" }, { id: 2, name: "BlueSky Technologies" }, { id: 3, name: "Quantum Dynamics" }, { id: 4, name: "Aurora Systems" }, { id: 5, name: "Nimbus Labs" }, { id: 6, name: "Vertex Analytics" }, { id: 7, name: "Solaris Consulting" }, { id: 8, name: "Evergreen Solutions" }, { id: 9, name: "Summit Digital" }, { id: 10, name: "Atlas Ventures" }, { id: 11, name: "NeonSoft" }, { id: 12, name: "CorePulse" }, { id: 13, name: "Zenith Global" }, { id: 14, name: "Ironclad Industries" }, { id: 15, name: "Frontier Robotics" }, { id: 16, name: "Lighthouse Data" }, { id: 17, name: "Horizon Partners" }, { id: 18, name: "NextGen BioWorks" }, { id: 19, name: "Cascade Interactive" }, { id: 20, name: "Echo Networks" }, { id: 21, name: "Velocity Ventures" }, { id: 22, name: "PolarEdge" }, { id: 23, name: "SummitOne Group" }, { id: 24, name: "Cobalt Consulting" }, { id: 25, name: "Helix Cloud" }, { id: 26, name: "NovaLink Systems" }, { id: 27, name: "TrueNorth Media" }, { id: 28, name: "BrightPath AI" }, { id: 29, name: "EchoBase Security" }, { id: 30, name: "TerraWorks" }, ];

zamuka avatar Oct 28 '25 10:10 zamuka

The workaround for this is to manually scroll to top on value change

<CommandInput placeholder="Search organization..." onValueChange={() => { setTimeout(() => { commandListRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); }); }} /> <CommandList ref={commandListRef} > ....

zamuka avatar Oct 28 '25 10:10 zamuka

Facing this same issue, can confirm the workaround above works! Hoping for a native fix in the future 🤞. For anyone using shadcn/ui here's how I implemented this workaround in command.tsx

const CommandContext = React.createContext<{
  listRef: React.RefObject<HTMLDivElement | null>;
}>({ listRef: { current: null } });

// ...

function Command({ className, ...props }: CommandProps) {
  const listRef = React.useRef<HTMLDivElement | null>(null);
  return (
    <CommandContext.Provider value={{ listRef }}>
      <CommandPrimitive
        data-slot="command"
        className={cn(
          'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
          className
        )}
        {...props}
      />
    </CommandContext.Provider>
  );
}

// ...

function CommandInput({
  className,
  onValueChange,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
  const { listRef } = React.useContext(CommandContext);
  return (
    <div
      data-slot="command-input-wrapper"
      className="flex h-9 items-center gap-2 border-b px-2"
    >
      <SearchIcon className="size-4 shrink-0 opacity-50" />
      <CommandPrimitive.Input
        data-slot="command-input"
        className={cn(
          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
          className
        )}
        onValueChange={(value) => {
          onValueChange?.(value);
          setTimeout(() => {
            listRef.current?.scrollTo({ top: 0, behavior: 'instant' });
          });
        }}
        {...props}
      />
    </div>
  );
}

// ...

function CommandList({
  className,
  ...props
}: Omit<React.ComponentProps<typeof CommandPrimitive.List>, 'ref'>) {
  const { listRef } = React.useContext(CommandContext);
  return (
    <CommandPrimitive.List
      data-slot="command-list"
      className={cn(
        'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
        className
      )}
      ref={listRef}
      {...props}
    />
  );
}

jmsheff avatar Nov 25 '25 15:11 jmsheff