From a391d5794b99bf50af561ae1ecb54f40789332fd Mon Sep 17 00:00:00 2001 From: Mattias Erlingsson Date: Thu, 15 Feb 2024 12:59:28 +0100 Subject: [PATCH] fix(slider): snap to mark (#200) Co-authored-by: Samuel Andersson --- components/slider/src/range-slider.spec.tsx | 28 +++++++++++++ components/slider/src/use-range-slider.ts | 36 +++++++++++++---- ...e-slider-with-streps-and-marks-example.tsx | 39 +++++++++++++++++++ examples/src/stories/slider/slider-page.tsx | 10 +++++ 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 examples/src/stories/slider/examples/range-slider-with-streps-and-marks-example.tsx diff --git a/components/slider/src/range-slider.spec.tsx b/components/slider/src/range-slider.spec.tsx index 4446470c..4b110f6c 100644 --- a/components/slider/src/range-slider.spec.tsx +++ b/components/slider/src/range-slider.spec.tsx @@ -130,6 +130,34 @@ describe("range-slider", () => { expect(onChangeCommitted).toHaveBeenCalledWith({ value: [40, 100] }); }); + + it("should snap to closest mark", () => { + const onChange = vi.fn(); + const onChangeCommitted = vi.fn(); + const { getByTestId } = render( + 20 }, + { value: 40, label: "40" }, + { value: 60, label: "60" }, + ]} + data-testid="slider-root" + /> + ); + + const mark: HTMLElement = getByTestId("mark"); + getControlRoot(getByTestId("slider-root")); + fireEvent.mouseDown(mark, { button: 0, clientX: 29 }); + fireEvent.mouseUp(mark, { button: 0, clientX: 29 }); + + expect(onChange).toHaveBeenCalledWith({ value: [20] }); + expect(onChangeCommitted).toHaveBeenCalledWith({ value: [20] }); + }); }); describe("keyboard interaction", () => { diff --git a/components/slider/src/use-range-slider.ts b/components/slider/src/use-range-slider.ts index ab032f3d..2401370f 100644 --- a/components/slider/src/use-range-slider.ts +++ b/components/slider/src/use-range-slider.ts @@ -28,6 +28,7 @@ import { ThumbProps } from "./thumb/thumb.types"; import { toPercent } from "./utils"; import { MarkLabelProps } from "./mark/label/mark-label.types"; import { MarkLabel } from "./mark/label/mark-label"; +import { sliderClassNames } from "./use-slider-styles"; const asc = (a: number, b: number): number => a - b; @@ -80,6 +81,19 @@ const findClosest = (value: number, candidates: number[]) => { return { value: closest.value, index: closest.index }; }; +const isMarkLabelElement = (target: EventTarget | null): boolean => { + if ( + !target || !(target instanceof Element) + || target.classList.contains(sliderClassNames.root) + ) { + return false; + } + if (target.classList.contains(sliderClassNames.mark.label)) { + return true; + } + return isMarkLabelElement(target.parentNode); +}; + export const useRangeSlider_unstable = ( props: RangeSliderProps, ref: React.Ref @@ -165,17 +179,20 @@ export const useRangeSlider_unstable = ( const controlRef = useFocusWithin(); const snapValueToClosest = useCallback( - (value: number) => { + (value: number, snapToMarkValue: boolean) => { if (snapValues) { return findClosest(value, snapValues).value; } + if (snapToMarkValue) { + return findClosest(value, markValues).value; + } return value; }, - [snapValues] + [snapValues, markValues] ); const getNewValue = useCallback( - (clientX: number): number => { + (clientX: number, snapToMarkValue: boolean): number => { const clientRect = controlRef.current?.getBoundingClientRect() as DOMRect; let relativePosition = clamp( (clientX - clientRect.x) / clientRect.width, @@ -188,7 +205,8 @@ export const useRangeSlider_unstable = ( } return snapValueToClosest( - Math.round(relativePosition * (max - min) + min) + Math.round(relativePosition * (max - min) + min), + snapToMarkValue ); }, [controlRef, dir, max, min, snapValueToClosest] @@ -210,7 +228,8 @@ export const useRangeSlider_unstable = ( if (e.button !== 0) { return; } - const newValue = getNewValue(e.clientX); + + const newValue = getNewValue(e.clientX, isMarkLabelElement(e.target)); // avoid text selection e.preventDefault(); @@ -245,7 +264,7 @@ export const useRangeSlider_unstable = ( return; } - const wantedValue = getNewValue(clientX); + const wantedValue = getNewValue(clientX, false); const previousValue = wantedValue; const newValues = internalValues.slice(); newValues[active] = wantedValue; @@ -291,7 +310,10 @@ export const useRangeSlider_unstable = ( touchId.current = touch.identifier; } - const newValue = getNewValue(touch.clientX); + const newValue = getNewValue( + touch.clientX, + isMarkLabelElement(event.target) + ); const closestIndex = findClosest(newValue, internalValues).index; const newValues = internalValues.slice(); diff --git a/examples/src/stories/slider/examples/range-slider-with-streps-and-marks-example.tsx b/examples/src/stories/slider/examples/range-slider-with-streps-and-marks-example.tsx new file mode 100644 index 00000000..cf0f3091 --- /dev/null +++ b/examples/src/stories/slider/examples/range-slider-with-streps-and-marks-example.tsx @@ -0,0 +1,39 @@ +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsAndMarksRangeSliderExample() { + return ( + + ); +} + +export const WithStepsAndMarksRangeSliderExampleAsString = ` +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsAndMarksRangeSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/slider-page.tsx b/examples/src/stories/slider/slider-page.tsx index bcb03039..d7f1420b 100644 --- a/examples/src/stories/slider/slider-page.tsx +++ b/examples/src/stories/slider/slider-page.tsx @@ -51,6 +51,10 @@ import { WithStepsSliderExample, WithStepsSliderExampleAsString, } from "./examples/with-steps-example"; +import { + WithStepsAndMarksRangeSliderExample, + WithStepsAndMarksRangeSliderExampleAsString, +} from "./examples/range-slider-with-streps-and-marks-example"; const useStyles = makeStyles({ example: { @@ -89,6 +93,12 @@ const examples: pageData[] = [ example: , codeString: WithStepsSliderExampleAsString, }, + { + title: "With steps and marks", + anchor: "WithStepsAndMarksSliderExample", + example: , + codeString: WithStepsAndMarksRangeSliderExampleAsString, + }, { title: "Stepping to marks", anchor: "SteppingToMarksSliderExample",