Skip to content

Commit

Permalink
Make a custom slider
Browse files Browse the repository at this point in the history
  • Loading branch information
tayyabataimur committed Jan 24, 2025
1 parent 277b074 commit 7a2dcde
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 22 deletions.
7 changes: 7 additions & 0 deletions packages/lab/src/custom-slider/CustomSlider.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,18 @@
cursor: pointer;
}

.customSlider.dragging,
.customSlider.dragging .customSlider-track,
.customSlider.dragging .customSlider-thumb {
cursor: grabbing;
}

.customSlider-thumb {
position: absolute;
width: var(--salt-size-indicator);
height: var(--salt-size-selectable);
background: var(--salt-accent-borderColor);
left: var(--salt-slider-progressPercentage);
transform: translateX(-50%);
cursor: pointer;
outline: none;
Expand Down
58 changes: 36 additions & 22 deletions packages/lab/src/custom-slider/CustomSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface SliderProps
/**
* Initial value of the slider
*/
defaultValue?: string;
defaultValue?: number;
/**
* Position of the labels
*/
Expand All @@ -39,11 +39,11 @@ export interface SliderProps
/**
* Value of the slider, to be used when in a controlled state
*/
value?: string | number;
value?: number;
/**
* Change handler to be used when in a controlled state
*/
onChange?: (value: string | number) => void;
onChange?: (value: number) => void;
}

export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
Expand All @@ -57,6 +57,7 @@ export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
onChange,
className,
"aria-label": ariaLabel,
"aria-valuetext": ariaValueText,
labelPosition = "inline",
showMarkers = false,
...rest
Expand All @@ -71,7 +72,7 @@ export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
});

const [value, setValue] = useControlled({
controlled: valueProp,
controlled: Number.isNaN(valueProp) ? min : valueProp,
default: defaultValue,
name: "Slider",
state: "value",
Expand Down Expand Up @@ -119,31 +120,47 @@ export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
if (!isDragging) setTooltipVisible(false);
};

const handleFocus = () => {
setTooltipVisible(true);
};
const handleFocus = () => setTooltipVisible(true);

const handleBlur = () => setTooltipVisible(false);

const parseValue = (value: string | number) =>
typeof value === "string" ? Number.parseFloat(value) : value;

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
let newValue = parseValue(value);
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
newValue = Math.min(parseValue(value) + step, max);
} else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
newValue = Math.max(parseValue(value) - step, min);
let newValue = value;
switch (event.key) {
case "ArrowUp":
newValue = Math.min(value + step, max);
break;
case "ArrowRight":
newValue = Math.min(value + step, max);
break;
case "ArrowDown":
newValue = Math.max(value - step, min);
break;
case "ArrowLeft":
newValue = Math.max(value - step, min);
break;
case "Home":
newValue = min;
break;
case "End":
newValue = max;
break;
}

setValue(newValue);
onChange?.(newValue);
};

const progressPercentage = ((parseValue(value) - min) / (max - min)) * 100;
const progressPercentage = ((value - min) / (max - min)) * 100;

return (
<div
className={clsx(withBaseName(), withBaseName(`${labelPosition}Labels`))}
className={clsx(
withBaseName(),
withBaseName(`${labelPosition}Labels`),
{ dragging: isDragging },
)}
ref={ref}
{...rest}
>
<Text as="label" className={clsx(withBaseName("minLabel"))}>
Expand All @@ -161,9 +178,6 @@ export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
>
<div
className={clsx(withBaseName("thumb"))}
style={{
left: `${((parseValue(value) - min) / (max - min)) * 100}%`,
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onFocus={handleFocus}
Expand All @@ -173,8 +187,8 @@ export const CustomSlider = forwardRef<HTMLDivElement, SliderProps>(
role="slider"
aria-valuemax={max}
aria-valuemin={min}
aria-valuenow={parseValue(value)}
aria-valuetext={`Value: ${value}`}
aria-valuenow={value}
aria-valuetext={ariaValueText}
aria-label={ariaLabel}
>
{(isDragging || tooltipVisible) && (
Expand Down
83 changes: 83 additions & 0 deletions packages/lab/stories/custom-slider/CustomSlider.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { StoryFn } from "@storybook/react";

Check failure on line 1 in packages/lab/stories/custom-slider/CustomSlider.stories.tsx

View workflow job for this annotation

GitHub Actions / lint

organizeImports

Import statements differs from the output
import { CustomSlider } from "packages/lab/src/custom-slider/CustomSlider";
import { type ChangeEvent, useEffect, useState } from "react";
import {
FlexLayout,
FormField,
FormFieldLabel,
Input,
StackLayout,
} from "@salt-ds/core";

export default {
title: "Lab/CustomSlider",
Expand Down Expand Up @@ -35,3 +43,78 @@ WithBottomLabels.args = {
"aria-label": "WithBottomLabels",
labelPosition: "bottom",
};

const validateSingle = (value: string | number, bounds: [number, number]) => {
if (Number.isNaN(Number(value))) return false;
if (Number(value) < bounds[0] || Number(value) > bounds[1]) return false;
return true;
};

export const WithInput = () => {
const [value, setValue] = useState<number>(5);
const [inputValue, setInputValue] = useState<string | number>(value);
const [validationStatus, setValidationStatus] = useState<undefined | "error">(
undefined,
);
const bounds: [number, number] = [-50, 50];

const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
setInputValue(inputValue);
if (Number.isNaN(Number(inputValue))) return;
setValue(Number.parseFloat(inputValue));
};

const handleChange = (val: number) => {
setInputValue(val);
setValue(val);
};

useEffect(() => {
const valid = validateSingle(inputValue, bounds);
setValidationStatus(valid ? undefined : "error");
}, [inputValue]);

return (
<FormField>
<FormFieldLabel> Slider with Input </FormFieldLabel>
<FlexLayout gap={1}>
<Input
value={inputValue}
style={{ width: "10px" }}
onChange={handleInputChange}
validationStatus={validationStatus}
/>
<CustomSlider
style={{ width: "300px" }}
min={bounds[0]}
max={bounds[1]}
value={value}
onChange={handleChange}
aria-label="withInput"
/>
</FlexLayout>
</FormField>
);
};

export const CustomStep = () => (
<StackLayout gap={10} style={{ width: "400px" }}>
<FormField>
<FormFieldLabel>Step: 1 (default)</FormFieldLabel>
<CustomSlider min={-1} max={1} />
</FormField>
<FormField>
<FormFieldLabel>Step: 0.2</FormFieldLabel>
<CustomSlider min={-1} max={1} step={0.2} />
</FormField>
<FormField>
<FormFieldLabel>Step: 0.25 (two decimal places)</FormFieldLabel>
<CustomSlider min={-1} max={1} step={0.25} />
</FormField>
<FormField>
<FormFieldLabel>Step: 0.3 (not multiple of total range)</FormFieldLabel>
<CustomSlider min={0} max={1} step={0.3} defaultValue={0.9} />
</FormField>
</StackLayout>
);

0 comments on commit 7a2dcde

Please sign in to comment.