From 5297fda560530dbc2407c2e3583dbc48d49c6d27 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Sat, 29 Aug 2026 14:59:39 -0400 Subject: [PATCH] Dispatch the combobox input event so onInput works AbstractCombobox registers "input" as a valid event and exposes onInput() to subscribe to it, but the native input listener only ever dispatched "search". A caller using onInput() therefore registered a callback that was never invoked, with no error to indicate it: the dispatcher accepts the registration because the event name is valid. Dispatch "input" alongside "search" rather than removing onInput(). Nothing in the tree calls onInput() today, so either would compile, but dispatching keeps the declared API and cannot break a consumer outside this repository that is already subscribing. Adds the first spec for this class, covering both callbacks and the event they receive. Fixes #659 Signed-off-by: Arpit Jain --- .../abstract-combobox.spec.ts | 66 +++++++++++++++++++ .../abstract-combobox/abstract-combobox.ts | 4 ++ 2 files changed, 70 insertions(+) create mode 100644 src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.spec.ts diff --git a/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.spec.ts b/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.spec.ts new file mode 100644 index 000000000..b1ebd3471 --- /dev/null +++ b/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.spec.ts @@ -0,0 +1,66 @@ +import { AbstractCombobox } from "./abstract-combobox"; + +// A popup stands in for AbstractPopup: the combobox only needs the three +// members its constructor and event wiring touch. +function popupStub() { + const cell = { + node: { id: "cell-1" }, + addToTabOrder: () => {}, + removeFromTabOrder: () => {}, + }; + return { + node: { id: "popup-1" }, + focused: cell, + getSelected: () => cell, + onClick: (_callback: Function, _context: Object) => {}, + onKeydown: (_callback: Function, _context: Object) => {}, + }; +} + +describe("The AbstractCombobox", () => { + let input: HTMLInputElement; + let combobox: AbstractCombobox; + + beforeEach(() => { + input = document.createElement("input"); + input.id = "combobox-input"; + document.body.appendChild(input); + combobox = new AbstractCombobox(input, popupStub() as any); + }); + + afterEach(() => { + input.remove(); + }); + + it("should call an onInput callback when the input changes", () => { + // onInput() is public and "input" is a registered event, but nothing used + // to dispatch it, so callbacks registered here were never invoked. + let calls = 0; + combobox.onInput(() => calls++, {}); + + input.value = "abc"; + input.dispatchEvent(new Event("input")); + + expect(calls).toBe(1); + }); + + it("should still call an onSearch callback when the input changes", () => { + let calls = 0; + combobox.onSearch(() => calls++, {}); + + input.value = "abc"; + input.dispatchEvent(new Event("input")); + + expect(calls).toBe(1); + }); + + it("should pass the input event through to both callbacks", () => { + const seen: string[] = []; + combobox.onInput((e: Event) => seen.push(`input:${e.type}`), {}); + combobox.onSearch((e: Event) => seen.push(`search:${e.type}`), {}); + + input.dispatchEvent(new Event("input")); + + expect(seen).toEqual(["input:input", "search:input"]); + }); +}); diff --git a/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.ts b/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.ts index a50af1429..d1b77e288 100755 --- a/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.ts +++ b/src/ui-kit/experimental/aria/abstract-combobox/abstract-combobox.ts @@ -65,6 +65,10 @@ export class AbstractCombobox { private _setupInputEvents(): void { this._input.addEventListener("input", (e) => { + // "input" is declared in _initEventDispatcher and exposed by onInput(), + // so it has to be dispatched here as well; only "search" used to be, which + // left onInput() registering callbacks that were never called. + this._dispatcher.dispatch("input", e); this._dispatcher.dispatch("search", e); });