@marimo-team/frontend 0.25.2-dev91 → 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/assets/{index-CqJKdR1j.js → index-C6-WDESh.js} +2 -2
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/components/data-table/charts/__tests__/slider-field.test.tsx +54 -0
- package/src/components/data-table/charts/components/form-fields.tsx +3 -1
- package/src/components/ui/__tests__/slider.test.tsx +7 -0
- package/src/components/ui/slider.tsx +2 -0
- package/src/plugins/impl/SliderPlugin.tsx +4 -0
- package/src/plugins/impl/__test__/slider-labels.test.tsx +136 -0
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-
|
|
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
|
@@ -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 & 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
|
+
});
|