diff --git a/apps/docs/src/examples/slider.tsx b/apps/docs/src/examples/slider.tsx index 8f05c58b..9b0a247c 100644 --- a/apps/docs/src/examples/slider.tsx +++ b/apps/docs/src/examples/slider.tsx @@ -31,7 +31,7 @@ export function MultipleThumbsExample() { - + @@ -98,7 +98,7 @@ export function MinStepsBetweenExample() { - + @@ -141,7 +141,7 @@ export function CustomValueLabelExample() { - + diff --git a/packages/core/src/skeleton/index.tsx b/packages/core/src/skeleton/index.tsx index 81e54e2f..09858ff5 100644 --- a/packages/core/src/skeleton/index.tsx +++ b/packages/core/src/skeleton/index.tsx @@ -1 +1,5 @@ -export { Skeleton as Root } from "./skeleton"; +import { Skeleton as Root, type SkeletonProps } from "./skeleton"; + +export { Root }; + +export type { SkeletonProps }; diff --git a/packages/core/src/slider/create-slider-state.ts b/packages/core/src/slider/create-slider-state.ts index 47fff0a5..7cd5c623 100644 --- a/packages/core/src/slider/create-slider-state.ts +++ b/packages/core/src/slider/create-slider-state.ts @@ -134,13 +134,15 @@ export function createSliderState(props: StateOpts): SliderState { }; const getThumbMinValue = (index: number) => { - return index === 0 ? mergedProps.minValue!() : values()[index - 1]; + return index === 0 + ? props.minValue!() + : values()[index - 1] + props.minStepsBetweenThumbs!() * props.step!(); }; const getThumbMaxValue = (index: number) => { return index === values().length - 1 - ? mergedProps.maxValue!() - : values()[index + 1]; + ? props.maxValue!() + : values()[index + 1] - props.minStepsBetweenThumbs!() * props.step!(); }; const isThumbEditable = (index: number) => { @@ -215,9 +217,8 @@ export function createSliderState(props: StateOpts): SliderState { ); }; - const incrementThumb = (index: number, stepSize = 1) => { - const s = Math.max(stepSize, mergedProps.step!()); - const nextValue = values()[index] + s; + const snapThumbValue = (index: number, value: number) => { + const nextValue = values()[index] + value; const nextValues = getNextSortedValues(values(), nextValue, index); if ( hasMinStepsBetweenValues( @@ -237,26 +238,12 @@ export function createSliderState(props: StateOpts): SliderState { } }; + const incrementThumb = (index: number, stepSize = 1) => { + snapThumbValue(index, Math.max(stepSize, props.step!())); + }; + const decrementThumb = (index: number, stepSize = 1) => { - const s = Math.max(stepSize, mergedProps.step!()); - const nextValue = values()[index] - s; - const nextValues = getNextSortedValues(values(), nextValue, index); - if ( - hasMinStepsBetweenValues( - nextValues, - mergedProps.minStepsBetweenThumbs!() * mergedProps.step!(), - ) - ) { - updateValue( - index, - snapValueToStep( - nextValue, - mergedProps.minValue!(), - mergedProps.maxValue!(), - mergedProps.step!(), - ), - ); - } + snapThumbValue(index, -Math.max(stepSize, props.step!())); }; return { diff --git a/packages/core/src/slider/slider-input.tsx b/packages/core/src/slider/slider-input.tsx index cd42fb17..82e0334a 100644 --- a/packages/core/src/slider/slider-input.tsx +++ b/packages/core/src/slider/slider-input.tsx @@ -80,7 +80,7 @@ export function SliderInput(props: SliderInputProps) { type="range" id={fieldProps.id()} name={formControlContext.name()} - tabIndex={!context.state.isDisabled() ? 0 : undefined} + tabIndex={context.state.isDisabled() ? undefined : -1} min={context.state.getThumbMinValue(thumb.index())} max={context.state.getThumbMaxValue(thumb.index())} step={context.state.step()} diff --git a/packages/core/src/slider/slider-root.tsx b/packages/core/src/slider/slider-root.tsx index e0dec06c..b0b00f16 100644 --- a/packages/core/src/slider/slider-root.tsx +++ b/packages/core/src/slider/slider-root.tsx @@ -39,9 +39,9 @@ import { SliderDataSet, } from "./slider-context"; import { - getClosestValueIndex, getNextSortedValues, hasMinStepsBetweenValues, + stopEventDefaultAndPropagation, } from "./utils"; export interface GetValueLabelParams { @@ -278,22 +278,26 @@ export function SliderRoot(props: SliderRootProps) { if (activeThumb !== undefined) { state.setThumbDragging(activeThumb, false); + (thumbs()[activeThumb].ref() as HTMLElement).focus(); } }; - const onHomeKeyDown = () => { - !formControlContext.isDisabled() && - state.focusedThumb() !== undefined && - state.setThumbValue(0, state.getThumbMinValue(0)); + const onHomeKeyDown = (event: KeyboardEvent) => { + const focusedThumb = state.focusedThumb(); + + if (!formControlContext.isDisabled() && focusedThumb !== undefined) { + stopEventDefaultAndPropagation(event); + state.setThumbValue(focusedThumb, state.getThumbMinValue(focusedThumb)); + } }; - const onEndKeyDown = () => { - !formControlContext.isDisabled() && - state.focusedThumb() !== undefined && - state.setThumbValue( - state.values().length - 1, - state.getThumbMaxValue(state.values().length - 1), - ); + const onEndKeyDown = (event: KeyboardEvent) => { + const focusedThumb = state.focusedThumb(); + + if (!formControlContext.isDisabled() && focusedThumb !== undefined) { + stopEventDefaultAndPropagation(event); + state.setThumbValue(focusedThumb, state.getThumbMaxValue(focusedThumb)); + } }; const onStepKeyDown = (event: KeyboardEvent, index: number) => { @@ -301,8 +305,9 @@ export function SliderRoot(props: SliderRootProps) { switch (event.key) { case "Left": case "ArrowLeft": - event.preventDefault(); - event.stopPropagation(); + case "Down": + case "ArrowDown": + stopEventDefaultAndPropagation(event); if (!isLTR()) { state.incrementThumb( index, @@ -317,24 +322,9 @@ export function SliderRoot(props: SliderRootProps) { break; case "Right": case "ArrowRight": - event.preventDefault(); - event.stopPropagation(); - if (!isLTR()) { - state.decrementThumb( - index, - event.shiftKey ? state.pageSize() : state.step(), - ); - } else { - state.incrementThumb( - index, - event.shiftKey ? state.pageSize() : state.step(), - ); - } - break; case "Up": case "ArrowUp": - event.preventDefault(); - event.stopPropagation(); + stopEventDefaultAndPropagation(event); if (!isLTR()) { state.decrementThumb( index, @@ -347,32 +337,18 @@ export function SliderRoot(props: SliderRootProps) { ); } break; - case "Down": - case "ArrowDown": - event.preventDefault(); - event.stopPropagation(); - if (!isLTR()) { - state.incrementThumb( - index, - event.shiftKey ? state.pageSize() : state.step(), - ); - } else { - state.decrementThumb( - index, - event.shiftKey ? state.pageSize() : state.step(), - ); - } - break; case "Home": - onHomeKeyDown(); + onHomeKeyDown(event); break; case "End": - onEndKeyDown(); + onEndKeyDown(event); break; case "PageUp": + stopEventDefaultAndPropagation(event); state.incrementThumb(index, state.pageSize()); break; case "PageDown": + stopEventDefaultAndPropagation(event); state.decrementThumb(index, state.pageSize()); break; } diff --git a/packages/core/src/slider/utils.ts b/packages/core/src/slider/utils.ts index b255a321..1c04e297 100644 --- a/packages/core/src/slider/utils.ts +++ b/packages/core/src/slider/utils.ts @@ -20,7 +20,11 @@ export function getClosestValueIndex(values: number[], nextValue: number) { if (values.length === 1) return 0; const distances = values.map((value) => Math.abs(value - nextValue)); const closestDistance = Math.min(...distances); - return distances.indexOf(closestDistance); + const closestIndex = distances.indexOf(closestDistance); + + return nextValue < values[closestIndex] + ? closestIndex + : distances.lastIndexOf(closestDistance); } /** @@ -78,3 +82,8 @@ export function linearScale( return output[0] + ratio * (value - input[0]); }; } + +export function stopEventDefaultAndPropagation(event: Event) { + event.preventDefault(); + event.stopPropagation(); +}