Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions chartlets.js/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- `RadioGroup` and `Radio`
- `Switch`
- `Tabs`
- `Slider`

* Supporting `tooltip` property for interactive MUI components.

Expand Down
115 changes: 115 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Slider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { Slider } from "./Slider";
import "@testing-library/jest-dom";
import { createChangeHandler } from "@/plugins/mui/common.test";
import { useState } from "react";
import type { ComponentChangeEvent } from "@/types/state/event";

describe("Slider", () => {
it("should render the Slider component", () => {
render(
<Slider
id="slider"
type={"Slider"}
min={0}
max={100}
value={50}
onChange={() => {}}
/>,
);

const slider = screen.getByRole("slider");
expect(slider).toBeDefined();

expect(slider.getAttribute("aria-orientation")).toEqual("horizontal");
expect(slider.getAttribute("min")).toEqual("0");
expect(slider.getAttribute("max")).toEqual("100");
expect(slider.getAttribute("value")).toEqual("50");
});

it("should render the Slider component", () => {
render(
<Slider
id="slider"
type={"Slider"}
min={0}
max={100}
value={50}
onChange={() => {}}
/>,
);

const slider = screen.getByRole("slider");
expect(slider).toBeDefined();

expect(slider.getAttribute("aria-orientation")).toEqual("horizontal");
expect(slider.getAttribute("min")).toEqual("0");
expect(slider.getAttribute("max")).toEqual("100");
expect(slider.getAttribute("value")).toEqual("50");
});

it("should fire 'value' property", () => {
const { recordedEvents, onChange } = createChangeHandler();

const TestSlider = () => {
const [sliderValue, setSliderValue] = useState(60);

const handleChange = (event: ComponentChangeEvent) => {
setSliderValue(event.value as number);
onChange(event);
};

return (
<Slider
type={"Slider"}
data-testid="sliderId"
id="sliderId"
aria-label={"slider"}
min={0}
max={1000}
onChange={handleChange} // Use the local handleChange
value={sliderValue} // Connect the value
/>
);
};

render(<TestSlider />);
const slider = screen.getByTestId("sliderId");
expect(slider).toBeInTheDocument();
expect(screen.getByRole("slider")).toHaveValue("60");

const input = document.querySelector("input")?.value;
expect(input).toEqual("60");

const sliderBounds = {
left: 100,
width: 200,
top: 0,
bottom: 0,
height: 20,
};
vi.spyOn(slider, "getBoundingClientRect").mockReturnValue(
sliderBounds as DOMRect,
);

// The value selected should be 100
const clientX = sliderBounds.left + sliderBounds.width * 0.1;

fireEvent.mouseDown(slider, { clientX: clientX });
fireEvent.mouseMove(slider, { clientX: clientX });
fireEvent.mouseUp(slider);
expect(recordedEvents.length).toEqual(1);

expect(recordedEvents[0]).toEqual({
componentType: "Slider",
id: "sliderId",
property: "value",
value: 100,
});

expect(screen.getByRole("slider")).toHaveValue("100");
const updated_input = document.querySelector("input");
expect(updated_input?.value).toEqual("100");
});
});
92 changes: 92 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import MuiSlider from "@mui/material/Slider";
import type { OverridableStringUnion } from "@mui/types";

import type { ComponentProps, ComponentState } from "@/index";
import type { ReactNode } from "react";

interface SliderState extends ComponentState {
defaultValue?: number;
ariaLabel?: string;
color?: OverridableStringUnion<
"primary" | "secondary" | "success" | "error" | "info" | "warning",
string
>;
disableSwap?: boolean;
getAriaValueText?: (value: number, index: number) => string;
min?: number;
max?: number;
marks?: boolean | { value: number; label?: ReactNode }[];
orientation?: "horizontal" | "vertical";
step?: number;
size?: OverridableStringUnion<"small" | "medium", string>;
track?: "inverted" | "normal" | false;
value?: number | number[];
valueLabelDisplay?: "auto" | "on" | "off";
["data-testid"]?: string;
}

interface SliderProps extends ComponentProps, SliderState {}

