diff --git a/.changeset/loud-colts-lie.md b/.changeset/loud-colts-lie.md new file mode 100644 index 000000000000..751478a206ee --- /dev/null +++ b/.changeset/loud-colts-lie.md @@ -0,0 +1,6 @@ +--- +"@gradio/dropdown": patch +"gradio": patch +--- + +fix:Cancel Dropdown Filter diff --git a/js/dropdown/dropdown.test.ts b/js/dropdown/dropdown.test.ts index 64c58ad7fe63..13b2b9f4aab6 100644 --- a/js/dropdown/dropdown.test.ts +++ b/js/dropdown/dropdown.test.ts @@ -1,4 +1,4 @@ -import { test, describe, assert, afterEach } from "vitest"; +import { test, describe, assert, afterEach, vi } from "vitest"; import { cleanup, fireEvent, render, get_text, wait } from "@gradio/tootils"; import event from "@testing-library/user-event"; import { setupi18n } from "../app/src/i18n"; @@ -18,7 +18,10 @@ const loading_status: LoadingStatus = { }; describe("Dropdown", () => { - afterEach(() => cleanup()); + afterEach(() => { + cleanup(); + vi.useRealTimers(); + }); beforeEach(() => { setupi18n(); }); @@ -87,7 +90,55 @@ describe("Dropdown", () => { expect(options[0]).toContainHTML("zebra"); }); + test("blurring the textbox should cancel the filter", async () => { + const { getByLabelText, listen } = await render(Dropdown, { + show_label: true, + loading_status, + value: "default", + label: "Dropdown", + choices: ["default", "other"] + }); + + const item: HTMLInputElement = getByLabelText( + "Dropdown" + ) as HTMLInputElement; + const change_event = listen("change"); + const select_event = listen("select"); + + await item.focus(); + await event.keyboard("other"); + await item.blur(); + + assert.equal(item.value, "default"); + assert.equal(change_event.callCount, 0); + assert.equal(select_event.callCount, 0); + }); + + test("focusing the label should toggle the options", async () => { + const { getByLabelText, listen } = await render(Dropdown, { + show_label: true, + loading_status, + value: "default", + label: "Dropdown", + choices: ["default", "other"] + }); + + const item: HTMLInputElement = getByLabelText( + "Dropdown" + ) as HTMLInputElement; + const blur_event = listen("blur"); + const focus_event = listen("focus"); + + await item.focus(); + await item.blur(); + await item.focus(); + + assert.equal(blur_event.callCount, 1); + assert.equal(focus_event.callCount, 1); + }); + test("deselecting and reselcting a filtered dropdown should show all options again", async () => { + vi.useFakeTimers(); const { getByLabelText, getAllByTestId, debug } = await render(Dropdown, { show_label: true, loading_status, @@ -108,6 +159,8 @@ describe("Dropdown", () => { expect(options).toHaveLength(1); await item.blur(); + // Mock 100ms delay between interactions. + vi.runAllTimers(); await item.focus(); const options_new = getAllByTestId("dropdown-option"); diff --git a/js/dropdown/shared/Dropdown.svelte b/js/dropdown/shared/Dropdown.svelte index d03289e38d57..499b8a947b66 100644 --- a/js/dropdown/shared/Dropdown.svelte +++ b/js/dropdown/shared/Dropdown.svelte @@ -101,24 +101,29 @@ e.preventDefault(); } + let blurring = false; + function handle_blur(e: FocusEvent): void { + if (blurring) return; + blurring = true; if (multiselect) { inputValue = ""; } else if (!allow_custom_value) { - if (value !== inputValue) { - if (typeof value === "string" && inputValue == "") { - inputValue = value; - } else { - value = undefined; - inputValue = ""; - } - } + inputValue = value as string | undefined; } showOptions = false; dispatch("blur"); + setTimeout(() => { + blurring = false; + }, 100); } function handle_focus(e: FocusEvent): void { + if (blurring) { + // Remove focus triggered by blurring to the label. + let target = e.target as HTMLInputElement; + return target.blur(); + } dispatch("focus"); showOptions = true; filtered = choices;