@marimo-team/frontend 0.25.2-dev90 → 0.25.2-dev92

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/dist/index.html CHANGED
@@ -66,7 +66,7 @@
66
66
  <marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
67
67
  <!-- /TODO -->
68
68
  <title>{{ title }}</title>
69
- <script type="module" crossorigin src="./assets/index-CqJKdR1j.js"></script>
69
+ <script type="module" crossorigin src="./assets/index-C6-WDESh.js"></script>
70
70
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-B0Z9INg1.js">
71
71
  <link rel="modulepreload" crossorigin href="./assets/react-CUdNIagt.js">
72
72
  <link rel="modulepreload" crossorigin href="./assets/compiler-runtime-BQ9S8nyo.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/frontend",
3
- "version": "0.25.2-dev90",
3
+ "version": "0.25.2-dev92",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,54 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { useForm, useWatch } from "react-hook-form";
4
+ import { describe, expect, test } from "vitest";
5
+ import { SetupMocks } from "@/__mocks__/common";
6
+ import { Form } from "@/components/ui/form";
7
+ import { SliderField } from "../components/form-fields";
8
+
9
+ SetupMocks.resizeObserver();
10
+ function Harness({ label = "Opacity" }: { label?: string }) {
11
+ const form = useForm({ defaultValues: { xAxis: { width: 400 } } });
12
+ const value = useWatch({ control: form.control, name: "xAxis.width" });
13
+ return (
14
+ <Form {...form}>
15
+ <SliderField
16
+ fieldName="xAxis.width"
17
+ label={label}
18
+ defaultValue={400}
19
+ start={200}
20
+ stop={800}
21
+ step={10}
22
+ />
23
+ <output aria-label="Current opacity">{value}</output>
24
+ </Form>
25
+ );
26
+ }
27
+
28
+ describe("SliderField", () => {
29
+ test("names the chart-form thumb and preserves keyboard value updates", () => {
30
+ const { rerender } = render(<Harness />);
31
+ fireEvent.keyDown(screen.getByRole("slider", { name: "Opacity" }), {
32
+ key: "ArrowRight",
33
+ });
34
+ expect(screen.getByLabelText("Current opacity")).toHaveTextContent("410");
35
+ rerender(<Harness label="Updated opacity" />);
36
+ expect(
37
+ screen.getByRole("slider", { name: "Updated opacity" }),
38
+ ).toHaveAttribute("aria-valuenow", "410");
39
+ });
40
+
41
+ test("independently associates repeated chart form instances", () => {
42
+ render(
43
+ <>
44
+ <Harness />
45
+ <Harness />
46
+ </>,
47
+ );
48
+ const thumbs = screen.getAllByRole("slider", { name: "Opacity" });
49
+ expect(thumbs).toHaveLength(2);
50
+ expect(thumbs[0].getAttribute("aria-labelledby")).not.toBe(
51
+ thumbs[1].getAttribute("aria-labelledby"),
52
+ );
53
+ });
54
+ });
@@ -358,6 +358,7 @@ export const SliderField = ({
358
358
  className,
359
359
  }: SliderFieldProps) => {
360
360
  const form = useFormContext();
361
+ const labelId = React.useId();
361
362
 
362
363
  return (
363
364
  <FormField
@@ -378,10 +379,11 @@ export const SliderField = ({
378
379
  <FormItem
379
380
  className={cn("flex flex-row items-center gap-2 w-1/2", className)}
380
381
  >
381
- <FormLabel>{label}</FormLabel>
382
+ <FormLabel id={labelId}>{label}</FormLabel>
382
383
  <FormControl>
383
384
  <Slider
384
385
  {...field}
386
+ aria-labelledby={labelId}
385
387
  id={fieldName}
386
388
  className="relative flex items-center select-none"
387
389
  value={[numericValue]}
@@ -27,4 +27,11 @@ describe("Slider", () => {
27
27
 
28
28
  expect(onValueChange).toHaveBeenCalledWith([2 * step]);
29
29
  });
30
+
31
+ it("forwards a direct accessible name to the thumb", () => {
32
+ const { getByRole } = render(
33
+ <Slider aria-label="Opacity" value={[50]} valueMap={(v) => v} />,
34
+ );
35
+ expect(getByRole("slider")).toHaveAccessibleName("Opacity");
36
+ });
30
37
  });
@@ -57,6 +57,8 @@ const Slider = React.forwardRef<
57
57
  <TooltipRoot delayDuration={0} open={open}>
58
58
  <TooltipTrigger asChild={true}>
59
59
  <SliderPrimitive.Thumb
60
+ aria-label={props["aria-label"]}
61
+ aria-labelledby={props["aria-labelledby"]}
60
62
  data-testid="thumb"
61
63
  className="block h-4 w-4 rounded-full shadow-xs-solid border border-blue-500 dark:border-primary dark:bg-accent bg-white hover:bg-blue-300 focus:bg-blue-300 transition-colors focus-visible:outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50"
62
64
  onFocus={openActions.setTrue}
@@ -152,6 +152,7 @@ const SliderComponent = ({
152
152
  disabled,
153
153
  }: SliderProps): JSX.Element => {
154
154
  const id = useId();
155
+ const labelId = useId();
155
156
  const { locale } = useLocale();
156
157
 
157
158
  // Hold internal value
@@ -225,6 +226,7 @@ const SliderComponent = ({
225
226
  <Labeled
226
227
  label={label}
227
228
  controlId={id}
229
+ labelId={labelId}
228
230
  align={orientation === "horizontal" ? "left" : "top"}
229
231
  fullWidth={fullWidth}
230
232
  className={cn(fullWidth && "my-1 w-full")}
@@ -238,6 +240,8 @@ const SliderComponent = ({
238
240
  >
239
241
  <Slider
240
242
  id={id}
243
+ aria-labelledby={label ? labelId : undefined}
244
+ aria-label={label ? undefined : "Slider"}
241
245
  className={cn(
242
246
  "relative flex items-center select-none",
243
247
  !fullWidth && "data-[orientation=horizontal]:w-36 ",
@@ -0,0 +1,136 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { describe, expect, test, vi } from "vitest";
4
+ import { SetupMocks } from "@/__mocks__/common";
5
+ import { SliderPlugin } from "../SliderPlugin";
6
+
7
+ SetupMocks.resizeObserver();
8
+ const plugin = new SliderPlugin();
9
+ function props(label: string | null) {
10
+ return {
11
+ host: document.createElement("div"),
12
+ value: 5,
13
+ setValue: vi.fn(),
14
+ functions: {},
15
+ data: plugin.validator.parse({
16
+ initialValue: 5,
17
+ start: 0,
18
+ stop: 10,
19
+ step: 1,
20
+ steps: null,
21
+ label,
22
+ }),
23
+ };
24
+ }
25
+
26
+ describe("SliderPlugin accessible labels", () => {
27
+ test("names the actual thumb from formatted labels and updates its reference", () => {
28
+ const initial = props("<strong>Price &amp; tax</strong>");
29
+ const { rerender } = render(plugin.render(initial));
30
+ const thumb = screen.getByRole("slider", { name: "Price & tax" });
31
+ const labelId = thumb.getAttribute("aria-labelledby");
32
+ rerender(
33
+ plugin.render({
34
+ ...initial,
35
+ data: { ...initial.data, label: "Updated price" },
36
+ }),
37
+ );
38
+ const updatedThumb = screen.getByRole("slider", { name: "Updated price" });
39
+ expect(updatedThumb).toHaveAccessibleName("Updated price");
40
+ expect(updatedThumb).toHaveAttribute("aria-labelledby", labelId);
41
+ rerender(
42
+ plugin.render({ ...initial, data: { ...initial.data, label: null } }),
43
+ );
44
+ expect(screen.getByRole("slider")).toHaveAccessibleName("Slider");
45
+ expect(screen.getByRole("slider")).not.toHaveAttribute("aria-labelledby");
46
+ });
47
+
48
+ test.each([null, ""])("retains a fallback name for label %s", (label) => {
49
+ render(plugin.render(props(label)));
50
+ expect(screen.getByRole("slider")).toHaveAccessibleName("Slider");
51
+ });
52
+
53
+ test("keeps repeated widget labels independently associated", () => {
54
+ render(
55
+ <>
56
+ {plugin.render(props("Price"))}
57
+ {plugin.render(props("Price"))}
58
+ </>,
59
+ );
60
+ const thumbs = screen.getAllByRole("slider", { name: "Price" });
61
+ expect(thumbs).toHaveLength(2);
62
+ expect(thumbs[0].getAttribute("aria-labelledby")).not.toBe(
63
+ thumbs[1].getAttribute("aria-labelledby"),
64
+ );
65
+ });
66
+
67
+ test.each([false, true])(
68
+ "preserves keyboard commits with debounce=%s",
69
+ (debounce) => {
70
+ const initial = props("Price");
71
+ render(
72
+ plugin.render({ ...initial, data: { ...initial.data, debounce } }),
73
+ );
74
+ fireEvent.keyDown(screen.getByRole("slider"), { key: "ArrowRight" });
75
+ expect(initial.setValue).toHaveBeenCalledWith(6);
76
+ },
77
+ );
78
+
79
+ test("preserves maximum and minimum keyboard boundaries", () => {
80
+ const initial = props("Price");
81
+ const { rerender } = render(plugin.render({ ...initial, value: 10 }));
82
+ fireEvent.keyDown(screen.getByRole("slider"), { key: "ArrowRight" });
83
+ expect(initial.setValue).not.toHaveBeenCalled();
84
+ rerender(plugin.render({ ...initial, value: 0 }));
85
+ fireEvent.keyDown(screen.getByRole("slider"), { key: "ArrowLeft" });
86
+ expect(initial.setValue).not.toHaveBeenCalled();
87
+ });
88
+
89
+ test("retains the name while disabled and removes keyboard focusability", () => {
90
+ const initial = props("Price");
91
+ render(
92
+ plugin.render({ ...initial, data: { ...initial.data, disabled: true } }),
93
+ );
94
+ const thumb = screen.getByRole("slider", { name: "Price" });
95
+ expect(thumb).not.toHaveAttribute("tabindex");
96
+ expect(thumb).toHaveAttribute("data-disabled");
97
+ });
98
+
99
+ test("preserves vertical keyboard interaction", () => {
100
+ const initial = props("Price");
101
+ render(
102
+ plugin.render({
103
+ ...initial,
104
+ data: { ...initial.data, orientation: "vertical" },
105
+ }),
106
+ );
107
+ const thumb = screen.getByRole("slider", { name: "Price" });
108
+ expect(thumb).toHaveAttribute("aria-orientation", "vertical");
109
+ fireEvent.keyDown(thumb, { key: "ArrowUp" });
110
+ expect(initial.setValue).toHaveBeenCalledWith(6);
111
+ });
112
+
113
+ test("preserves mapped values while naming the index-space thumb", () => {
114
+ const initial = props("Mapped price");
115
+ render(
116
+ plugin.render({
117
+ ...initial,
118
+ value: 1,
119
+ data: {
120
+ ...initial.data,
121
+ start: 0,
122
+ stop: 2,
123
+ steps: [10, 20, 50],
124
+ showValue: true,
125
+ },
126
+ }),
127
+ );
128
+ expect(screen.getByText("20")).toBeInTheDocument();
129
+ fireEvent.keyDown(screen.getByRole("slider", { name: "Mapped price" }), {
130
+ key: "ArrowRight",
131
+ });
132
+ expect(initial.setValue).toHaveBeenCalledWith(2);
133
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "2");
134
+ expect(screen.getByRole("tooltip")).toHaveTextContent("50");
135
+ });
136
+ });