@gradio/code 0.17.5 → 0.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @gradio/code
2
2
 
3
+ ## 0.17.7
4
+
5
+ ### Features
6
+
7
+ - [#13222](https://github.com/gradio-app/gradio/pull/13222) [`10b22a0`](https://github.com/gradio-app/gradio/commit/10b22a02b7cccccbd2cfee624040b3092a4347f0) - code tests. Thanks @pngwn!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/atoms@0.23.1
12
+ - @gradio/statustracker@0.14.0
13
+
14
+ ## 0.17.6
15
+
16
+ ### Dependency updates
17
+
18
+ - @gradio/utils@0.12.2
19
+ - @gradio/atoms@0.23.0
20
+ - @gradio/statustracker@0.13.1
21
+ - @gradio/upload@0.17.8
22
+
3
23
  ## 0.17.5
4
24
 
5
25
  ### Dependency updates
package/Code.test.ts ADDED
@@ -0,0 +1,440 @@
1
+ import { test, describe, afterEach, expect, vi } from "vitest";
2
+ import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
3
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
4
+ import event from "@testing-library/user-event";
5
+
6
+ import Code from "./Index.svelte";
7
+
8
+ const default_props = {
9
+ value: "print('hello')",
10
+ language: "python",
11
+ lines: 5,
12
+ interactive: true,
13
+ show_label: true,
14
+ label: "Code"
15
+ };
16
+
17
+ // Code uses BlockLabel (not BlockTitle/block-info) and conditionally renders it
18
+ // via {#if show_label}, so the shared show_label:false test (which expects the
19
+ // label to remain in the DOM with sr-only) does not apply. Custom label tests
20
+ // are written below in the "Props: show_label" describe block.
21
+ run_shared_prop_tests({
22
+ component: Code,
23
+ name: "Code",
24
+ base_props: {
25
+ value: "print('hello')",
26
+ language: "python",
27
+ lines: 5
28
+ },
29
+ has_label: false
30
+ });
31
+
32
+ describe("Props: show_label", () => {
33
+ afterEach(() => cleanup());
34
+
35
+ test("show_label=true renders the label text", async () => {
36
+ const { getByText } = await render(Code, {
37
+ ...default_props,
38
+ label: "My Code Editor",
39
+ show_label: true
40
+ });
41
+ expect(getByText("My Code Editor")).toBeVisible();
42
+ });
43
+
44
+ test("show_label=false hides the label from view", async () => {
45
+ const { queryByText } = await render(Code, {
46
+ ...default_props,
47
+ label: "My Code Editor",
48
+ show_label: false
49
+ });
50
+ // Code uses {#if show_label} so the BlockLabel is not rendered at all.
51
+ expect(queryByText("My Code Editor")).toBeNull();
52
+ });
53
+
54
+ test("label text is derived from the label prop", async () => {
55
+ const { getByText } = await render(Code, {
56
+ ...default_props,
57
+ label: "Custom Label Text",
58
+ show_label: true
59
+ });
60
+ expect(getByText("Custom Label Text")).toBeTruthy();
61
+ });
62
+ });
63
+
64
+ describe("Code", () => {
65
+ afterEach(() => cleanup());
66
+
67
+ test("renders the code editor when a value is provided", async () => {
68
+ const { getByLabelText } = await render(Code, default_props);
69
+ expect(getByLabelText("Code input container")).toBeTruthy();
70
+ });
71
+
72
+ test("renders the editor when interactive with no value", async () => {
73
+ const { getByLabelText } = await render(Code, {
74
+ ...default_props,
75
+ value: ""
76
+ });
77
+ expect(getByLabelText("Code input container")).toBeTruthy();
78
+ });
79
+
80
+ test("renders empty state (no editor) when value is empty and not interactive", async () => {
81
+ const { queryByLabelText } = await render(Code, {
82
+ ...default_props,
83
+ value: "",
84
+ interactive: false
85
+ });
86
+ expect(queryByLabelText("Code input container")).toBeNull();
87
+ });
88
+
89
+ test("renders the editor (not empty state) when value is present and not interactive", async () => {
90
+ const { getByLabelText } = await render(Code, {
91
+ ...default_props,
92
+ interactive: false
93
+ });
94
+ expect(getByLabelText("Code input container")).toBeTruthy();
95
+ });
96
+ });
97
+
98
+ describe("Props: interactive", () => {
99
+ afterEach(() => cleanup());
100
+
101
+ test("interactive=true makes the editor contenteditable", async () => {
102
+ const { getByLabelText } = await render(Code, {
103
+ ...default_props,
104
+ interactive: true
105
+ });
106
+ const editor = getByLabelText("Code input container");
107
+ expect(editor).toHaveAttribute("contenteditable", "true");
108
+ });
109
+
110
+ test("interactive=false makes the editor read-only", async () => {
111
+ const { getByLabelText } = await render(Code, {
112
+ ...default_props,
113
+ interactive: false
114
+ });
115
+ const editor = getByLabelText("Code input container");
116
+ expect(editor).toHaveAttribute("contenteditable", "false");
117
+ });
118
+ });
119
+
120
+ describe("Props: buttons", () => {
121
+ afterEach(() => cleanup());
122
+
123
+ test("default (null) shows both copy and download buttons", async () => {
124
+ const { getByLabelText, getByRole } = await render(Code, {
125
+ ...default_props,
126
+ buttons: null
127
+ });
128
+ expect(getByLabelText("Copy")).toBeTruthy();
129
+ expect(getByLabelText("Download")).toBeTruthy();
130
+ });
131
+
132
+ test("buttons=['copy', 'download'] shows both buttons", async () => {
133
+ const { getByLabelText } = await render(Code, {
134
+ ...default_props,
135
+ buttons: ["copy", "download"]
136
+ });
137
+ expect(getByLabelText("Copy")).toBeTruthy();
138
+ expect(getByLabelText("Download")).toBeTruthy();
139
+ });
140
+
141
+ test("buttons=['copy'] shows only the copy button", async () => {
142
+ const { getByLabelText, queryByLabelText } = await render(Code, {
143
+ ...default_props,
144
+ buttons: ["copy"]
145
+ });
146
+ expect(getByLabelText("Copy")).toBeTruthy();
147
+ expect(queryByLabelText("Download")).toBeNull();
148
+ });
149
+
150
+ test("buttons=['download'] shows only the download button", async () => {
151
+ const { getByLabelText, queryByLabelText } = await render(Code, {
152
+ ...default_props,
153
+ buttons: ["download"]
154
+ });
155
+ expect(getByLabelText("Download")).toBeTruthy();
156
+ expect(queryByLabelText("Copy")).toBeNull();
157
+ });
158
+
159
+ test("buttons=[] shows no copy or download buttons", async () => {
160
+ const { queryByLabelText } = await render(Code, {
161
+ ...default_props,
162
+ buttons: []
163
+ });
164
+ expect(queryByLabelText("Copy")).toBeNull();
165
+ expect(queryByLabelText("Download")).toBeNull();
166
+ });
167
+
168
+ test("custom button is rendered in the toolbar", async () => {
169
+ const { getByRole } = await render(Code, {
170
+ ...default_props,
171
+ buttons: [{ id: 42, value: "Run", icon: null }]
172
+ });
173
+ expect(getByRole("button", { name: "Run" })).toBeTruthy();
174
+ });
175
+
176
+ test("clicking a custom button fires custom_button_click with the button id", async () => {
177
+ const { listen, getByRole } = await render(Code, {
178
+ ...default_props,
179
+ buttons: [{ id: 99, value: "Execute", icon: null }]
180
+ });
181
+ const custom = listen("custom_button_click");
182
+ await fireEvent.click(getByRole("button", { name: "Execute" }));
183
+ expect(custom).toHaveBeenCalledTimes(1);
184
+ expect(custom).toHaveBeenCalledWith({ id: 99 });
185
+ });
186
+ });
187
+
188
+ describe("Props: language", () => {
189
+ afterEach(() => cleanup());
190
+
191
+ const languages = [
192
+ "python",
193
+ "javascript",
194
+ "typescript",
195
+ "json",
196
+ "html",
197
+ "css",
198
+ "markdown",
199
+ "shell",
200
+ "sql",
201
+ "yaml",
202
+ "dockerfile",
203
+ "r",
204
+ "c",
205
+ "cpp"
206
+ ] as const;
207
+
208
+ for (const lang of languages) {
209
+ test(`language='${lang}' renders without error`, async () => {
210
+ const { getByLabelText } = await render(Code, {
211
+ ...default_props,
212
+ language: lang
213
+ });
214
+ expect(getByLabelText("Code input container")).toBeTruthy();
215
+ });
216
+ }
217
+
218
+ test("language=null renders without error", async () => {
219
+ const { getByLabelText } = await render(Code, {
220
+ ...default_props,
221
+ language: null
222
+ });
223
+ expect(getByLabelText("Code input container")).toBeTruthy();
224
+ });
225
+ });
226
+
227
+ test.todo(
228
+ "VISUAL: lines=10 sets the minimum visible height of the editor to 10 line-heights — needs Playwright screenshot comparison"
229
+ );
230
+
231
+ test.todo(
232
+ "VISUAL: max_lines=3 caps the editor height at 3 line-heights and enables scrolling for longer content — needs Playwright screenshot comparison"
233
+ );
234
+
235
+ test.todo(
236
+ "VISUAL: show_line_numbers=true renders the line number gutter — needs Playwright screenshot comparison"
237
+ );
238
+
239
+ test.todo(
240
+ "VISUAL: show_line_numbers=false hides the line number gutter — needs Playwright screenshot comparison"
241
+ );
242
+
243
+ test.todo(
244
+ "VISUAL: wrap_lines=true wraps long lines within the container width instead of scrolling — needs Playwright screenshot comparison"
245
+ );
246
+
247
+ test.todo(
248
+ "VISUAL: wrap_lines=false lets long lines overflow horizontally — needs Playwright screenshot comparison"
249
+ );
250
+
251
+ test.todo(
252
+ "VISUAL: autocomplete=true shows an autocompletion dropdown when typing in supported languages — needs Playwright interaction test"
253
+ );
254
+
255
+ describe("Events", () => {
256
+ afterEach(() => cleanup());
257
+
258
+ test("change: not fired on initial mount", async () => {
259
+ const { listen } = await render(Code, default_props);
260
+ // retrospective: true replays any events buffered during render
261
+ const change = listen("change", { retrospective: true });
262
+ expect(change).not.toHaveBeenCalled();
263
+ });
264
+
265
+ test("change: fired when value is updated via set_data", async () => {
266
+ const { listen, set_data } = await render(Code, default_props);
267
+ const change = listen("change");
268
+ await set_data({ value: "print('world')" });
269
+ expect(change).toHaveBeenCalledTimes(1);
270
+ });
271
+
272
+ test("change: not fired when the same value is set twice", async () => {
273
+ const { listen, set_data } = await render(Code, default_props);
274
+ const change = listen("change");
275
+ await set_data({ value: "x = 1" });
276
+ await set_data({ value: "x = 1" });
277
+ expect(change).toHaveBeenCalledTimes(1);
278
+ });
279
+
280
+ test("input: fired when the user types in the editor", async () => {
281
+ const { listen, getByLabelText } = await render(Code, default_props);
282
+ const input = listen("input");
283
+ const editor = getByLabelText("Code input container");
284
+ editor.focus();
285
+ await event.keyboard("a");
286
+ await waitFor(() => expect(input).toHaveBeenCalledTimes(1));
287
+ });
288
+
289
+ test("focus: fired when the editor gains focus", async () => {
290
+ const { listen, getByLabelText } = await render(Code, default_props);
291
+ const focus = listen("focus");
292
+ const editor = getByLabelText("Code input container");
293
+ editor.focus();
294
+ expect(focus).toHaveBeenCalledTimes(1);
295
+ });
296
+
297
+ test("blur: fired when the editor loses focus", async () => {
298
+ const { listen, getByLabelText } = await render(Code, default_props);
299
+ const blur = listen("blur");
300
+ const editor = getByLabelText("Code input container");
301
+ editor.focus();
302
+ editor.blur();
303
+ expect(blur).toHaveBeenCalledTimes(1);
304
+ });
305
+
306
+ test("custom_button_click: carries the button id in the payload", async () => {
307
+ const { listen, getByRole } = await render(Code, {
308
+ ...default_props,
309
+ buttons: [{ id: 7, value: "Go", icon: null }]
310
+ });
311
+ const custom = listen("custom_button_click");
312
+ await fireEvent.click(getByRole("button", { name: "Go" }));
313
+ expect(custom).toHaveBeenCalledWith({ id: 7 });
314
+ });
315
+ });
316
+
317
+ describe("get_data / set_data", () => {
318
+ afterEach(() => cleanup());
319
+
320
+ test("get_data returns the initial value", async () => {
321
+ const { get_data } = await render(Code, default_props);
322
+ const data = await get_data();
323
+ expect(data.value).toBe("print('hello')");
324
+ });
325
+
326
+ test("set_data updates the value returned by get_data", async () => {
327
+ const { get_data, set_data } = await render(Code, default_props);
328
+ await set_data({ value: "x = 42" });
329
+ const data = await get_data();
330
+ expect(data.value).toBe("x = 42");
331
+ });
332
+
333
+ test("set_data / get_data round-trip", async () => {
334
+ const { get_data, set_data } = await render(Code, default_props);
335
+ const new_value = "def foo():\n return 1";
336
+ await set_data({ value: new_value });
337
+ const data = await get_data();
338
+ expect(data.value).toBe(new_value);
339
+ });
340
+
341
+ test("user typing is reflected in get_data", async () => {
342
+ const { get_data, getByLabelText } = await render(Code, {
343
+ ...default_props,
344
+ value: ""
345
+ });
346
+ const editor = getByLabelText("Code input container");
347
+ editor.focus();
348
+ await event.keyboard("hi");
349
+ await waitFor(async () => {
350
+ const data = await get_data();
351
+ expect(data.value).toContain("hi");
352
+ });
353
+ });
354
+ });
355
+
356
+ describe("Copy button", () => {
357
+ afterEach(() => cleanup());
358
+
359
+ test("clicking the copy button writes the code value to the clipboard", async () => {
360
+ // navigator.clipboard is unavailable in insecure (non-HTTPS) test origins
361
+ // so we mock writeText to prevent the silent no-op in Copy.svelte's `if
362
+ // ("clipboard" in navigator)` guard. This mock makes clipboard available.
363
+ Object.defineProperty(navigator, "clipboard", {
364
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
365
+ writable: true,
366
+ configurable: true
367
+ });
368
+
369
+ const { getByLabelText } = await render(Code, {
370
+ ...default_props,
371
+ value: "my code"
372
+ });
373
+ await fireEvent.click(getByLabelText("Copy"));
374
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith("my code");
375
+ });
376
+
377
+ test.todo(
378
+ "VISUAL: copy button icon changes from Copy to Check for ~2 seconds after clicking — needs Playwright screenshot comparison"
379
+ );
380
+ });
381
+
382
+ describe("Download button", () => {
383
+ afterEach(() => cleanup());
384
+
385
+ test("download link has filename extension matching the language", async () => {
386
+ const { getByRole } = await render(Code, {
387
+ ...default_props,
388
+ language: "python",
389
+ value: "print('hello')"
390
+ });
391
+ // The Download button is inside an <a download="file.py"> anchor
392
+ const link = getByRole("link");
393
+ expect(link).toHaveAttribute("download", "file.py");
394
+ });
395
+
396
+ test("download link href is a blob URL", async () => {
397
+ const { getByRole } = await render(Code, {
398
+ ...default_props,
399
+ value: "some code"
400
+ });
401
+ const link = getByRole("link");
402
+ expect(link.getAttribute("href")).toMatch(/^blob:/);
403
+ });
404
+
405
+ test.todo(
406
+ "VISUAL: download button icon changes from Download to Check for ~2 seconds after clicking — needs Playwright screenshot comparison"
407
+ );
408
+ });
409
+
410
+ describe("Edge cases", () => {
411
+ afterEach(() => cleanup());
412
+
413
+ test("null value renders without error", async () => {
414
+ const { queryByLabelText } = await render(Code, {
415
+ ...default_props,
416
+ value: null
417
+ });
418
+ // null value with non-interactive should show empty state (no editor)
419
+ // but with interactive=true, editor is shown
420
+ expect(queryByLabelText("Code input container")).toBeTruthy();
421
+ });
422
+
423
+ test("empty string value with interactive=true shows the editor", async () => {
424
+ const { getByLabelText } = await render(Code, {
425
+ ...default_props,
426
+ value: "",
427
+ interactive: true
428
+ });
429
+ expect(getByLabelText("Code input container")).toBeTruthy();
430
+ });
431
+
432
+ test("value with leading and trailing whitespace is displayed", async () => {
433
+ const { get_data } = await render(Code, {
434
+ ...default_props,
435
+ value: " hello "
436
+ });
437
+ const data = await get_data();
438
+ expect(data.value).toBe(" hello ");
439
+ });
440
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tests for Download.svelte's language → file-extension mapping.
3
+ *
4
+ * Download.svelte is not a Gradio component so the tootils render() utility
5
+ * cannot mount it with props correctly (non-shared props are wrapped under a
6
+ * `props` key rather than spread at the top level). We therefore test the
7
+ * extension mapping through the full Code component, which is the realistic
8
+ * usage path anyway.
9
+ */
10
+ import { test, describe, afterEach, expect } from "vitest";
11
+ import { cleanup, render } from "@self/tootils/render";
12
+
13
+ import Code from "./Index.svelte";
14
+
15
+ const base_props = {
16
+ value: "// code",
17
+ lines: 5,
18
+ interactive: false,
19
+ show_label: false,
20
+ label: "Code",
21
+ buttons: ["download"] as const
22
+ };
23
+
24
+ function get_download_link(container: HTMLElement): HTMLAnchorElement {
25
+ // The Download component renders an <a download="file.ext"> anchor.
26
+ // No accessible name or role distinguishes it better than the download
27
+ // attribute itself, so querySelector is used as the last-resort query.
28
+ const link = container.querySelector<HTMLAnchorElement>("a[download]");
29
+ if (!link) throw new Error("No download link found in rendered output");
30
+ return link;
31
+ }
32
+
33
+ describe("Download: language → file extension mapping", () => {
34
+ afterEach(() => cleanup());
35
+
36
+ const cases: Array<[string, string]> = [
37
+ ["python", "py"],
38
+ ["py", "py"],
39
+ ["javascript", "js"],
40
+ ["js", "js"],
41
+ ["typescript", "ts"],
42
+ ["ts", "ts"],
43
+ ["shell", "sh"],
44
+ ["sh", "sh"],
45
+ ["markdown", "md"],
46
+ ["md", "md"],
47
+ ["json", "json"],
48
+ ["html", "html"],
49
+ ["css", "css"],
50
+ ["yaml", "yaml"],
51
+ ["yml", "yml"],
52
+ ["dockerfile", "dockerfile"],
53
+ ["latex", "tex"],
54
+ ["r", "r"],
55
+ ["c", "c"],
56
+ ["cpp", "cpp"]
57
+ ];
58
+
59
+ for (const [language, expected_ext] of cases) {
60
+ test(`language='${language}' produces download filename 'file.${expected_ext}'`, async () => {
61
+ const { container } = await render(Code, {
62
+ ...base_props,
63
+ language
64
+ });
65
+ const link = get_download_link(container);
66
+ expect(link).toHaveAttribute("download", `file.${expected_ext}`);
67
+ });
68
+ }
69
+
70
+ test("unknown language falls back to 'file.txt'", async () => {
71
+ const { container } = await render(Code, {
72
+ ...base_props,
73
+ language: "brainfuck"
74
+ });
75
+ const link = get_download_link(container);
76
+ expect(link).toHaveAttribute("download", "file.txt");
77
+ });
78
+
79
+ test("null language falls back to 'file.txt'", async () => {
80
+ const { container } = await render(Code, {
81
+ ...base_props,
82
+ language: null
83
+ });
84
+ const link = get_download_link(container);
85
+ expect(link).toHaveAttribute("download", "file.txt");
86
+ });
87
+ });
88
+
89
+ describe("Download: link structure", () => {
90
+ afterEach(() => cleanup());
91
+
92
+ test("download link href is a blob URL", async () => {
93
+ const { container } = await render(Code, {
94
+ ...base_props,
95
+ language: "python"
96
+ });
97
+ const link = get_download_link(container);
98
+ expect(link.getAttribute("href")).toMatch(/^blob:/);
99
+ });
100
+
101
+ test("download link is visible", async () => {
102
+ const { container } = await render(Code, {
103
+ ...base_props,
104
+ language: "python"
105
+ });
106
+ const link = get_download_link(container);
107
+ expect(link).toBeVisible();
108
+ });
109
+
110
+ test("download link contains a button with label 'Download'", async () => {
111
+ const { getByLabelText } = await render(Code, {
112
+ ...base_props,
113
+ language: "python"
114
+ });
115
+ expect(getByLabelText("Download")).toBeTruthy();
116
+ });
117
+
118
+ test.todo(
119
+ "VISUAL: clicking the download link changes the icon from Download to Check for ~2 seconds — needs Playwright screenshot comparison"
120
+ );
121
+ });
package/Index.svelte CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  let label = $derived(gradio.shared.label || gradio.i18n("code.code"));
25
25
  let old_value = $state(gradio.props.value);
26
- let first_change = true;
26
+ let first_change = $state(true);
27
27
 
28
28
  $effect(() => {
29
29
  if (first_change) {
package/dist/Index.svelte CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  let label = $derived(gradio.shared.label || gradio.i18n("code.code"));
25
25
  let old_value = $state(gradio.props.value);
26
- let first_change = true;
26
+ let first_change = $state(true);
27
27
 
28
28
  $effect(() => {
29
29
  if (first_change) {
@@ -32,4 +32,4 @@
32
32
  });
33
33
  </script>
34
34
 
35
- <IconButton Icon={copied ? Check : Copy} onclick={handle_copy} />
35
+ <IconButton Icon={copied ? Check : Copy} label="Copy" onclick={handle_copy} />
@@ -63,5 +63,5 @@
63
63
  href={download_value}
64
64
  onclick={copy_feedback}
65
65
  >
66
- <IconButton Icon={copied ? Check : Download} />
66
+ <IconButton Icon={copied ? Check : Download} label="Download" />
67
67
  </DownloadLink>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/code",
3
- "version": "0.17.5",
3
+ "version": "0.17.7",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -27,11 +27,11 @@
27
27
  "cm6-theme-basic-dark": "^0.2.0",
28
28
  "cm6-theme-basic-light": "^0.2.0",
29
29
  "codemirror": "^6.0.2",
30
- "@gradio/atoms": "^0.22.2",
30
+ "@gradio/atoms": "^0.23.1",
31
+ "@gradio/statustracker": "^0.14.0",
31
32
  "@gradio/icons": "^0.15.1",
32
- "@gradio/statustracker": "^0.13.0",
33
- "@gradio/upload": "^0.17.7",
34
- "@gradio/utils": "^0.12.1"
33
+ "@gradio/upload": "^0.17.8",
34
+ "@gradio/utils": "^0.12.2"
35
35
  },
36
36
  "main_changeset": true,
37
37
  "main": "./Index.svelte",
@@ -51,7 +51,7 @@
51
51
  "./package.json": "./package.json"
52
52
  },
53
53
  "devDependencies": {
54
- "@gradio/preview": "^0.16.1"
54
+ "@gradio/preview": "^0.16.2"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "svelte": "^5.48.0"
@@ -32,4 +32,4 @@
32
32
  });
33
33
  </script>
34
34
 
35
- <IconButton Icon={copied ? Check : Copy} onclick={handle_copy} />
35
+ <IconButton Icon={copied ? Check : Copy} label="Copy" onclick={handle_copy} />
@@ -63,5 +63,5 @@
63
63
  href={download_value}
64
64
  onclick={copy_feedback}
65
65
  >
66
- <IconButton Icon={copied ? Check : Download} />
66
+ <IconButton Icon={copied ? Check : Download} label="Download" />
67
67
  </DownloadLink>