ComboBox fires change events on blur when isReadOnly, clearing a fully controlled value that is not in the collection

Aperta Adatta ai principianti
#10,846 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@mangeshraut712 ci sta già lavorando.

Dal 27/10/2026.

  • #10850 di @florianaltendorfer — chiusa senza merge
  • #10869 di @mangeshraut712 — aperta
  • #10882 di @ayuxy027 — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
74/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
react, typescript

Direzione di ricerca

Inizia da useComboBoxState e segui come setFocused gestisce la perdita del focus e conferma il valore quando isReadOnly è true. Riproduci il problema con l’esempio controllato di ComboBox fornito, usando un elenco di opzioni vuoto e una chiave selezionata assente dalla collection. Il problema è risolto quando la perdita del focus lascia invariati la selezione controllata e il testo di input e non emette una modifica con null.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug
Provide a general summary of the issue here

A fully controlled ComboBox with isReadOnly fires change events with null on blur when the currently selected key is not present in the collection. This becomes an issue when, for example, options are fetched asynchronously and the list is empty while the dropdown is closed.

isReadOnly seems to only be consulted when to decide whether to auto-open the dropdown on focus. Committing the value runs regardless of isReadOnly. Apps that derive the input text from the selection end up clearing a field the user never changed.

Simple workarounds exist, making this somewhat non-critical. But it might still be a bit confusing when it's happening.

🤔 Expected Behavior?

A ComboBox with isReadOnly should not attempt to commit values on blur. The user cannot change the selection or the input while read-only, so a blur should be a no-op.

😯 Current Behavior
  1. The input shows the selected label on mount
  2. Click into the input (focus)
  3. Click/tab out (blur)
  4. Change event fires with null and the input is cleared
💁 Possible Solution

Gate the blur commit on isReadOnly in useComboBoxState's setFocused.

🔦 Context

We fetch a list of objects from our server, which cannot be modified anymore. When rendered in a grid, we want to be able to append more objects to that list and we want them to be in the same grid as the ones returned from the server. For the fields related to the existing objects, we set isReadOnly to show them to the user, without the user being able to modify them. New objects are appended and can be modified before saving.

One of the fields in those objects is handled by the ComboBox. When a user types in the ComboBox, we query our server for a list of results. The query requires a minimum of 3 characters. Querying is only done on input change, so it never gets triggered unless the user actually is typing in the input field. Hence why the items list is empty when the blur happens. When selecting an option, the input value is changed to match the queried name.

There are simple workarounds, like ensuring that the items always contain at least the selected item, or defaulting a null selection change back to the selected item if isReadOnly.

🖥️ Steps to Reproduce
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import type { Key } from "react-aria-components";
import {
  Button,
  ComboBox,
  Input,
  Label,
  ListBox,
  ListBoxItem,
  Popover,
} from "react-aria-components";

// Repro: a read-only, fully controlled ComboBox (value + inputValue) whose
// options are fetched asynchronously while typing. When the menu is closed
// there is no active query, so `items` is empty and the selected key is not in
// the collection. Focusing then blurring the input clears it, even though the
// field is read-only and nothing changed: RAC still runs the blur commit and,
// because the selected key can't be resolved in the empty collection, fires
// onChange(null).

const styles = {
  root: { display: "flex", flexDirection: "column", gap: 4, maxWidth: 320 },
  field: {
    display: "flex",
    alignItems: "center",
    border: "1px solid #D9D9D9",
    borderRadius: 6,
    background: "#FFF",
  },
  input: { flex: 1, border: "none", outline: "none", padding: "8px 12px" },
  button: { border: "none", background: "transparent", padding: "0 10px" },
  popover: {
    background: "#FFF",
    border: "1px solid #D9D9D9",
    borderRadius: 6,
    boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
    minWidth: "var(--trigger-width)",
    padding: 4,
  },
  item: { padding: "8px 12px", borderRadius: 4, outline: "none" },
} satisfies Record<string, React.CSSProperties>;

const meta = { title: "Bugs/RAC ComboBox ReadOnly Blur" } satisfies Meta;
export default meta;
type Story = StoryObj<typeof meta>;

type Country = { id: string; name: string };

function Repro({ options }: { options: Countryy[] }) {
  // Pre-selected value with its display text, as if restored from saved data.
  const [value, setValue] = useState<Key | null>("fi");
  const [inputValue, setInputValue] = useState("Finland");

  return (
    <ComboBox
      aria-label="Country"
      items={options}
      value={value}
      inputValue={inputValue}
      onChange={(key) => {
        const match = options.find((c) => c.id === key) ?? null;
        setValue(match?.id ?? null);
        setInputValue(match?.name ?? "");
      }}
      onInputChange={setInputValue}
      isReadOnly
      allowsEmptyCollection
      style={styles.root}
    >
      <Label>Country</Label>
      <div style={styles.field}>
        <Input placeholder="Select a country" style={styles.input} />
        <Button style={styles.button}>▼</Button>
      </div>
      <Popover style={styles.popover}>
        <ListBox<Country>>
          {(item) => (
            <ListBoxItem id={item.id} style={styles.item}>
              {item.name}
            </ListBoxItem>
          )}
        </ListBox>
      </Popover>
    </ComboBox>
  );
}

// No active query -> empty options. Focus + blur clears the input.
export const Bug: Story = { render: () => <Repro options={[]} /> };

// Selected item present in the collection. Focus + blur keeps the value.
export const Ok: Story = {
  render: () => <Repro options={[{ id: "fi", name: "Finland" }]} />,
};

Version

react-aria-components: 1.22.0

What browsers are you seeing the problem on?

Other

If other, please specify.

all

What operating system are you using?

Linux, MacOs, Windows

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Lingua principale
TypeScript
Stelle
16k
Fork
1.6k
Merge medio
2g 19h
PR unite (30g)
41

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di adobe/react-spectrum

Tutte le issue di adobe/react-spectrum

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.