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

Cerrado Apto para principiantes
#10,846 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@ayuxy027 ya está trabajando en esto.

Desde el 28/10/2026.

  • #10850 de @florianaltendorfer — cerrado sin fusionar
  • #10869 de @mangeshraut712 — fusionado
  • #10882 de @ayuxy027 — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
74/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
react, typescript

Línea de trabajo

Empieza en useComboBoxState y sigue cómo setFocused gestiona el desenfoque y confirma el valor cuando isReadOnly es true. Reproduce el problema con el ejemplo de ComboBox controlado proporcionado, usando una lista de opciones vacía y una clave seleccionada que no está en la colección. Se considera resuelto cuando el desenfoque deja sin cambios la selección controlada y el texto de entrada, y no emite un cambio con null.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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

Lenguaje dominante
TypeScript
Estrellas
16k
Forks
1.6k
Merge medio
2 d 17 h
PR fusionados (30 d)
41

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de adobe/react-spectrum

Todos los issues de adobe/react-spectrum

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.