UNPKG

@react-md/form

Version:

This package is for creating all the different form input types.

281 lines (257 loc) 6.5 kB
import { getPercentage, nearest } from "@react-md/utils"; import type { DefinedSliderValueOptions, RangeSliderControls, RangeSliderValue, SliderControls, SliderValue, ThumbIndex, } from "./types"; /** * Gets the number of steps in the allowed range of values. * * @internal * @remarks \@since 2.5.0 */ export const getSteps = (min: number, max: number, step: number): number => Math.abs(max - min) / step; /** * * @internal * @remarks \@since 2.5.0 */ export const getJumpValue = ( min: number, max: number, step: number, jump: number | undefined ): number => { const steps = getSteps(min, max, step); const value = jump ?? (steps / 10) * step; if (Number.isInteger(step)) { return Math.ceil(value); } return value; }; /** * @internal * @remarks \@since 2.5.0 */ export type SliderDragEvent = | MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent; /** * @internal * @remarks \@since 2.5.0 */ export const isMouseEvent = ( event: SliderDragEvent ): event is MouseEvent & { type: "mousedown" | "mousemove" | "mouseup" } => event.type === "mousedown" || event.type === "mousemove" || event.type === "mouseup"; /** * @internal * @remarks \@since 2.5.0 */ export const isTouchEvent = ( event: SliderDragEvent ): event is TouchEvent & { type: "touchstart" | "touchmove" | "touchend" } => event.type === "touchstart" || event.type === "touchmove" || event.type === "touchend"; /** * @internal * @remarks \@since 2.5.0 */ export interface SimpleSliderControls extends SliderControls { value: SliderValue; } /** * @internal * @remarks \@since 2.5.0 */ export interface ComplexSliderControls extends RangeSliderControls { value: RangeSliderValue; } /** * @internal * @remarks \@since 2.5.0 */ export type CombinedSliderControls = | SimpleSliderControls | ComplexSliderControls; /** * @internal * @remarks \@since 2.5.0 */ export const isRangeSlider = ( controls: CombinedSliderControls ): controls is ComplexSliderControls => Array.isArray(controls.value); /** * @internal * @remarks \@since 2.5.0 */ export interface SliderDragValues extends DefinedSliderValueOptions { clientX: number; clientY: number; top: number; height: number; left: number; width: number; isRtl: boolean; vertical: boolean; minValue: number; maxValue: number; } /** * @remarks \@since 2.5.0 * @internal */ export interface SliderDragValue { /** * This is the current value for the slider that is completely "valid" and * within the provided range. */ value: number; /** * The current percentage dragged number (`> 0` and `< 1`). This is used only * while dragging with the mouse or touch since it makes the drag experience * smoother. If this is omitted and there is a small number of "steps" in the * range, the mouse/touch won't align with the thumb since it will only move * when the value is updated as well. * * Example: * - slider has width of 1000px, min value is 0, max value is 100, step is 20 * - formula: * - range = max - min * - steps = range / step * - new-value-at = slider-width / steps * - new-value-at = slider-width / ((max - min) / step) * - so: * - new-value-at = 1000px / ((100 - 0) / 20) * - new-value-at = 1000px / (100 / 20) * - new-value-at = 1000px / 5 * - new-value-at = 200px * - user drags from `0px -> 10px` * - no visual change * - user drags from `10px -> 190px` * - no visual change * - user drags from `190px -> 200px` * - visual change to first step * * The current value allows for a visual change while the user drags, but the * thumb will move to the correct value once the user stops dragging. */ current: number; } /** * This is used to get the next value for the slider while being dragged via * mouse or touch. * * @internal * @remarks \@since 2.5.0 */ export const getDragValue = ({ min, max, step, vertical, clientX, clientY, left, top, height, width, isRtl, minValue, maxValue, }: SliderDragValues): SliderDragValue => { const sliderSize = vertical ? height : width; const sliderPosition = vertical ? top + height : left; const cursorPosition = vertical ? clientY : clientX; const difference = vertical ? sliderPosition - cursorPosition : cursorPosition - sliderPosition; const distanceDragged = Math.min(Math.max(0, difference), sliderSize); let percentageDragged = distanceDragged / sliderSize; if (isRtl && !vertical) { percentageDragged = 1 - percentageDragged; } const range = max - min; const steps = getSteps(min, max, step); const value = percentageDragged * range + min; const rounded = nearest(value, minValue, maxValue, steps, range); return { value: rounded, current: percentageDragged, }; }; /** * @remarks \@since 2.5.0 * @internal */ interface DragPercentageOptions { min: number; max: number; thumb1Value: number; thumb2Value?: number; dragging: boolean; dragValue: number; draggingIndex: ThumbIndex | null; } interface DragPercentage { thumb1Percentage: string; thumb2Percentage: string | undefined; } /** * Small util to get the drag percentage for the thumbs within a slider. This * makes sure to use the current `dragValue` when possible so that the thumb * moves with the mouse/touch instead of only for the current values. See * {@link getDragValue} for more examples. * * @remarks \@since 2.5.0 * @internal */ export const getDragPercentage = ({ min, max, dragging, dragValue, draggingIndex, thumb1Value, thumb2Value, }: DragPercentageOptions): DragPercentage => { let thumb1Percentage = dragging && draggingIndex === 0 ? dragValue : getPercentage({ min, max, value: thumb1Value, validate: false, }); let thumb2Percentage: number | undefined; if (typeof thumb2Value === "number") { const percentage = getPercentage({ min, max, value: thumb2Value, validate: false, }); thumb1Percentage = Math.min(thumb1Percentage, percentage); thumb2Percentage = dragging && draggingIndex === 1 ? Math.max(thumb1Percentage, dragValue) : percentage; } return { thumb1Percentage: `${thumb1Percentage * 100}%`, thumb2Percentage: typeof thumb2Percentage === "number" ? `${thumb2Percentage * 100}%` : undefined, }; };