export const Slider = ({
type,
id,
style,
defaultValue,
ariaLabel,
color,
disableSwap,
getAriaValueText,
min,
max,
marks,
orientation,
step,
size,
track,
value,
valueLabelDisplay,
onChange,
...props
}: SliderProps) => {
// We need to drop children prop because we want to access the data-testid for
// tests and slider does not accept children components
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { children: _, ...sliderProps } = props;

const handleSlide = (
_event: Event,
value: number | number[],
_activeThumb: number,
) => {
if (id) {
onChange({
componentType: type,
id: id,
property: "value",
value: value,
});
}
};
return (
<MuiSlider
{...sliderProps}
id={id}
defaultValue={defaultValue}
aria-label={ariaLabel}
color={color}
style={style}
disableSwap={disableSwap}
getAriaValueText={getAriaValueText}
min={min}
max={max}
marks={marks}
orientation={orientation}
step={step}
size={size}
track={track}
value={value ?? 0}
valueLabelDisplay={valueLabelDisplay}
onChange={handleSlide}
/>
);
};
2 changes: 2 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Select } from "./Select";
import { Switch } from "./Switch";
import { Tabs } from "./Tabs";
import { Typography } from "./Typography";
import { Slider } from "./Slider";

export default function mui(): Plugin {
return {
Expand All @@ -20,6 +21,7 @@ export default function mui(): Plugin {
["IconButton", IconButton],
["RadioGroup", RadioGroup],
["Select", Select],
["Slider", Slider],
["Switch", Switch],
["Tabs", Tabs],
["Typography", Typography],
Expand Down
1 change: 1 addition & 0 deletions chartlets.py/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- `Switch`
- `RadioGroup` and `Radio`
- `Tabs`
- `Slider`

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

Expand Down
3 changes: 2 additions & 1 deletion chartlets.py/chartlets/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
from .button import Button
from .button import IconButton
from .checkbox import Checkbox
from .charts.vega import VegaChart
from .progress import CircularProgress
from .progress import CircularProgressWithLabel
from .progress import LinearProgress
from .progress import LinearProgressWithLabel
from .charts.vega import VegaChart
from .radiogroup import Radio
from .radiogroup import RadioGroup
from .select import Select
from .slider import Slider
from .switch import Switch
from .tabs import Tab
from .tabs import Tabs
Expand Down
93 changes: 93 additions & 0 deletions chartlets.py/chartlets/components/slider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from dataclasses import dataclass
from typing import Literal, TypedDict, Callable

from chartlets import Component


@dataclass(frozen=True)
class Slider(Component):
"""Sliders allow users to make selections from a range of values along a
bar."""

aria_label: str | None = None
"""The label of the slider."""

color: str | None = None
"""The color of the component. It supports both default and custom theme
colors
"""

defaultValue: list[int] | int | None = None
"""The default value. Use when the component is not controlled. If used
as an array, it will create multiple sliding points on the bar
"""

disableSwap: bool | None = None
"""If true, the active thumb doesn't swap when moving pointer over a thumb
while dragging another thumb.
"""

getAriaValueText: Callable[[int, int], str] | None = None
"""Accepts a function which returns a string value that provides a
user-friendly name for the current value of the slider. This is important
for screen reader users.

Signature:
function(value: number, index: number) => string

value: The thumb label's value to format.
index: The thumb label's index to format.
"""

min: int | None = None
"""The minimum allowed value of the slider. Should not be equal to max."""

max: int | None = None
"""The maximum allowed value of the slider. Should not be equal to min."""

marks: (bool |
list[TypedDict("marks", {"value": int, "label": str})] |
None) = None
"""Marks indicate predetermined values to which the user can move the
slider. If true the marks are spaced according the value of the step
prop. If an array, it should contain objects with value and an optional
label keys.
"""

orientation: Literal["horizontal", "vertical"] | None = None
"""The component orientation."""

step: int | None = None
"""The granularity with which the slider can step through values. (A
"discrete" slider.) The min prop serves as the origin for the valid values.
We recommend (max - min) to be evenly divisible by the step. When step is
null, the thumb can only be slid onto marks provided with the marks prop.
"""

size: str | None = None
"""The size of the slider."""

tooltip: str | None = None
"""Tooltip title. Optional."""

track: Literal["inverted", "normal"] | False | None = None
"""The track presentation:

`normal`: the track will render a bar representing the slider value.
`inverted`: the track will render a bar representing the remaining slider
value.
`false`: the track will render without a bar.
"""

value: list[int] | int | None = None
"""The value of the slider. For ranged sliders, provide an array with two
values.
"""

valueLabelDisplay: Literal['auto', 'on', 'off'] | None = None
"""Controls when the value label is displayed:

`auto` the value label will display when the thumb is hovered or focused.
`on` will display persistently.
`off` will never display.
"""
2 changes: 2 additions & 0 deletions chartlets.py/demo/my_extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from .my_panel_1 import panel as my_panel_1
from .my_panel_2 import panel as my_panel_2
from .my_panel_3 import panel as my_panel_3
from .my_panel_4 import panel as my_panel_4

ext = Extension(__name__)
ext.add(my_panel_1)
ext.add(my_panel_2)
ext.add(my_panel_3)
ext.add(my_panel_4)
Loading
Loading