Accessibility Improvements (#5554)

* allow remove token via keyboard

* more a11y enhancements

* upload + dataset a11y tweaks

* add changeset

* add webcam label

* improve checkbox focus styling and allow interaction via keyboard

* add changeset

* improve radio focus color

* tweak

* add radio label

* add changeset

* add annotated image alt + use button for labels

* button tweaks

* add changeset

* tweak

* more changes

* tiny tweaks

* galley / image

* label tweaks and add semantic tags to confidence

* nit + docstring

* tweak

* add changeset

* fix tests

* unit test fix

* range tweak

* fix alignment in gallery

* range tweak

* slider test tweak

* tweak

* more test fixes

* last? test tweak

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Hannah 2023-09-22 14:12:26 +02:00 committed by GitHub
parent 9ccc4794a7
commit 75ddeb390d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 321 additions and 182 deletions

View File

@ -0,0 +1,24 @@
---
"@gradio/accordion": minor
"@gradio/annotatedimage": minor
"@gradio/app": minor
"@gradio/button": minor
"@gradio/chatbot": minor
"@gradio/checkbox": minor
"@gradio/checkboxgroup": minor
"@gradio/code": minor
"@gradio/dropdown": minor
"@gradio/gallery": minor
"@gradio/image": minor
"@gradio/json": minor
"@gradio/label": minor
"@gradio/number": minor
"@gradio/plot": minor
"@gradio/radio": minor
"@gradio/slider": minor
"@gradio/textbox": minor
"@gradio/upload": minor
"gradio": minor
---
feat:Accessibility Improvements

View File

@ -91,7 +91,7 @@ class Textbox(
min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
interactive: if True, will be rendered as an editable textbox; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output.
visible: If False, component will be hidden.
autofocus: If True, will focus on the textbox when the page loads.
autofocus: If True, will focus on the textbox when the page loads. Use this carefully, as it can cause usability issues for sighted and non-sighted users.
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.
type: The type of textbox. One of: 'text', 'password', 'email', Default is 'text'.

View File

@ -3,15 +3,12 @@
export let open = true;
</script>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click={() => (open = !open)} class="label-wrap" class:open>
<button on:click={() => (open = !open)} class="label-wrap" class:open>
<span>{label}</span>
<span style:transform={open ? "rotate(0)" : "rotate(90deg)"} class="icon">
</span>
</div>
</button>
<div style:display={open ? "block" : "none"}>
<slot />
</div>

View File

@ -41,8 +41,8 @@
normalise_file(value[0], root, root_url) as FileData,
value[1].map(([file, _label]) => [
normalise_file(file, root, root_url) as FileData,
_label
])
_label,
]),
];
} else {
_value = null;
@ -58,7 +58,7 @@
function handle_click(i: number): void {
gradio.dispatch("select", {
value: label,
index: i
index: i,
});
}
</script>
@ -83,15 +83,15 @@
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
<div class="image-container">
<!-- svelte-ignore a11y-missing-attribute -->
<img
class="base-image"
class:fit-height={height}
src={_value ? _value[0].data : null}
alt="uploaded file"
/>
{#each _value ? _value[1] : [] as [file, label], i}
<!-- svelte-ignore a11y-missing-attribute -->
<img
alt="segmentation mask identifying {label} within the uploaded file"
class="mask fit-height"
class:active={active == label}
class:inactive={active != label && active != null}
@ -107,10 +107,7 @@
{#if show_legend && _value}
<div class="legend">
{#each _value[1] as [_, label], i}
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
<button
class="legend-item"
style="background-color: {color_map && label in color_map
? color_map[label] + '88'
@ -124,7 +121,7 @@
on:click={() => handle_click(i)}
>
{label}
</div>
</button>
{/each}
</div>
{/if}

View File

@ -13,7 +13,7 @@
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script type="module" src="./src/main.ts"></script>

View File

@ -39,7 +39,7 @@
{$_("common.hosted_on")}
<a class="hf" href="https://huggingface.co/spaces"
><span class="space-logo">
<img src={space_logo} alt={`Hugging Face Space }`} />
<img src={space_logo} alt="Hugging Face Space" />
</span> Spaces</a
>
</span>

View File

@ -69,7 +69,7 @@
$: component_meta = selected_samples.map((sample_row) =>
sample_row.map((sample_cell, j) => ({
value: sample_cell,
component: component_map[components[j]] as ComponentType<SvelteComponent>
component: component_map[components[j]] as ComponentType<SvelteComponent>,
}))
);
</script>
@ -130,7 +130,7 @@
</div>
{:else}
<div class="table-wrap">
<table>
<table tabindex="0" role="grid">
<thead>
<tr class="tr-head">
{#each headers as header}

View File

@ -67,8 +67,7 @@
<div class="interpretation">
<canvas bind:this={saliency_layer} />
</div>
<!-- svelte-ignore a11y-missing-attribute -->
<img bind:this={image} src={original} />
<img bind:this={image} src={original} alt="uploaded input" />
</div>
</div>

View File

@ -20,10 +20,9 @@ test("updates frontend correctly", async ({ page }) => {
});
test("updates backend correctly", async ({ page }) => {
const min_slider = await page.getByLabel("min");
const max_slider = await page.getByLabel("max");
const num = await page.getByLabel("input");
const output = await page.getByLabel("out");
const min_slider = await page.getByLabel("number input for min");
const num = await page.getByLabel("input").first();
const output = await page.getByLabel("output");
await min_slider.fill("10");
await num.fill("15");

View File

@ -12,8 +12,8 @@ test("renders the correct elements", async ({ page }) => {
});
test("can run an api request and display the data", async ({ page }) => {
await page.getByLabel("Covid").check();
await page.getByLabel("Lung Cancer").check();
await page.getByTitle("Covid").check();
await page.getByTitle("Lung Cancer").check();
const run_button = await page.locator("button", { hasText: /Run/ }).first();

View File

@ -12,7 +12,7 @@ test("test inputs", async ({ page }) => {
await textbox2.fill("hello world");
await expect(textbox2).toHaveValue("hello world");
const number = await page.getByLabel("Number");
const number = await page.getByLabel("Number").first();
await expect(number).toHaveValue("42");
await number.fill("10");
await expect(number).toHaveValue("10");

View File

@ -4,9 +4,9 @@ test("selecting matplotlib should show matplotlib image and pressing clear shoul
page
}) => {
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Matplotlib" }).click();
await page.getByRole("option", { name: "Matplotlib" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByRole("option", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([
@ -26,9 +26,9 @@ test("selecting plotly should show plotly plot and pressing clear should clear o
page
}) => {
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Plotly" }).click();
await page.getByRole("option", { name: "Plotly" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByRole("option", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([
@ -44,9 +44,9 @@ test("selecting altair should show altair plot and pressing clear should clear o
page
}) => {
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "altair" }).click();
await page.getByRole("option", { name: "altair" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByRole("option", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([
@ -66,9 +66,9 @@ test("switching between all 3 plot types and pressing submit should update outpu
}) => {
//Matplotlib
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Matplotlib" }).click();
await page.getByRole("option", { name: "Matplotlib" }).click();
await page.getByLabel("Month").click();
await page.getByRole("button", { name: "January" }).click();
await page.getByRole("option", { name: "January" }).click();
await page.getByLabel("Social Distancing?").check();
await Promise.all([
@ -82,7 +82,7 @@ test("switching between all 3 plot types and pressing submit should update outpu
//Plotly
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Plotly" }).click();
await page.getByRole("option", { name: "Plotly" }).click();
await Promise.all([
page.click("text=Submit"),
@ -92,7 +92,7 @@ test("switching between all 3 plot types and pressing submit should update outpu
//Altair
await page.getByLabel("Plot Type").click();
await page.getByRole("button", { name: "Altair" }).click();
await page.getByRole("option", { name: "Altair" }).click();
await Promise.all([
page.click("text=Submit"),

View File

@ -34,7 +34,7 @@
id={elem_id}
>
{#if icon}
<img class="button-icon" src={icon_path} alt={`${value}-icon`} />
<img class="button-icon" src={icon_path} alt={`${value} icon`} />
{/if}
<slot />
</a>
@ -52,7 +52,7 @@
{disabled}
>
{#if icon}
<img class="button-icon" src={icon_path} alt={`${value}-icon`} />
<img class="button-icon" src={icon_path} alt={`${value} icon`} />
{/if}
<slot />
</button>

View File

@ -44,12 +44,17 @@
});
</script>
<button on:click={handle_copy} title="copy">
<button
on:click={handle_copy}
title="copy"
aria-roledescription={copied ? "Value copied" : "Copy value"}
aria-label={copied ? "Copied" : "Copy"}
>
{#if !copied}
<span><Copy /> </span>
<Copy />
{/if}
{#if copied}
<span><Check /></span>
<Check />
{/if}
</button>

View File

@ -28,12 +28,22 @@
<label class:disabled>
<input
bind:checked={value}
on:keydown={(event) => {
if (event.key === "Enter") {
value = !value;
dispatch("select", {
index: 0,
value: label,
selected: value,
});
}
}}
on:input={(evt) => {
value = evt.currentTarget.checked;
dispatch("select", {
index: 0,
value: label,
selected: evt.currentTarget.checked
selected: evt.currentTarget.checked,
});
}}
{disabled}
@ -77,6 +87,12 @@
background-color: var(--checkbox-background-color-selected);
}
input:checked:focus {
background-image: var(--checkbox-check);
background-color: var(--checkbox-background-color-selected);
border-color: var(--checkbox-border-color-focus);
}
input:hover {
border-color: var(--checkbox-border-color-hover);
background-color: var(--checkbox-background-color-hover);

View File

@ -58,11 +58,22 @@
dispatch("select", {
index: i,
value: choice[1],
selected: evt.currentTarget.checked
selected: evt.currentTarget.checked,
})}
on:keydown={(event) => {
if (event.key === "Enter") {
toggleChoice(choice[1]);
dispatch("select", {
index: i,
value: choice[1],
selected: !value.includes(choice[1]),
});
}
}}
checked={value.includes(choice[1])}
type="checkbox"
name="test"
name={choice[1]?.toString()}
title={choice[1]?.toString()}
/>
<span class="ml-2">{choice[0]}</span>
</label>
@ -125,14 +136,19 @@
background-color: var(--checkbox-background-color-selected);
}
input:checked:focus {
border-color: var(--checkbox-border-color-focus);
background-image: var(--checkbox-check);
background-color: var(--checkbox-background-color-selected);
}
input:hover {
border-color: var(--checkbox-border-color-hover);
background-color: var(--checkbox-background-color-hover);
}
input:focus {
input:not(:checked):focus {
border-color: var(--checkbox-border-color-focus);
background-color: var(--checkbox-background-color-focus);
}
input[disabled],

View File

@ -4,7 +4,7 @@
import {
EditorView,
keymap,
placeholder as placeholderExt
placeholder as placeholderExt,
} from "@codemirror/view";
import { StateEffect, EditorState, type Extension } from "@codemirror/state";
import { indentWithTab } from "@codemirror/commands";
@ -42,7 +42,7 @@
$: reconfigure(), lang_extension;
$: setDoc(value);
$: updateLines(lines);
$: updateLines();
function setDoc(newDoc: string): void {
if (view && newDoc !== view.state.doc.toString()) {
@ -50,13 +50,13 @@
changes: {
from: 0,
to: view.state.doc.length,
insert: newDoc
}
insert: newDoc,
},
});
}
}
function updateLines(newLines: number): void {
function updateLines(): void {
if (view) {
view.requestMeasure({ read: updateGutters });
}
@ -65,7 +65,7 @@
function createEditorView(): EditorView {
return new EditorView({
parent: element,
state: createEditorState(value)
state: createEditorState(value),
});
}
@ -119,7 +119,7 @@
),
FontTheme,
...getTheme(),
...extensions
...extensions,
];
return stateExtensions;
}
@ -127,36 +127,36 @@
const FontTheme = EditorView.theme({
"&": {
fontSize: "var(--text-sm)",
backgroundColor: "var(--border-color-secondary)"
backgroundColor: "var(--border-color-secondary)",
},
".cm-content": {
paddingTop: "5px",
paddingBottom: "5px",
color: "var(--body-text-color)",
fontFamily: "var(--font-mono)",
minHeight: "100%"
minHeight: "100%",
},
".cm-gutters": {
marginRight: "1px",
borderRight: "1px solid var(--border-color-primary)",
backgroundColor: "transparent",
color: "var(--body-text-color-subdued)"
color: "var(--body-text-color-subdued)",
},
".cm-focused": {
outline: "none"
outline: "none",
},
".cm-scroller": {
height: "auto"
height: "auto",
},
".cm-cursor": {
borderLeftColor: "var(--body-text-color)"
}
borderLeftColor: "var(--body-text-color)",
},
});
function createEditorState(_value: string | null | undefined): EditorState {
return EditorState.create({
doc: _value ?? undefined,
extensions: getExtensions()
extensions: getExtensions(),
});
}
@ -169,7 +169,8 @@
): Extension[] {
const extensions: Extension[] = [
EditorView.editable.of(!readonly),
EditorState.readOnly.of(readonly)
EditorState.readOnly.of(readonly),
EditorView.contentAttributes.of({ "aria-label": "Code input container" }),
];
if (basic) {
@ -202,7 +203,7 @@
function reconfigure(): void {
view?.dispatch({
effects: StateEffect.reconfigure.of(getExtensions())
effects: StateEffect.reconfigure.of(getExtensions()),
});
}

View File

@ -27,12 +27,21 @@
});
</script>
<button on:click={handle_copy} title="copy">
<!-- {#if !copied} -->
<span class="copy-text" class:copied><Copy /> </span>
<!-- {/if} -->
<button
on:click={handle_copy}
title="copy"
class:copied
aria-roledescription="Copy value"
aria-label="Copy"
>
<Copy />
{#if copied}
<span class="check" transition:fade><Check /></span>
<span
class="check"
transition:fade
aria-roledescription="Value copied"
aria-label="Copied"><Check /></span
>
{/if}
</button>

View File

@ -74,8 +74,6 @@
<div class="reference" bind:this={refElement} />
{#if show_options && !disabled}
<!-- TODO: fix-->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<ul
class="options"
transition:fly={{ duration: 200, y: 5 }}
@ -85,13 +83,11 @@
style:max-height={`calc(${max_height}px - var(--window-padding))`}
style:width={input_width + "px"}
bind:this={listElement}
role="listbox"
>
{#each filtered_indices as index}
<!-- TODO: fix-->
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<li
class="item"
role="button"
class:selected={selected_indices.includes(index)}
class:active={index === active_index}
class:bg-gray-100={index === active_index}
@ -99,6 +95,8 @@
data-index={index}
aria-label={choices[index][0]}
data-testid="dropdown-option"
role="option"
aria-selected={selected_indices.includes(index)}
>
<span class:hide={!selected_indices.includes(index)} class="inner-item">

View File

@ -221,13 +221,7 @@
<div class="wrap">
<div class="wrap-inner" class:show_options>
{#each selected_indices as s}
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions-->
<div
on:click|preventDefault={() => remove_selected_choice(s)}
class="token"
>
<div class="token">
<span>
{#if typeof s === "number"}
{choices_names[s]}
@ -236,7 +230,18 @@
{/if}
</span>
{#if !disabled}
<div class="token-remove" title={$_("common.remove") + " " + s}>
<div
class="token-remove"
on:click|preventDefault={() => remove_selected_choice(s)}
on:keydown|preventDefault={(event) => {
if (event.key === "Enter") {
remove_selected_choice(s);
}
}}
role="button"
tabindex="0"
title={$_("common.remove") + " " + s}
>
<Remove />
</div>
{/if}
@ -257,15 +262,20 @@
on:focus={handle_focus}
readonly={!filterable}
/>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions-->
{#if !disabled}
{#if selected_indices.length > 0}
<div
role="button"
tabindex="0"
class="token-remove remove-all"
title={$_("common.clear")}
on:click={remove_all}
on:keydown={(event) => {
if (event.key === "Enter") {
remove_all(event);
}
}}
>
<Remove />
</div>

View File

@ -127,7 +127,7 @@
if (selected_image !== null) {
dispatch("select", {
index: selected_image,
value: _value?.[selected_image][1]
value: _value?.[selected_image][1],
});
}
}
@ -160,7 +160,7 @@
container_element?.scrollTo({
left: pos < 0 ? 0 : pos,
behavior: "smooth"
behavior: "smooth",
});
}
@ -177,8 +177,7 @@
<Empty unpadded_box={true} size="large"><Image /></Empty>
{:else}
{#if selected_image !== null && allow_preview}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:keydown={on_keydown} class="preview">
<button on:keydown={on_keydown} class="preview">
<div class="icon-buttons">
{#if show_download_button}
<a
@ -195,24 +194,27 @@
on:clear={() => (selected_image = null)}
/>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img
data-testid="detailed-image"
<button
class="image-button"
on:click={(event) => handle_preview_click(event)}
src={_value[selected_image][0].data}
alt={_value[selected_image][1] || ""}
title={_value[selected_image][1] || null}
class:with-caption={!!_value[selected_image][1]}
style="height: calc(100% - {_value[selected_image][1]
? '80px'
: '60px'})"
loading="lazy"
/>
aria-label="detailed view of selected image"
>
<img
data-testid="detailed-image"
src={_value[selected_image][0].data}
alt={_value[selected_image][1] || ""}
title={_value[selected_image][1] || null}
class:with-caption={!!_value[selected_image][1]}
loading="lazy"
/>
</button>
{#if _value[selected_image][1]}
<div class="caption">
<caption class="caption">
{_value[selected_image][1]}
</div>
</caption>
{/if}
<div
bind:this={container_element}
@ -225,17 +227,18 @@
on:click={() => (selected_image = i)}
class="thumbnail-item thumbnail-small"
class:selected={selected_image === i}
aria-label={"Thumbnail " + (i + 1) + " of " + _value.length}
>
<img
src={image[0].data}
title={image[1] || null}
alt={image[1] || null}
alt=""
loading="lazy"
/>
</button>
{/each}
</div>
</div>
</button>
{/if}
<div
@ -263,6 +266,7 @@
class="thumbnail-item thumbnail-lg"
class:selected={selected_image === i}
on:click={() => (selected_image = i)}
aria-label={"Thumbnail " + (i + 1) + " of " + _value.length}
>
<img
alt={caption || ""}
@ -306,14 +310,19 @@
}
}
.image-button {
height: calc(100% - 60px);
width: 100%;
display: flex;
}
.preview img {
width: var(--size-full);
height: calc(var(--size-full) - 60px);
height: var(--size-full);
object-fit: contain;
}
.preview img.with-caption {
height: calc(var(--size-full) - 80px);
height: var(--size-full);
}
.caption {
@ -324,6 +333,7 @@
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
align-self: center;
}
.thumbnails {
@ -341,9 +351,7 @@
.thumbnail-item {
--ring-color: transparent;
position: relative;
box-shadow:
0 0 0 2px var(--ring-color),
var(--shadow-drop);
box-shadow: 0 0 0 2px var(--ring-color), var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--button-small-radius);
background: var(--background-fill-secondary);

View File

@ -5,13 +5,12 @@
export let selected = false;
</script>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-missing-attribute -->
<img
src={samples_dir + value}
class:table={type === "table"}
class:gallery={type === "gallery"}
class:selected
alt=""
/>
<style>

View File

@ -69,12 +69,12 @@
if (source === "webcam" && initial) {
value = {
image: detail,
mask: null
mask: null,
};
} else {
value = {
image: typeof value === "string" ? value : value?.image || null,
mask: detail
mask: detail,
};
}
} else if (
@ -278,7 +278,7 @@
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
<img
src={value.image || value}
alt="hello"
alt=""
class:webcam={source === "webcam" && mirror_webcam}
class:selectable
on:click={handle_click}

View File

@ -32,7 +32,7 @@
try {
stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: include_audio
audio: include_audio,
});
video_source.srcObject = stream;
video_source.muted = true;
@ -81,7 +81,7 @@
dispatch("capture", {
data: e.target.result,
name: "sample." + mimeType.substring(6),
is_example: false
is_example: false,
});
dispatch("stop_recording");
}
@ -102,7 +102,7 @@
return;
}
media_recorder = new MediaRecorder(stream, {
mimeType: mimeType
mimeType: mimeType,
});
media_recorder.addEventListener("dataavailable", function (e) {
recorded_blobs.push(e.data);
@ -125,21 +125,25 @@
<div class="wrap">
<!-- svelte-ignore a11y-media-has-caption -->
<!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
<video bind:this={video_source} class:flip={mirror_webcam} />
{#if !streaming}
<button on:click={mode === "image" ? take_picture : take_recording}>
<button
on:click={mode === "image" ? take_picture : take_recording}
aria-label={mode === "image" ? "capture photo" : "start recording"}
>
{#if mode === "video"}
{#if recording}
<div class="icon">
<div class="icon" title="stop recording">
<Square />
</div>
{:else}
<div class="icon">
<div class="icon" title="start recording">
<Circle />
</div>
{/if}
{:else}
<div class="icon">
<div class="icon" title="capture photo">
<Camera />
</div>
{/if}

View File

@ -58,16 +58,9 @@
/>
{/if}
</div>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
<img
src={value}
alt=""
class:selectable
on:click={handle_click}
loading="lazy"
/>
<button on:click={handle_click}>
<img src={value} alt="" class:selectable loading="lazy" />
</button>
{/if}
<style>

View File

@ -40,13 +40,19 @@
</script>
{#if value && value !== '""' && !is_empty(value)}
<button on:click={handle_copy}>
<button
on:click={handle_copy}
title="copy"
class={copied ? "" : "copy-text"}
aria-roledescription={copied ? "Copied value" : "Copy value"}
aria-label={copied ? "Copied" : "Copy"}
>
{#if copied}
<span in:fade={{ duration: 300 }}>
<Check />
</span>
{:else}
<span class="copy-text"><Copy /></span>
<Copy />
{/if}
</button>
<div class="json-holder">

View File

@ -48,8 +48,8 @@
depth={depth + 1}
key={i}
/><!--
-->{#if i !== Object.keys(value).length - 1}<!--
-->,
-->{#if i !== Object.keys(value).length - 1}<!--
-->,
{/if}
</div>
{/each}

View File

@ -14,21 +14,18 @@
</script>
<div class="container">
<div
<h2
class="output-class"
data-testid="label-output-value"
class:no-confidence={!("confidences" in value)}
style:background-color={color || "transparent"}
>
{value.label}
</div>
</h2>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events-->
<!-- svelte-ignore a11y-no-static-element-interactions-->
{#if typeof value === "object" && value.confidences}
{#each value.confidences as confidence_set, i}
<div
<button
class="confidence-set group"
data-testid={`${confidence_set.label}-confidence-set`}
class:selectable
@ -37,18 +34,31 @@
}}
>
<div class="inner-wrap">
<div class="bar" style="width: {confidence_set.confidence * 100}%" />
<div class="label">
<div class="text">{confidence_set.label}</div>
<meter
aria-labelledby="meter-text"
class="bar"
min="0"
max="100"
style="width: {confidence_set.confidence *
100}%; background: var(--stat-background-fill);
"
value={confidence_set.confidence * 100}
aria-label={Math.round(confidence_set.confidence * 100) + "%"}
/>
<dl class="label">
<dt id={`meter-text-${confidence_set.label}`} class="text">
{confidence_set.label}
</dt>
{#if value.confidences}
<div class="line" />
<div class="confidence">
<dd class="confidence">
{Math.round(confidence_set.confidence * 100)}%
</div>
</dd>
{/if}
</div>
</dl>
</div>
</div>
</button>
{/each}
{/if}
</div>
@ -75,6 +85,7 @@
color: var(--body-text-color);
line-height: var(--line-none);
font-family: var(--font-mono);
width: 100%;
}
.confidence-set:last-child {
@ -83,9 +94,13 @@
.inner-wrap {
flex: 1 1 0%;
display: flex;
flex-direction: column;
}
.bar {
appearance: none;
align-self: flex-start;
margin-bottom: var(--size-1);
border-radius: var(--radius-md);
background: var(--stat-background-fill);
@ -105,6 +120,10 @@
color: var(--color-accent);
}
.confidence-set:focus .label {
color: var(--color-accent);
}
.text {
line-height: var(--line-md);
}
@ -120,7 +139,4 @@
margin-left: auto;
text-align: right;
}
.selectable {
cursor: pointer;
}
</style>

View File

@ -46,6 +46,7 @@
<label class="block" class:container>
<BlockTitle {show_label} {info}>{label}</BlockTitle>
<input
aria-label={label}
type="number"
bind:value
min={minimum}

View File

@ -116,7 +116,7 @@
`https://cdn.pydata.org/bokeh/release/bokeh-widgets-${bokeh_version}.min.js`,
`https://cdn.pydata.org/bokeh/release/bokeh-tables-${bokeh_version}.min.js`,
`https://cdn.pydata.org/bokeh/release/bokeh-gl-${bokeh_version}.min.js`,
`https://cdn.pydata.org/bokeh/release/bokeh-api-${bokeh_version}.min.js`
`https://cdn.pydata.org/bokeh/release/bokeh-api-${bokeh_version}.min.js`,
];
function load_plugins(): HTMLScriptElement[] {
@ -201,8 +201,7 @@
</div>
{:else if type == "matplotlib"}
<div data-testid={"matplotlib"} class="matplotlib layout">
<!-- svelte-ignore a11y-missing-attribute -->
<img src={plot} />
<img src={plot} alt={`${value.chart} plot visualising provided data`} />
</div>
{:else}
<Empty unpadded_box={true} size="large"><PlotIcon /></Empty>

View File

@ -81,6 +81,7 @@
label:focus {
background: var(--checkbox-label-background-fill-focus);
}
label.selected {
background: var(--checkbox-label-background-fill-selected);
color: var(--checkbox-label-text-color-selected);
@ -101,8 +102,7 @@
}
input:checked,
input:checked:hover,
input:checked:focus {
input:checked:hover {
border-color: var(--checkbox-border-color-selected);
background-image: var(--radio-circle);
background-color: var(--checkbox-background-color-selected);
@ -118,6 +118,12 @@
background-color: var(--checkbox-background-color-focus);
}
input:checked:focus {
border-color: var(--checkbox-border-color-focus);
background-image: var(--radio-circle);
background-color: var(--checkbox-background-color-selected);
}
input[disabled],
.disabled {
cursor: not-allowed;

View File

@ -62,7 +62,12 @@ test("Slider Default Value And Label rendered", async ({ mount }) => {
}
});
await expect(component).toContainText("My Slider");
await expect(component.getByLabel("My Slider")).toHaveValue("3");
expect(
component.getByRole("spinbutton", {
name: "My Slider"
})
).toHaveValue("3");
});
test("Slider respects show_label", async ({ mount, page }) => {
@ -100,11 +105,28 @@ test("Slider Maximum/Minimum values", async ({ mount, page }) => {
}
}
});
const slider = component.getByLabel("My Slider");
await changeSlider(page, slider, slider, 1);
await expect(component.getByLabel("My Slider")).toHaveValue("10");
await changeSlider(page, slider, slider, 0);
await expect(component.getByLabel("My Slider")).toHaveValue("0");
const sliderNumberInput = component.getByRole("spinbutton", {
name: "My Slider"
});
await expect(sliderNumberInput).toHaveValue("3");
await sliderNumberInput.press("ArrowUp");
await expect(sliderNumberInput).toHaveValue("4");
const sliderRangeInput = component.getByRole("slider");
await sliderRangeInput.focus();
sliderRangeInput.press("ArrowRight");
await expect(sliderNumberInput).toHaveValue("5");
changeSlider(page, sliderRangeInput, sliderRangeInput, 2);
await expect(sliderNumberInput).toHaveValue("10");
});
test("Slider Change event", async ({ mount, page }) => {
@ -133,10 +155,17 @@ test("Slider Change event", async ({ mount, page }) => {
}
});
const slider = page.getByLabel("Slider");
const sliderNumberInput = component.getByRole("spinbutton", {
name: "My Slider"
});
await changeSlider(page, slider, slider, 0.7);
await expect(component.getByLabel("My Slider")).toHaveValue("7");
const sliderRangeInput = component.getByRole("slider");
await expect(sliderNumberInput).toHaveValue("3");
await changeSlider(page, sliderRangeInput, sliderRangeInput, 0.7);
await expect(sliderNumberInput).toHaveValue("7");
// More than one change event and one release event.
await expect(events.change).toBeGreaterThanOrEqual(1);

View File

@ -66,6 +66,7 @@
</label>
<input
aria-label={`number input for ${label}`}
data-testid="number-input"
type="number"
bind:value
@ -91,6 +92,7 @@
{step}
{disabled}
on:pointerup={handle_release}
aria-label={`range slider for ${label}`}
/>
<style>

View File

@ -3,7 +3,7 @@
beforeUpdate,
afterUpdate,
createEventDispatcher,
tick
tick,
} from "svelte";
import { BlockTitle } from "@gradio/atoms";
import { Copy, Check } from "@gradio/icons";
@ -92,7 +92,7 @@
const text = target.value;
const index: [number, number] = [
target.selectionStart as number,
target.selectionEnd as number
target.selectionEnd as number,
];
dispatch("select", { value: text.substring(...index), index: index });
}
@ -154,7 +154,7 @@
resize({ target: _el });
return {
destroy: () => _el.removeEventListener("input", resize)
destroy: () => _el.removeEventListener("input", resize),
};
}
</script>
@ -217,9 +217,17 @@
{:else}
{#if show_label && show_copy_button}
{#if copied}
<button in:fade={{ duration: 300 }}><Check /></button>
<button
in:fade={{ duration: 300 }}
aria-label="Copied"
aria-roledescription="Text copied"><Check /></button
>
{:else}
<button on:click={handle_copy} class="copy-text"><Copy /></button>
<button
on:click={handle_copy}
aria-label="Copy"
aria-roledescription="Copy text"><Copy /></button
>
{/if}
{/if}
<textarea

View File

@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { FileData } from "./types";
import { blobToBase64 } from "./utils";
export let filetype: string | null = null;
@ -39,7 +38,7 @@
if (include_file_metadata) {
var file_metadata: { name: string; size: number }[] = _files.map((f) => ({
name: f.name,
size: f.size
size: f.size,
}));
}
var load_file_data = [];
@ -53,13 +52,13 @@
if (parse_to_data_url) {
load_file_data = file_data.map((data, i) => ({
data,
...file_metadata[i]
...file_metadata[i],
}));
} else {
load_file_data = file_data.map((data, i) => ({
data: "",
blob: data,
...file_metadata[i]
...file_metadata[i],
}));
}
} else {
@ -85,10 +84,7 @@
}
</script>
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
<button
class:center
class:boundedheight
class:flex
@ -114,17 +110,18 @@
webkitdirectory={file_count === "directory" || undefined}
mozdirectory={file_count === "directory" || undefined}
/>
</div>
</button>
<style>
div {
button {
cursor: pointer;
width: var(--size-full);
height: var(--size-full);
}
.center {
text-align: center;
display: flex;
justify-content: center;
}
.flex {
display: flex;