Skip to content
Snippets Groups Projects
Unverified Commit 6ba6e487 authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Dual Slider Component (#23085)

* add generic range slider input
parent ba247045
No related branches found
No related tags found
No related merge requests found
import React, { useState } from "react";
import { ComponentStory } from "@storybook/react";
import Slider from "./Slider";
export default {
title: "Core/Slider",
component: Slider,
};
const Template: ComponentStory<typeof Slider> = args => {
const [value, setValue] = useState([10, 40]);
return <Slider {...args} value={value} onChange={setValue} />;
};
export const Default = Template.bind({});
import styled from "@emotion/styled";
import { color, alpha } from "metabase/lib/colors";
export const SliderContainer = styled.div`
position: relative;
display: flex;
`;
const thumbStyles = `
-webkit-appearance: none;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
border: 2px solid ${color("brand")};
background-color: ${color("white")};
cursor: pointer;
box-shadow: 0 0 2px 1px ${color("brand")};
pointer-events: all;
&:active {
box-shadow: 0 0 4px 1px ${color("brand")};
}
`;
export const SliderInput = styled.input`
-webkit-appearance: none;
position: absolute;
width: 100%;
height: 0;
border: none;
outline: none;
background: none;
pointer-events: none;
&::-webkit-slider-thumb {
${thumbStyles}
}
&::-moz-range-thumb {
${thumbStyles}
}
`;
export const SliderTrack = styled.span`
width: 100%;
background-color: ${alpha("brand", 0.5)};
height: 0.2rem;
border-radius: 0.2rem;
`;
interface ActiveTrackProps {
left: number;
width: number;
}
export const ActiveTrack = styled.span<ActiveTrackProps>`
position: absolute;
left: ${props => props.left}%;
width: ${props => props.width}%;
background-color: ${color("brand")};
height: 0.2rem;
`;
import React, {
ChangeEvent,
InputHTMLAttributes,
useCallback,
useMemo,
} from "react";
import {
SliderContainer,
SliderInput,
SliderTrack,
ActiveTrack,
} from "./Slider.styled";
export type NumericInputAttributes = Omit<
InputHTMLAttributes<HTMLDivElement>,
"value" | "size" | "onChange"
>;
export interface SliderProps extends NumericInputAttributes {
value: number[];
onChange: (value: number[]) => void;
min?: number;
max?: number;
step?: number;
}
const Slider = ({
value,
onChange,
min = 0,
max = 100,
step = 1,
}: SliderProps) => {
const [rangeMin, rangeMax] = useMemo(
() => [Math.min(...value, min, max), Math.max(...value, min, max)],
[value, min, max],
);
const [beforeRange, rangeWidth] = useMemo(() => {
const totalRange = rangeMax - rangeMin;
return [
((Math.min(...value) - rangeMin) / totalRange) * 100,
(Math.abs(value[1] - value[0]) / totalRange) * 100,
];
}, [value, rangeMin, rangeMax]);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>, valueIndex: number) => {
const changedValue = [...value];
changedValue[valueIndex] = Number(event.target.value);
onChange(changedValue);
},
[value, onChange],
);
const sortValues = useCallback(() => {
if (value[0] > value[1]) {
const sortedValues = [...value].sort();
onChange(sortedValues);
}
}, [value, onChange]);
return (
<SliderContainer>
<SliderTrack />
<ActiveTrack left={beforeRange} width={rangeWidth} />
<SliderInput
type="range"
aria-label="min"
value={value[0]}
onChange={e => handleChange(e, 0)}
onMouseUp={sortValues}
min={rangeMin}
max={rangeMax}
step={step}
/>
<SliderInput
type="range"
aria-label="max"
value={value[1]}
onChange={e => handleChange(e, 1)}
onMouseUp={sortValues}
min={rangeMin}
max={rangeMax}
step={step}
/>
</SliderContainer>
);
};
export default Slider;
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import Slider from "./Slider";
describe("Slider", () => {
it("should render 2 range inputs", () => {
render(<Slider value={[10, 40]} onChange={() => null} min={0} max={100} />);
const minInput = screen.getByLabelText("min");
const maxInput = screen.getByLabelText("max");
expect(minInput).toHaveAttribute("type", "range");
expect(maxInput).toHaveAttribute("type", "range");
});
it("should always have values in range", () => {
render(
<Slider value={[10, 412]} onChange={() => null} min={0} max={100} />,
);
const minInput = screen.getByLabelText("min");
expect(minInput).toHaveAttribute("max", "412");
});
it("should call onChange with the new value on input change", () => {
const spy = jest.fn();
render(<Slider value={[10, 20]} onChange={spy} min={0} max={100} />);
const minInput = screen.getByLabelText("min");
const maxInput = screen.getByLabelText("max");
// would be nice to use userEvent when we upgrade to v14 so we mock drage events
fireEvent.change(minInput, { target: { value: "5" } });
fireEvent.change(maxInput, { target: { value: "15" } });
const [firstCall, secondCall] = spy.mock.calls;
expect(firstCall[0]).toEqual([5, 20]);
expect(secondCall[0]).toEqual([10, 15]);
});
it("should sort input values on mouse up", () => {
const spy = jest.fn();
render(<Slider value={[2, 1]} onChange={spy} min={0} max={100} />);
const minInput = screen.getByLabelText("min");
fireEvent.mouseUp(minInput);
expect(spy.mock.calls[0][0]).toEqual([1, 2]);
});
});
export { default } from "./Slider";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment