Skip to content

Commit

Permalink
Allows the gr.Dropdown to have separate names and values, as well a…
Browse files Browse the repository at this point in the history
…s enables `allow_custom_value` for multiselect dropdown (#5384)

* dropdown

* changes

* add changeset

* refactor

* cleanup

* dropdown

* more refactoring

* fixes

* simplify, docstring

* restore active_index

* split into two files

* new files

* simplify

* single select dropdown working

* single select dropdown almost working

* dropdown

* multiselect

* multiselect wip

* multiselect

* multiselect

* multiselect

* interactive working

* dropdown

* lint

* add changeset

* type

* typing

* fix multiselect static

* dropdown

* stories and tests

* split stories

* lint

* add changeset

* revert

* add changeset

* fix x

* dropdown

* lint, test

* backend fixes

* lint

* fix tests

* lint

* fix final test

* clean

* review fixes

* dropdown

* lint

* lint

* changes

* typing

* fixes

* active index null bug

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
  • Loading branch information
abidlabs and gradio-pr-bot committed Sep 12, 2023
1 parent 9ee20f4 commit ddc0226
Show file tree
Hide file tree
Showing 16 changed files with 840 additions and 397 deletions.
8 changes: 8 additions & 0 deletions .changeset/cute-tools-switch.md
@@ -0,0 +1,8 @@
---
"@gradio/checkboxgroup": minor
"@gradio/dropdown": minor
"@gradio/radio": minor
"gradio": minor
---

feat:Allows the `gr.Dropdown` to have separate names and values, as well as enables `allow_custom_value` for multiselect dropdown
47 changes: 36 additions & 11 deletions gradio/components/dropdown.py
Expand Up @@ -41,9 +41,9 @@ class Dropdown(

def __init__(
self,
choices: list[str] | None = None,
choices: list[str | int | float | tuple[str, str | int | float]] | None = None,
*,
value: str | list[str] | Callable | None = None,
value: str | int | float | list[str | int | float] | Callable | None = None,
type: Literal["value", "index"] = "value",
multiselect: bool | None = None,
allow_custom_value: bool = False,
Expand All @@ -63,7 +63,7 @@ def __init__(
):
"""
Parameters:
choices: list of options to select from.
choices: A list of string options to choose from. An option can also be a tuple of the form (name, value), where name is the displayed name of the dropdown choice and value is the value to be passed to the function, or returned by the function.
value: default value(s) selected in dropdown. If None, no value is selected by default. If callable, the function will be called whenever the app loads to set the initial value of the component.
type: Type of value to be returned by component. "value" returns the string of the choice selected, "index" returns the index of the choice selected.
multiselect: if True, multiple choices can be selected.
Expand All @@ -81,7 +81,11 @@ def __init__(
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
"""
self.choices = [str(choice) for choice in choices] if choices else []
self.choices = (
[c if isinstance(c, tuple) else (str(c), c) for c in choices]
if choices
else []
)
valid_types = ["value", "index"]
if type not in valid_types:
raise ValueError(
Expand All @@ -97,10 +101,6 @@ def __init__(
)
self.max_choices = max_choices
self.allow_custom_value = allow_custom_value
if multiselect and allow_custom_value:
raise ValueError(
"Custom values are not supported when `multiselect` is True."
)
self.interpret_by_tokens = False
self.select: EventListenerMethod
"""
Expand Down Expand Up @@ -162,7 +162,7 @@ def get_config(self):
@staticmethod
def update(
value: Any | Literal[_Keywords.NO_VALUE] | None = _Keywords.NO_VALUE,
choices: str | list[str] | None = None,
choices: str | list[str | tuple[str, str]] | None = None,
label: str | None = None,
info: str | None = None,
show_label: bool | None = None,
Expand Down Expand Up @@ -203,15 +203,37 @@ def preprocess(
if x is None:
return None
elif self.multiselect:
return [self.choices.index(c) for c in x]
return [
[value for _, value in self.choices].index(choice) for choice in x
]
else:
if isinstance(x, str):
return self.choices.index(x) if x in self.choices else None
return (
[value for _, value in self.choices].index(x)
if x in self.choices
else None
)
else:
raise ValueError(
f"Unknown type: {self.type}. Please choose from: 'value', 'index'."
)

def _warn_if_invalid_choice(self, y):
if self.allow_custom_value or y in [value for _, value in self.choices]:
return
warnings.warn(
f"The value passed into gr.Dropdown() is not in the list of choices. Please update the list of choices to include: {y} or set allow_custom_value=True."
)

def postprocess(self, y):
if y is None:
return None
if self.multiselect:
[self._warn_if_invalid_choice(_y) for _y in y]
else:
self._warn_if_invalid_choice(y)
return y

def set_interpret_parameters(self):
"""
Calculates interpretation score of each choice by comparing the output against each of the outputs when alternative choices are selected.
Expand Down Expand Up @@ -241,3 +263,6 @@ def style(self, *, container: bool | None = None, **kwargs):
if container is not None:
self.container = container
return self

def as_example(self, input_data):
return next((c[0] for c in self.choices if c[1] == input_data), None)
2 changes: 1 addition & 1 deletion js/checkboxgroup/shared/Checkboxgroup.svelte
Expand Up @@ -6,7 +6,7 @@
export let value: (string | number)[] = [];
let old_value: (string | number)[] = value.slice();
export let value_is_output = false;
export let choices: [string, number][];
export let choices: [string, string | number][];
export let disabled = false;
export let label: string;
export let info: string | undefined = undefined;
Expand Down
33 changes: 10 additions & 23 deletions js/dropdown/Dropdown.stories.svelte
Expand Up @@ -24,37 +24,24 @@
name="Single-select"
args={{
value: "swim",
choices: ["run", "swim", "jump"],
multiselect: false,
choices: [
["run", "run"],
["swim", "swim"],
["jump", "jump"]
],
label: "Single-select Dropdown"
}}
/>
<Story
name="Single-select Static"
args={{
value: "swim",
choices: ["run", "swim", "jump"],
multiselect: false,
choices: [
["run", "run"],
["swim", "swim"],
["jump", "jump"]
],
disabled: true,
label: "Single-select Dropdown"
}}
/>
<Story
name="Multiselect"
args={{
value: ["swim", "run"],
choices: ["run", "swim", "jump"],
label: "Multiselect Dropdown",
multiselect: true
}}
/>
<Story
name="Multiselect Static"
args={{
value: ["swim", "run"],
choices: ["run", "swim", "jump"],
label: "Multiselect Dropdown",
multiselect: true,
disabled: true
}}
/>
46 changes: 46 additions & 0 deletions js/dropdown/Multiselect.stories.svelte
@@ -0,0 +1,46 @@
<script>
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import Multiselect from "./shared/Multiselect.svelte";
</script>

<Meta
title="Components/Multiselect"
component={Multiselect}
argTypes={{
multiselect: {
control: [true, false],
name: "multiselect",
value: false
}
}}
/>

<Template let:args>
<Multiselect {...args} />
</Template>

<Story
name="Multiselect"
args={{
value: ["swim", "run"],
choices: [
["run", "run"],
["swim", "swim"],
["jump", "jump"]
],
label: "Multiselect Dropdown"
}}
/>
<Story
name="Multiselect Static"
args={{
value: ["swim", "run"],
choices: [
["run", "run"],
["swim", "swim"],
["jump", "jump"]
],
label: "Multiselect Dropdown",
disabled: true
}}
/>
66 changes: 49 additions & 17 deletions js/dropdown/dropdown.test.ts
Expand Up @@ -29,10 +29,13 @@ describe("Dropdown", () => {
const { getByLabelText } = await render(Dropdown, {
show_label: true,
loading_status,
max_choices: 10,
max_choices: null,
value: "choice",
label: "Dropdown",
choices: ["choice", "choice2"]
choices: [
["choice", "choice"],
["choice2", "choice2"]
]
});

const item: HTMLInputElement = getByLabelText(
Expand All @@ -42,13 +45,16 @@ describe("Dropdown", () => {
});

test("selecting the textbox should show the options", async () => {
const { getByLabelText, getAllByTestId, debug } = await render(Dropdown, {
const { getByLabelText, getAllByTestId } = await render(Dropdown, {
show_label: true,
loading_status,
max_choices: 10,
value: "choice",
label: "Dropdown",
choices: ["choice", "choice2"]
choices: [
["choice", "choice"],
["name2", "choice2"]
]
});

const item: HTMLInputElement = getByLabelText(
Expand All @@ -61,17 +67,20 @@ describe("Dropdown", () => {

expect(options).toHaveLength(2);
expect(options[0]).toContainHTML("choice");
expect(options[1]).toContainHTML("choice2");
expect(options[1]).toContainHTML("name2");
});

test("editing the textbox value should filter the options", async () => {
const { getByLabelText, getAllByTestId, debug } = await render(Dropdown, {
const { getByLabelText, getAllByTestId } = await render(Dropdown, {
show_label: true,
loading_status,
max_choices: 10,
value: "",
label: "Dropdown",
choices: ["apple", "zebra"]
choices: [
["apple", "apple"],
["zebra", "zebra"]
]
});

const item: HTMLInputElement = getByLabelText(
Expand All @@ -83,6 +92,7 @@ describe("Dropdown", () => {

expect(options).toHaveLength(2);

item.value = "";
await event.keyboard("z");
const options_new = getAllByTestId("dropdown-option");

Expand All @@ -96,7 +106,11 @@ describe("Dropdown", () => {
loading_status,
value: "default",
label: "Dropdown",
choices: ["default", "other"]
max_choices: undefined,
choices: [
["default", "default"],
["other", "other"]
]
});

const item: HTMLInputElement = getByLabelText(
Expand All @@ -120,7 +134,10 @@ describe("Dropdown", () => {
loading_status,
value: "default",
label: "Dropdown",
choices: ["default", "other"]
choices: [
["default", "default"],
["other", "other"]
]
});

const item: HTMLInputElement = getByLabelText(
Expand All @@ -131,28 +148,32 @@ describe("Dropdown", () => {

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, {
const { getByLabelText, getAllByTestId } = await render(Dropdown, {
show_label: true,
loading_status,
max_choices: 10,
value: "",
label: "Dropdown",
choices: ["apple", "zebra", "pony"]
choices: [
["apple", "apple"],
["zebra", "zebra"],
["pony", "pony"]
]
});

const item: HTMLInputElement = getByLabelText(
"Dropdown"
) as HTMLInputElement;

await item.focus();
item.value = "";
await event.keyboard("z");
const options = getAllByTestId("dropdown-option");

Expand All @@ -173,10 +194,13 @@ describe("Dropdown", () => {
{
show_label: true,
loading_status,
max_choices: 10,
value: "zebra",
value: "",
label: "Dropdown",
choices: ["apple", "zebra", "pony"]
choices: [
["apple", "apple"],
["zebra", "zebra"],
["pony", "pony"]
]
}
);

Expand All @@ -190,10 +214,18 @@ describe("Dropdown", () => {

expect(options).toHaveLength(3);

await component.$set({ choices: ["apple", "zebra", "pony"] });
await component.$set({
value: "",
choices: [
["apple", "apple"],
["zebra", "zebra"],
["pony", "pony"]
]
});

const options_new = getAllByTestId("dropdown-option");
await item.focus();

const options_new = getAllByTestId("dropdown-option");
expect(options_new).toHaveLength(3);
});
});

0 comments on commit ddc0226

Please sign in to comment.