Skip to content

Commit e32513f

Browse files
authored
Merge pull request #79 from bcdev/yogesh-41-slider
Added `Slider` component
2 parents 864bcae + 1f7d59d commit e32513f

File tree

10 files changed

+395
-1
lines changed

10 files changed

+395
-1
lines changed

chartlets.js/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
- `RadioGroup` and `Radio`
4343
- `Switch`
4444
- `Tabs`
45+
- `Slider`
4546

4647
* Supporting `tooltip` property for interactive MUI components.
4748

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import { Slider } from "./Slider";
4+
import "@testing-library/jest-dom";
5+
import { createChangeHandler } from "@/plugins/mui/common.test";
6+
import { useState } from "react";
7+
import type { ComponentChangeEvent } from "@/types/state/event";
8+
9+
describe("Slider", () => {
10+
it("should render the Slider component", () => {
11+
render(
12+
<Slider
13+
id="slider"
14+
type={"Slider"}
15+
min={0}
16+
max={100}
17+
value={50}
18+
onChange={() => {}}
19+
/>,
20+
);
21+
22+
const slider = screen.getByRole("slider");
23+
expect(slider).toBeDefined();
24+
25+
expect(slider.getAttribute("aria-orientation")).toEqual("horizontal");
26+
expect(slider.getAttribute("min")).toEqual("0");
27+
expect(slider.getAttribute("max")).toEqual("100");
28+
expect(slider.getAttribute("value")).toEqual("50");
29+
});
30+
31+
it("should render the Slider component", () => {
32+
render(
33+
<Slider
34+
id="slider"
35+
type={"Slider"}
36+
min={0}
37+
max={100}
38+
value={50}
39+
onChange={() => {}}
40+
/>,
41+
);
42+
43+
const slider = screen.getByRole("slider");
44+
expect(slider).toBeDefined();
45+
46+
expect(slider.getAttribute("aria-orientation")).toEqual("horizontal");
47+
expect(slider.getAttribute("min")).toEqual("0");
48+
expect(slider.getAttribute("max")).toEqual("100");
49+
expect(slider.getAttribute("value")).toEqual("50");
50+
});
51+
52+
it("should fire 'value' property", () => {
53+
const { recordedEvents, onChange } = createChangeHandler();
54+
55+
const TestSlider = () => {
56+
const [sliderValue, setSliderValue] = useState(60);
57+
58+
const handleChange = (event: ComponentChangeEvent) => {
59+
setSliderValue(event.value as number);
60+
onChange(event);
61+
};
62+
63+
return (
64+
<Slider
65+
type={"Slider"}
66+
data-testid="sliderId"
67+
id="sliderId"
68+
aria-label={"slider"}
69+
min={0}
70+
max={1000}
71+
onChange={handleChange} // Use the local handleChange
72+
value={sliderValue} // Connect the value
73+
/>
74+
);
75+
};
76+
77+
render(<TestSlider />);
78+
const slider = screen.getByTestId("sliderId");
79+
expect(slider).toBeInTheDocument();
80+
expect(screen.getByRole("slider")).toHaveValue("60");
81+
82+
const input = document.querySelector("input")?.value;
83+
expect(input).toEqual("60");
84+
85+
const sliderBounds = {
86+
left: 100,
87+
width: 200,
88+
top: 0,
89+
bottom: 0,
90+
height: 20,
91+
};
92+
vi.spyOn(slider, "getBoundingClientRect").mockReturnValue(
93+
sliderBounds as DOMRect,
94+
);
95+
96+
// The value selected should be 100
97+
const clientX = sliderBounds.left + sliderBounds.width * 0.1;
98+
99+
fireEvent.mouseDown(slider, { clientX: clientX });
100+
fireEvent.mouseMove(slider, { clientX: clientX });
101+
fireEvent.mouseUp(slider);
102+
expect(recordedEvents.length).toEqual(1);
103+
104+
expect(recordedEvents[0]).toEqual({
105+
componentType: "Slider",
106+
id: "sliderId",
107+
property: "value",
108+
value: 100,
109+
});
110+
111+
expect(screen.getByRole("slider")).toHaveValue("100");
112+
const updated_input = document.querySelector("input");
113+
expect(updated_input?.value).toEqual("100");
114+
});
115+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import MuiSlider from "@mui/material/Slider";
2+
import type { OverridableStringUnion } from "@mui/types";
3+
4+
import type { ComponentProps, ComponentState } from "@/index";
5+
import type { ReactNode } from "react";
6+
7+
interface SliderState extends ComponentState {
8+
defaultValue?: number;
9+
ariaLabel?: string;
10+
color?: OverridableStringUnion<
11+
"primary" | "secondary" | "success" | "error" | "info" | "warning",
12+
string
13+
>;
14+
disableSwap?: boolean;
15+
getAriaValueText?: (value: number, index: number) => string;
16+
min?: number;
17+
max?: number;
18+
marks?: boolean | { value: number; label?: ReactNode }[];
19+
orientation?: "horizontal" | "vertical";
20+
step?: number;
21+
size?: OverridableStringUnion<"small" | "medium", string>;
22+
track?: "inverted" | "normal" | false;
23+
value?: number | number[];
24+
valueLabelDisplay?: "auto" | "on" | "off";
25+
["data-testid"]?: string;
26+
}
27+
28+
interface SliderProps extends ComponentProps, SliderState {}
29+
30+
export const Slider = ({
31+
type,
32+
id,
33+
style,
34+
defaultValue,
35+
ariaLabel,
36+
color,
37+
disableSwap,
38+
getAriaValueText,
39+
min,
40+
max,
41+
marks,
42+
orientation,
43+
step,
44+
size,
45+
track,
46+
value,
47+
valueLabelDisplay,
48+
onChange,
49+
...props
50+
}: SliderProps) => {
51+
// We need to drop children prop because we want to access the data-testid for
52+
// tests and slider does not accept children components
53+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
54+
const { children: _, ...sliderProps } = props;
55+
56+
const handleSlide = (
57+
_event: Event,
58+
value: number | number[],
59+
_activeThumb: number,
60+
) => {
61+
if (id) {
62+
onChange({
63+
componentType: type,
64+
id: id,
65+
property: "value",
66+
value: value,
67+
});
68+
}
69+
};
70+
return (
71+
<MuiSlider
72+
{...sliderProps}
73+
id={id}
74+
defaultValue={defaultValue}
75+
aria-label={ariaLabel}
76+
color={color}
77+
style={style}
78+
disableSwap={disableSwap}
79+
getAriaValueText={getAriaValueText}
80+
min={min}
81+
max={max}
82+
marks={marks}
83+
orientation={orientation}
84+
step={step}
85+
size={size}
86+
track={track}
87+
value={value ?? 0}
88+
valueLabelDisplay={valueLabelDisplay}
89+
onChange={handleSlide}
90+
/>
91+
);
92+
};

chartlets.js/packages/lib/src/plugins/mui/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Select } from "./Select";
99
import { Switch } from "./Switch";
1010
import { Tabs } from "./Tabs";
1111
import { Typography } from "./Typography";
12+
import { Slider } from "./Slider";
1213

1314
export default function mui(): Plugin {
1415
return {
@@ -20,6 +21,7 @@ export default function mui(): Plugin {
2021
["IconButton", IconButton],
2122
["RadioGroup", RadioGroup],
2223
["Select", Select],
24+
["Slider", Slider],
2325
["Switch", Switch],
2426
["Tabs", Tabs],
2527
["Typography", Typography],

chartlets.py/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- `Switch`
2323
- `RadioGroup` and `Radio`
2424
- `Tabs`
25+
- `Slider`
2526

2627
## Version 0.0.29 (from 2024/11/26)
2728

chartlets.py/chartlets/components/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
from .button import Button
33
from .button import IconButton
44
from .checkbox import Checkbox
5+
from .charts.vega import VegaChart
56
from .progress import CircularProgress
67
from .progress import CircularProgressWithLabel
78
from .progress import LinearProgress
89
from .progress import LinearProgressWithLabel
9-
from .charts.vega import VegaChart
1010
from .radiogroup import Radio
1111
from .radiogroup import RadioGroup
1212
from .select import Select
13+
from .slider import Slider
1314
from .switch import Switch
1415
from .tabs import Tab
1516
from .tabs import Tabs
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from dataclasses import dataclass
2+
from typing import Literal, TypedDict, Callable
3+
4+
from chartlets import Component
5+
6+
7+
@dataclass(frozen=True)
8+
class Slider(Component):
9+
"""Sliders allow users to make selections from a range of values along a
10+
bar."""
11+
12+
aria_label: str | None = None
13+
"""The label of the slider."""
14+
15+
color: str | None = None
16+
"""The color of the component. It supports both default and custom theme
17+
colors
18+
"""
19+
20+
defaultValue: list[int] | int | None = None
21+
"""The default value. Use when the component is not controlled. If used
22+
as an array, it will create multiple sliding points on the bar
23+
"""
24+
25+
disableSwap: bool | None = None
26+
"""If true, the active thumb doesn't swap when moving pointer over a thumb
27+
while dragging another thumb.
28+
"""
29+
30+
getAriaValueText: Callable[[int, int], str] | None = None
31+
"""Accepts a function which returns a string value that provides a
32+
user-friendly name for the current value of the slider. This is important
33+
for screen reader users.
34+
35+
Signature:
36+
function(value: number, index: number) => string
37+
38+
value: The thumb label's value to format.
39+
index: The thumb label's index to format.
40+
"""
41+
42+
min: int | None = None
43+
"""The minimum allowed value of the slider. Should not be equal to max."""
44+
45+
max: int | None = None
46+
"""The maximum allowed value of the slider. Should not be equal to min."""
47+
48+
marks: (bool |
49+
list[TypedDict("marks", {"value": int, "label": str})] |
50+
None) = None
51+
"""Marks indicate predetermined values to which the user can move the
52+
slider. If true the marks are spaced according the value of the step
53+
prop. If an array, it should contain objects with value and an optional
54+
label keys.
55+
"""
56+
57+
orientation: Literal["horizontal", "vertical"] | None = None
58+
"""The component orientation."""
59+
60+
step: int | None = None
61+
"""The granularity with which the slider can step through values. (A
62+
"discrete" slider.) The min prop serves as the origin for the valid values.
63+
We recommend (max - min) to be evenly divisible by the step. When step is
64+
null, the thumb can only be slid onto marks provided with the marks prop.
65+
"""
66+
67+
size: str | None = None
68+
"""The size of the slider."""
69+
70+
tooltip: str | None = None
71+
"""Tooltip title. Optional."""
72+
73+
track: Literal["inverted", "normal"] | False | None = None
74+
"""The track presentation:
75+
76+
`normal`: the track will render a bar representing the slider value.
77+
`inverted`: the track will render a bar representing the remaining slider
78+
value.
79+
`false`: the track will render without a bar.
80+
"""
81+
82+
value: list[int] | int | None = None
83+
"""The value of the slider. For ranged sliders, provide an array with two
84+
values.
85+
"""
86+
87+
valueLabelDisplay: Literal['auto', 'on', 'off'] | None = None
88+
"""Controls when the value label is displayed:
89+
90+
`auto` the value label will display when the thumb is hovered or focused.
91+
`on` will display persistently.
92+
`off` will never display.
93+
"""

chartlets.py/demo/my_extension/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from .my_panel_1 import panel as my_panel_1
33
from .my_panel_2 import panel as my_panel_2
44
from .my_panel_3 import panel as my_panel_3
5+
from .my_panel_4 import panel as my_panel_4
56

67
ext = Extension(__name__)
78
ext.add(my_panel_1)
89
ext.add(my_panel_2)
910
ext.add(my_panel_3)
11+
ext.add(my_panel_4)

0 commit comments

Comments
 (0)