From 3e4e680a52ba5a73c108ef1b328dacd7b6e4b566 Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Fri, 16 Feb 2024 10:21:32 -0800 Subject: [PATCH] Fixes to the `.key_up()` method to make it usable for a dynamic dropdown autocomplete (#7425) * fixes * changes * add changeset * add changeset --------- Co-authored-by: gradio-pr-bot Co-authored-by: Hannah --- .changeset/witty-news-film.md | 6 +++++ gradio/blocks.py | 4 ++-- gradio/component_meta.py | 2 +- gradio/events.py | 2 +- js/dropdown/dropdown.test.ts | 37 ++++++++++++++++++++++++++---- js/dropdown/shared/Dropdown.svelte | 13 +++++++++-- 6 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 .changeset/witty-news-film.md diff --git a/.changeset/witty-news-film.md b/.changeset/witty-news-film.md new file mode 100644 index 0000000000..1f3f3c5095 --- /dev/null +++ b/.changeset/witty-news-film.md @@ -0,0 +1,6 @@ +--- +"@gradio/dropdown": patch +"gradio": patch +--- + +feat:Fixes to the `.key_up()` method to make it usable for a dynamic dropdown autocomplete diff --git a/gradio/blocks.py b/gradio/blocks.py index 42ca4cba3b..58c4f136aa 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -848,7 +848,7 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta): collects_event_data: whether to collect event data for this event trigger_after: if set, this event will be triggered after 'trigger_after' function index trigger_only_on_success: if True, this event will only be triggered if the previous event was successful (only applies if `trigger_after` is set) - trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` event) would allow a second submission after the pending event is complete. + trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `queue()`, which itself is 1 by default). concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. show_api: whether to show this event in the "view API" page of the Gradio app, or in the ".view_api()" method of the Gradio clients. Unlike setting api_name to False, setting show_api to False will still allow downstream apps to use this event. If fn is None, show_api will automatically be set to False. @@ -901,7 +901,7 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta): "Cannot set a value for `concurrency_limit` with `every`." ) - if _targets[0][1] == "change" and trigger_mode is None: + if _targets[0][1] in ["change", "key_up"] and trigger_mode is None: trigger_mode = "always_last" elif trigger_mode is None: trigger_mode = "once" diff --git a/gradio/component_meta.py b/gradio/component_meta.py index ab721353ff..bec5a697b9 100644 --- a/gradio/component_meta.py +++ b/gradio/component_meta.py @@ -50,7 +50,7 @@ INTERFACE_TEMPLATE = ''' postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser. cancels: A list of other events to cancel when this listener is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method. Functions that have not yet run (or generators that are iterating) will be cancelled, but functions that are currently running will be allowed to finish. every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. - trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` event) would allow a second submission after the pending event is complete. + trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components. concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `Blocks.queue()`, which itself is 1 by default). concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. diff --git a/gradio/events.py b/gradio/events.py index 53b2c79dfa..55a6cdf13a 100644 --- a/gradio/events.py +++ b/gradio/events.py @@ -240,7 +240,7 @@ class EventListener(str): postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser. cancels: A list of other events to cancel when this listener is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method. Functions that have not yet run (or generators that are iterating) will be cancelled, but functions that are currently running will be allowed to finish. every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. - trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` event) would allow a second submission after the pending event is complete. + trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components. concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `Blocks.queue()`, which itself is 1 by default). concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. diff --git a/js/dropdown/dropdown.test.ts b/js/dropdown/dropdown.test.ts index 1810f903ae..01fd1f444c 100644 --- a/js/dropdown/dropdown.test.ts +++ b/js/dropdown/dropdown.test.ts @@ -410,7 +410,7 @@ describe("Dropdown", () => { expect(item.value).toBe("apple"); }); - test("updating choices should keep the dropdown focus-able and change the choice name", async () => { + test("updating choices should keep the dropdown focus-able and change the value appropriately if custom values are not allowed", async () => { const { getByLabelText, component } = await render(Dropdown, { show_label: true, loading_status, @@ -443,11 +443,12 @@ describe("Dropdown", () => { await expect(item.value).toBe("apple_new_choice"); }); - test("ensure dropdown can have an empty value", async () => { - const { getByLabelText } = await render(Dropdown, { + test("updating choices should not reset the value if custom values are allowed", async () => { + const { getByLabelText, component } = await render(Dropdown, { show_label: true, loading_status, - allow_custom_value: false, + value: "apple_internal_value", + allow_custom_value: true, label: "Dropdown", choices: [ ["apple_choice", "apple_internal_value"], @@ -461,6 +462,34 @@ describe("Dropdown", () => { "Dropdown" ) as HTMLInputElement; + await expect(item.value).toBe("apple_choice"); + + component.$set({ + choices: [ + ["apple_new_choice", "apple_internal_value"], + ["zebra_new_choice", "zebra_internal_value"] + ] + }); + + await expect(item.value).toBe("apple_choice"); + }); + + test("ensure dropdown can have an empty value", async () => { + const { getByLabelText } = await render(Dropdown, { + show_label: true, + loading_status, + allow_custom_value: false, + label: "Dropdown", + choices: [ + ["apple_choice", "apple_internal_value"], + ["zebra_choice", "zebra_internal_value"] + ], + filterable: true, + interactive: true + }); + const item: HTMLInputElement = getByLabelText( + "Dropdown" + ) as HTMLInputElement; await expect(item.value).toBe(""); }); }); diff --git a/js/dropdown/shared/Dropdown.svelte b/js/dropdown/shared/Dropdown.svelte index 254c2b2226..1c68c4302f 100644 --- a/js/dropdown/shared/Dropdown.svelte +++ b/js/dropdown/shared/Dropdown.svelte @@ -55,6 +55,7 @@ [input_text, old_value] = choices[selected_index]; old_input_text = input_text; } + set_input_text(); } else if (choices.length > 0) { old_selected_index = 0; selected_index = 0; @@ -87,19 +88,26 @@ } } - $: { + function set_choice_names_values(): void { choices_names = choices.map((c) => c[0]); choices_values = choices.map((c) => c[1]); } + $: choices, set_choice_names_values(); + $: { if (choices !== old_choices) { - set_input_text(); + if (!allow_custom_value) { + set_input_text(); + } old_choices = choices; filtered_indices = handle_filter(choices, input_text); if (!allow_custom_value && filtered_indices.length > 0) { active_index = filtered_indices[0]; } + if (filter_input == document.activeElement) { + show_options = true; + } } } @@ -114,6 +122,7 @@ } function set_input_text(): void { + set_choice_names_values(); if (value === undefined) { input_text = ""; selected_index = null;