@base-ui-components/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
237 lines (229 loc) • 7.38 kB
JavaScript
'use client';
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps.js';
import { ownerDocument } from '../../utils/owner.js';
import { useForkRef } from '../../utils/useForkRef.js';
import { useEventCallback } from '../../utils/useEventCallback.js';
import { focusThumb, trackFinger, validateMinimumDistance } from '../root/useSliderRoot.js';
import { useFieldControlValidation } from '../../field/control/useFieldControlValidation.js';
const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
export function useSliderControl(parameters) {
const {
areValuesEqual,
disabled,
dragging,
getFingerNewValue,
handleValueChange,
onValueCommitted,
minStepsBetweenValues,
percentageValues,
registerSliderControl,
rootRef: externalRef,
setActive,
setDragging,
setValueState,
step,
thumbRefs
} = parameters;
const {
commitValidation
} = useFieldControlValidation();
const controlRef = React.useRef(null);
const handleRootRef = useForkRef(externalRef, registerSliderControl, controlRef);
// A number that uniquely identifies the current finger in the touch session.
const touchIdRef = React.useRef(null);
const moveCountRef = React.useRef(0);
// offset distance between:
// 1. pointerDown coordinates and
// 2. the exact intersection of the center of the thumb and the track
const offsetRef = React.useRef(0);
const handleTouchMove = useEventCallback(nativeEvent => {
const finger = trackFinger(nativeEvent, touchIdRef);
if (!finger) {
return;
}
moveCountRef.current += 1;
// Cancel move in case some other element consumed a pointerup event and it was not fired.
// @ts-ignore buttons doesn't not exists on touch event
if (nativeEvent.type === 'pointermove' && nativeEvent.buttons === 0) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
handleTouchEnd(nativeEvent);
return;
}
const newFingerValue = getFingerNewValue({
finger,
move: true,
offset: offsetRef.current
});
if (!newFingerValue) {
return;
}
const {
newValue,
activeIndex
} = newFingerValue;
focusThumb({
sliderRef: controlRef,
activeIndex,
setActive
});
if (validateMinimumDistance(newValue, step, minStepsBetweenValues)) {
setValueState(newValue);
if (!dragging && moveCountRef.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) {
setDragging(true);
}
if (handleValueChange && !areValuesEqual(newValue)) {
handleValueChange(newValue, activeIndex, nativeEvent);
}
}
});
const handleTouchEnd = useEventCallback(nativeEvent => {
const finger = trackFinger(nativeEvent, touchIdRef);
setDragging(false);
if (!finger) {
return;
}
const newFingerValue = getFingerNewValue({
finger,
move: true
});
if (!newFingerValue) {
return;
}
setActive(-1);
commitValidation(newFingerValue.newValue);
if (onValueCommitted) {
onValueCommitted(newFingerValue.newValue, nativeEvent);
}
touchIdRef.current = null;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stopListening();
});
const handleTouchStart = useEventCallback(nativeEvent => {
if (disabled) {
return;
}
const touch = nativeEvent.changedTouches[0];
if (touch != null) {
touchIdRef.current = touch.identifier;
}
const finger = trackFinger(nativeEvent, touchIdRef);
if (finger !== false) {
const newFingerValue = getFingerNewValue({
finger
});
if (!newFingerValue) {
return;
}
const {
newValue,
activeIndex
} = newFingerValue;
focusThumb({
sliderRef: controlRef,
activeIndex,
setActive
});
setValueState(newValue);
if (handleValueChange && !areValuesEqual(newValue)) {
handleValueChange(newValue, activeIndex, nativeEvent);
}
}
moveCountRef.current = 0;
const doc = ownerDocument(controlRef.current);
doc.addEventListener('touchmove', handleTouchMove, {
passive: true
});
doc.addEventListener('touchend', handleTouchEnd, {
passive: true
});
});
const stopListening = useEventCallback(() => {
offsetRef.current = 0;
const doc = ownerDocument(controlRef.current);
doc.removeEventListener('pointermove', handleTouchMove);
doc.removeEventListener('pointerup', handleTouchEnd);
doc.removeEventListener('touchmove', handleTouchMove);
doc.removeEventListener('touchend', handleTouchEnd);
});
React.useEffect(() => {
const {
current: sliderControl
} = controlRef;
if (!sliderControl) {
return () => stopListening();
}
sliderControl.addEventListener('touchstart', handleTouchStart, {
passive: true
});
return () => {
sliderControl.removeEventListener('touchstart', handleTouchStart);
stopListening();
};
}, [stopListening, handleTouchStart, controlRef]);
React.useEffect(() => {
if (disabled) {
stopListening();
}
}, [disabled, stopListening]);
const getRootProps = React.useCallback((externalProps = {}) => {
return mergeReactProps(externalProps, {
onPointerDown(event) {
if (disabled) {
return;
}
if (event.defaultPrevented) {
return;
}
// Only handle left clicks
if (event.button !== 0) {
return;
}
// Avoid text selection
event.preventDefault();
const finger = trackFinger(event, touchIdRef);
if (finger !== false) {
const newFingerValue = getFingerNewValue({
finger
});
if (!newFingerValue) {
return;
}
const {
newValue,
activeIndex,
newPercentageValue
} = newFingerValue;
focusThumb({
sliderRef: controlRef,
activeIndex,
setActive
});
// if the event lands on a thumb, don't change the value, just get the
// percentageValue difference represented by the distance between the click origin
// and the coordinates of the value on the track area
if (thumbRefs.current.includes(event.target)) {
const targetThumbIndex = event.target.getAttribute('data-index');
const offset = percentageValues[Number(targetThumbIndex)] / 100 - newPercentageValue;
offsetRef.current = offset;
} else {
setValueState(newValue);
if (handleValueChange && !areValuesEqual(newValue)) {
handleValueChange(newValue, activeIndex, event);
}
}
}
moveCountRef.current = 0;
const doc = ownerDocument(controlRef.current);
doc.addEventListener('pointermove', handleTouchMove, {
passive: true
});
doc.addEventListener('pointerup', handleTouchEnd);
},
ref: handleRootRef
});
}, [areValuesEqual, disabled, getFingerNewValue, handleRootRef, handleTouchMove, handleTouchEnd, handleValueChange, percentageValues, setActive, setValueState, thumbRefs]);
return React.useMemo(() => ({
getRootProps
}), [getRootProps]);
}