Dropdown Component Updates (#3211)

* dropdown

* more dropdown updates

* dropdown styling + option visibility

* changelog

* notebook

* fix test

* Allow more image formats (#3225)

* add wildcard to image input

* simplify mime types

* changelog

* regen noteboks

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
Co-authored-by: pngwn <hello@pngwn.io>

* fix webcam mirroring (#3245)

* fix webcam

* changelog

* fix changelog

* fix changelog

* fix changelog

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

* Add `interactive=False` mode to `gr.Button` (#3266)

* add interactive=False to button

* add interactive=True by default

* changelog

* fix frontend

* fix backend test

* formatting

* review changes

* LaTeX height fix (#3258)

* latex height fix

* changelog

* formatting

* em

* em

* accidentally added script (#3273)

* Adding a script to benchmark the queue (#3272)

* added benchmark queue script

* changelg

* fix instructions

* Fix matplotlib image size (#3274)

* Fix matplotlib css

* CHANGELOG

* Undo lockfile

* Add timeouts to queue messages (#3196)

* Fix + test

* Remove print statements + fix import for 3.7

* CHANGELOG

* Remove more print statements

* Add 60 second timeout for uploading data

* Fix test

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

* icons

* separate options into component

* formatting

* changelog

* changelog

* fix ui tests

* formatting again...

* backend test fix

* format

* doc fixes

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
Co-authored-by: fienestar <fienestar@gmail.com>
Co-authored-by: pngwn <hello@pngwn.io>
Co-authored-by: Freddy Boulton <alfonsoboulton@gmail.com>
This commit is contained in:
Dawood Khan 2023-02-23 16:32:18 -05:00 committed by GitHub
parent ed33e8f1a8
commit f36211050c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 365 additions and 33 deletions

View File

@ -2,6 +2,15 @@
## New Features:
### Dropdown Component Updates
The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component.
```python
gr.Dropdown(label="Choose your favorite colors", choices=["red", "blue", "green", "yellow", "orange"], multiselect=True, max_choices=2)
```
by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3211](https://github.com/gradio-app/gradio/pull/3211)
### Download button for images 🖼️
Output images will now automatically have a download button displayed to make it easier to save and share

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ def fn(
checkboxes,
radio,
dropdown,
multi_dropdown,
im1,
im2,
im3,
@ -96,6 +97,7 @@ demo = gr.Interface(
gr.CheckboxGroup(label="CheckboxGroup", choices=CHOICES, value=CHOICES[0:2]),
gr.Radio(label="Radio", choices=CHOICES, value=CHOICES[2]),
gr.Dropdown(label="Dropdown", choices=CHOICES),
gr.Dropdown(label="Multiselect Dropdown (Max choice: 2)", choices=CHOICES, multiselect=True, max_choices=2),
gr.Image(label="Image"),
gr.Image(label="Image w/ Cropper", tool="select"),
gr.Image(label="Sketchpad", source="canvas"),
@ -135,6 +137,7 @@ demo = gr.Interface(
["foo", "baz"],
"baz",
"bar",
["foo", "bar"],
os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),
os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),
os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),

View File

@ -1204,6 +1204,7 @@ class Dropdown(Changeable, IOComponent, SimpleSerializable, FormComponent):
value: str | List[str] | Callable | None = None,
type: str = "value",
multiselect: bool | None = None,
max_choices: int | None = None,
label: str | None = None,
info: str | None = None,
every: float | None = None,
@ -1219,6 +1220,7 @@ class Dropdown(Changeable, IOComponent, SimpleSerializable, FormComponent):
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.
max_choices: maximum number of choices that can be selected. If None, no limit is enforced.
label: component name in interface.
info: additional component description.
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
@ -1238,6 +1240,9 @@ class Dropdown(Changeable, IOComponent, SimpleSerializable, FormComponent):
if multiselect:
if isinstance(value, str):
value = [value]
if not multiselect and max_choices is not None:
warnings.warn("The `max_choices` parameter is ignored when `multiselect` is False.")
self.max_choices = max_choices
self.test_input = self.choices[0] if len(self.choices) else None
self.interpret_by_tokens = False
IOComponent.__init__(
@ -1259,6 +1264,7 @@ class Dropdown(Changeable, IOComponent, SimpleSerializable, FormComponent):
"choices": self.choices,
"value": self.value,
"multiselect": self.multiselect,
"max_choices": self.max_choices,
**IOComponent.get_config(self),
}

View File

@ -563,7 +563,7 @@ class TestDropdown:
assert dropdown_input.preprocess("a") == "a"
assert dropdown_input.postprocess("a") == "a"
dropdown_input_multiselect = gr.Dropdown(["a", "b", "c"], multiselect=True)
dropdown_input_multiselect = gr.Dropdown(["a", "b", "c"])
assert dropdown_input_multiselect.preprocess(["a", "c"]) == ["a", "c"]
assert dropdown_input_multiselect.postprocess(["a", "c"]) == ["a", "c"]
assert dropdown_input_multiselect.serialize(["a", "c"], True) == ["a", "c"]
@ -572,6 +572,8 @@ class TestDropdown:
value=["a", "c"],
choices=["a", "b", "c"],
label="Select Your Inputs",
multiselect=True,
max_choices=2,
)
assert dropdown_input_multiselect.get_config() == {
"choices": ["a", "b", "c"],
@ -584,7 +586,8 @@ class TestDropdown:
"visible": True,
"interactive": None,
"root_url": None,
"multiselect": None,
"multiselect": True,
"max_choices": 2,
}
with pytest.raises(ValueError):
gr.Dropdown(["a"], type="unknown")

View File

@ -11,6 +11,7 @@
export let visible: boolean = true;
export let value: string | Array<string> = [];
export let multiselect: boolean = false;
export let max_choices: number;
export let choices: Array<string>;
export let show_label: boolean;
export let style: Styles = {};
@ -30,6 +31,7 @@
bind:value
{choices}
{multiselect}
{max_choices}
{label}
{info}
{show_label}

View File

@ -31,8 +31,10 @@ test("matplotlib", async ({ page }) => {
await mock_api(page, [[{ type: "matplotlib", plot: BASE64_PLOT_IMG }]]);
await page.goto("http://localhost:9876");
await page.getByLabel("Plot Type").selectOption("Matplotlib");
await page.getByLabel("Month").selectOption("January");
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Matplotlib" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([
@ -57,8 +59,10 @@ test("plotly", async ({ page }) => {
]);
await page.goto("http://localhost:9876");
await page.getByLabel("Plot Type").selectOption("Plotly");
await page.getByLabel("Month").selectOption("January");
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Matplotlib" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([

View File

@ -9,6 +9,7 @@
"private": true,
"dependencies": {
"@gradio/atoms": "workspace:^0.0.1",
"@gradio/utils": "workspace:^0.0.1"
"@gradio/utils": "workspace:^0.0.1",
"@gradio/icons": "workspace:^0.0.1"
}
}

View File

@ -1,70 +1,270 @@
<script lang="ts">
import MultiSelect from "./MultiSelect.svelte";
import DropdownOptions from "./DropdownOptions.svelte";
import { createEventDispatcher } from "svelte";
import { BlockTitle } from "@gradio/atoms";
import { Remove, DropdownArrow } from "@gradio/icons";
export let label: string;
export let info: string | undefined = undefined;
export let value: string | Array<string> | undefined = undefined;
export let multiselect: boolean = false;
export let max_choices: number;
export let choices: Array<string>;
export let disabled: boolean = false;
export let show_label: boolean;
const dispatch = createEventDispatcher<{
change: string | Array<string>;
change: string | Array<string> | undefined;
}>();
let inputValue: string,
activeOption: string,
showOptions = false;
$: filtered = choices.filter((o) =>
inputValue ? o.toLowerCase().includes(inputValue.toLowerCase()) : o
);
$: if (
(activeOption && !filtered.includes(activeOption)) ||
(!activeOption && inputValue)
)
activeOption = filtered[0];
$: readonly =
(!multiselect && typeof value === "string") ||
(multiselect && Array.isArray(value) && value.length === max_choices);
// The initial value of value is [] so that can
// cause infinite loops in the non-multiselect case
$: if (!multiselect && !Array.isArray(value)) {
dispatch("change", value);
}
function add(option: string) {
if (Array.isArray(value)) {
if (value.length < max_choices) {
value.push(option);
dispatch("change", value);
}
showOptions = !(value.length === max_choices);
}
value = value;
}
function remove(option: string) {
if (Array.isArray(value)) {
value = value.filter((v: string) => v !== option);
dispatch("change", value);
}
}
function remove_all(e: any) {
value = [];
inputValue = "";
e.preventDefault();
dispatch("change", value);
}
function handleOptionMousedown(e: any) {
const option = e.detail.target.dataset.value;
inputValue = "";
if (option !== undefined) {
if (!multiselect) {
value = option;
inputValue = "";
dispatch("change", value);
return;
}
if (value?.includes(option)) {
remove(option);
} else {
add(option);
}
}
}
function handleKeyup(e: any) {
if (e.key === "Enter" && activeOption != undefined) {
if (!multiselect) {
value = activeOption;
inputValue = "";
} else if (multiselect && Array.isArray(value)) {
value.includes(activeOption) ? remove(activeOption) : add(activeOption);
inputValue = "";
}
}
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
const increment = e.key === "ArrowUp" ? -1 : 1;
const calcIndex = filtered.indexOf(activeOption) + increment;
activeOption =
calcIndex < 0
? filtered[filtered.length - 1]
: calcIndex === filtered.length
? filtered[0]
: filtered[calcIndex];
}
if (e.key === "Escape") {
showOptions = false;
}
}
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>
<BlockTitle {show_label} {info}>{label}</BlockTitle>
{#if !multiselect}
<select bind:value {disabled}>
{#each choices as choice}
<option>{choice}</option>
{/each}
</select>
{:else}
<MultiSelect bind:value {choices} on:change {disabled} />
{/if}
<div class="wrap">
<div class="wrap-inner" class:showOptions>
{#if Array.isArray(value)}
{#each value as s}
<div on:click|preventDefault={() => remove(s)} class="token">
<span>{s}</span>
<div
class:hidden={disabled}
class="token-remove"
title="Remove {s}"
>
<Remove />
</div>
</div>
{/each}
{:else}
<span class="single-select">{value}</span>
{/if}
<div class="secondary-wrap">
<input
class="border-none"
{disabled}
{readonly}
autocomplete="off"
bind:value={inputValue}
on:focus={() =>
(showOptions =
Array.isArray(value) && value.length === max_choices
? false
: true)}
on:blur={() => (showOptions = false)}
on:keyup={handleKeyup}
/>
<div
class:hide={!value?.length || disabled}
class="token-remove remove-all"
title="Remove All"
on:click={remove_all}
>
<Remove />
</div>
<DropdownArrow />
</div>
</div>
<DropdownOptions
bind:value
{showOptions}
{filtered}
{activeOption}
{disabled}
on:change={handleOptionMousedown}
/>
</div>
</label>
<style>
select {
.wrap {
--ring-color: transparent;
display: block;
position: relative;
outline: none !important;
box-shadow: 0 0 0 var(--shadow-spread) var(--ring-color),
var(--shadow-inset);
border: 1px solid var(--input-border-color-base);
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
background-color: var(--input-background-base);
padding: var(--size-2-5);
width: 100%;
color: var(--color-text-body);
font-size: var(--scale-00);
line-height: var(--line-sm);
}
select:focus {
.wrap:focus-within {
--ring-color: var(--color-focus-ring);
border-color: var(--input-border-color-focus);
}
select::placeholder {
color: var(--color-text-placeholder);
.wrap-inner {
display: flex;
position: relative;
flex-wrap: wrap;
align-items: center;
}
select[disabled] {
.token {
display: flex;
align-items: center;
cursor: pointer;
margin: var(--size-1);
box-shadow: var(--shadow-drop);
border: 1px solid var(--checkbox-label-border-color-base);
border-radius: var(--radius-md);
background: var(--checkbox-label-background-base);
padding: var(--size-1-5) var(--size-3);
color: var(--color-text-body);
font-size: var(--scale-00);
line-height: var(--line-md);
}
.token > * + * {
margin-left: var(--size-2);
}
.token:hover {
border: 1px solid var(--icon_button-border-color-hover);
color: var(--color-text-label);
}
.token-remove {
fill: var(--color-text-body);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-full);
background: var(--color-background-tertiary);
padding: var(--size-0-5);
width: 18px;
height: 18px;
}
.token-remove:hover,
.remove-all:hover {
border: 1px solid var(--icon_button-border-color-hover);
color: var(--color-text-label);
}
.single-select {
margin: var(--size-2);
color: var(--color-text-body);
}
.secondary-wrap {
display: flex;
flex: 1 1 0%;
align-items: center;
border: none;
min-width: min-content;
}
input {
outline: none;
border: none;
background: inherit;
padding: var(--size-2-5);
width: 100%;
color: var(--color-text-body);
font-size: var(--scale-00);
}
input:disabled {
cursor: not-allowed;
box-shadow: none;
}
.remove-all {
margin-left: var(--size-1);
width: 20px;
height: 20px;
}
</style>

View File

@ -0,0 +1,73 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { createEventDispatcher } from "svelte";
export let value: string | Array<string> | undefined = undefined;
export let filtered: Array<string>;
export let showOptions: boolean = false;
export let activeOption: string;
export let disabled: boolean = false;
const dispatch = createEventDispatcher();
</script>
{#if showOptions && !disabled}
<ul
class="options"
aria-expanded={showOptions}
transition:fly={{ duration: 200, y: 5 }}
on:mousedown|preventDefault={(e) => dispatch("change", e)}
>
{#each filtered as choice}
<li
class="item"
role="button"
class:selected={value?.includes(choice)}
class:active={activeOption === choice}
class:bg-gray-100={activeOption === choice}
class:dark:bg-gray-600={activeOption === choice}
data-value={choice}
aria-label={choice}
>
<span class:hide={!value?.includes(choice)} class="inner-item pr-1">
</span>
{choice}
</li>
{/each}
</ul>
{/if}
<style>
.options {
position: absolute;
z-index: var(--layer-5);
margin-left: 0;
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-lg);
background: var(--color-background-primary);
width: var(--size-full);
max-height: var(--size-32);
overflow: auto;
color: var(--color-text-body);
list-style: none;
}
.item {
display: flex;
cursor: pointer;
padding: var(--size-2);
}
.item:hover,
.active {
background: var(--color-background-secondary);
}
.inner-item {
padding-right: var(--size-1);
}
.hide {
visibility: hidden;
}
</style>

View File

@ -0,0 +1,17 @@
<svg
class="dropdown-arrow"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
>
<path d="M5 8l4 4 4-4z" />
</svg>
<style>
.dropdown-arrow {
fill: var(--color-text-body);
margin-right: var(--size-2);
width: var(--size-5);
}
</style>

After

Width:  |  Height:  |  Size: 275 B

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@ -5,6 +5,7 @@ export { default as Chat } from "./Chat.svelte";
export { default as Circle } from "./Circle.svelte";
export { default as Clear } from "./Clear.svelte";
export { default as Color } from "./Color.svelte";
export { default as DropdownArrow } from "./DropdownArrow.svelte";
export { default as Edit } from "./Edit.svelte";
export { default as File } from "./File.svelte";
export { default as Image } from "./Image.svelte";
@ -15,6 +16,7 @@ export { default as Music } from "./Music.svelte";
export { default as Pause } from "./Pause.svelte";
export { default as Play } from "./Play.svelte";
export { default as Plot } from "./Plot.svelte";
export { default as Remove } from "./Remove.svelte";
export { default as Sketch } from "./Sketch.svelte";
export { default as Square } from "./Square.svelte";
export { default as Table } from "./Table.svelte";

2
ui/pnpm-lock.yaml generated
View File

@ -230,9 +230,11 @@ importers:
packages/form:
specifiers:
'@gradio/atoms': workspace:^0.0.1
'@gradio/icons': workspace:^0.0.1
'@gradio/utils': workspace:^0.0.1
dependencies:
'@gradio/atoms': link:../atoms
'@gradio/icons': link:../icons
'@gradio/utils': link:../utils
packages/gallery: