UNPKG

20.5 kBJavaScriptView Raw
1"use strict";
2'use client';
3
4var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
5var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
6Object.defineProperty(exports, "__esModule", {
7 value: true
8});
9exports.Identity = void 0;
10exports.useSlider = useSlider;
11exports.valueToPercent = valueToPercent;
12var React = _interopRequireWildcard(require("react"));
13var _utils = require("@mui/utils");
14var _extractEventHandlers = _interopRequireDefault(require("@mui/utils/extractEventHandlers"));
15var _areArraysEqual = _interopRequireDefault(require("../utils/areArraysEqual"));
16const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
17function asc(a, b) {
18 return a - b;
19}
20function findClosest(values, currentValue) {
21 const {
22 index: closestIndex
23 } = values.reduce((acc, value, index) => {
24 const distance = Math.abs(currentValue - value);
25 if (acc === null || distance < acc.distance || distance === acc.distance) {
26 return {
27 distance,
28 index
29 };
30 }
31 return acc;
32 }, null) ?? {};
33 return closestIndex;
34}
35function trackFinger(event, touchId) {
36 // The event is TouchEvent
37 if (touchId.current !== undefined && event.changedTouches) {
38 const touchEvent = event;
39 for (let i = 0; i < touchEvent.changedTouches.length; i += 1) {
40 const touch = touchEvent.changedTouches[i];
41 if (touch.identifier === touchId.current) {
42 return {
43 x: touch.clientX,
44 y: touch.clientY
45 };
46 }
47 }
48 return false;
49 }
50
51 // The event is MouseEvent
52 return {
53 x: event.clientX,
54 y: event.clientY
55 };
56}
57function valueToPercent(value, min, max) {
58 return (value - min) * 100 / (max - min);
59}
60function percentToValue(percent, min, max) {
61 return (max - min) * percent + min;
62}
63function getDecimalPrecision(num) {
64 // This handles the case when num is very small (0.00000001), js will turn this into 1e-8.
65 // When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine.
66 if (Math.abs(num) < 1) {
67 const parts = num.toExponential().split('e-');
68 const matissaDecimalPart = parts[0].split('.')[1];
69 return (matissaDecimalPart ? matissaDecimalPart.length : 0) + parseInt(parts[1], 10);
70 }
71 const decimalPart = num.toString().split('.')[1];
72 return decimalPart ? decimalPart.length : 0;
73}
74function roundValueToStep(value, step, min) {
75 const nearest = Math.round((value - min) / step) * step + min;
76 return Number(nearest.toFixed(getDecimalPrecision(step)));
77}
78function setValueIndex({
79 values,
80 newValue,
81 index
82}) {
83 const output = values.slice();
84 output[index] = newValue;
85 return output.sort(asc);
86}
87function focusThumb({
88 sliderRef,
89 activeIndex,
90 setActive
91}) {
92 const doc = (0, _utils.unstable_ownerDocument)(sliderRef.current);
93 if (!sliderRef.current?.contains(doc.activeElement) || Number(doc?.activeElement?.getAttribute('data-index')) !== activeIndex) {
94 sliderRef.current?.querySelector(`[type="range"][data-index="${activeIndex}"]`).focus();
95 }
96 if (setActive) {
97 setActive(activeIndex);
98 }
99}
100function areValuesEqual(newValue, oldValue) {
101 if (typeof newValue === 'number' && typeof oldValue === 'number') {
102 return newValue === oldValue;
103 }
104 if (typeof newValue === 'object' && typeof oldValue === 'object') {
105 return (0, _areArraysEqual.default)(newValue, oldValue);
106 }
107 return false;
108}
109const axisProps = {
110 horizontal: {
111 offset: percent => ({
112 left: `${percent}%`
113 }),
114 leap: percent => ({
115 width: `${percent}%`
116 })
117 },
118 'horizontal-reverse': {
119 offset: percent => ({
120 right: `${percent}%`
121 }),
122 leap: percent => ({
123 width: `${percent}%`
124 })
125 },
126 vertical: {
127 offset: percent => ({
128 bottom: `${percent}%`
129 }),
130 leap: percent => ({
131 height: `${percent}%`
132 })
133 }
134};
135const Identity = x => x;
136
137// TODO: remove support for Safari < 13.
138// https://caniuse.com/#search=touch-action
139//
140// Safari, on iOS, supports touch action since v13.
141// Over 80% of the iOS phones are compatible
142// in August 2020.
143// Utilizing the CSS.supports method to check if touch-action is supported.
144// Since CSS.supports is supported on all but Edge@12 and IE and touch-action
145// is supported on both Edge@12 and IE if CSS.supports is not available that means that
146// touch-action will be supported
147exports.Identity = Identity;
148let cachedSupportsTouchActionNone;
149function doesSupportTouchActionNone() {
150 if (cachedSupportsTouchActionNone === undefined) {
151 if (typeof CSS !== 'undefined' && typeof CSS.supports === 'function') {
152 cachedSupportsTouchActionNone = CSS.supports('touch-action', 'none');
153 } else {
154 cachedSupportsTouchActionNone = true;
155 }
156 }
157 return cachedSupportsTouchActionNone;
158}
159/**
160 *
161 * Demos:
162 *
163 * - [Slider](https://mui.com/base-ui/react-slider/#hook)
164 *
165 * API:
166 *
167 * - [useSlider API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider)
168 */
169function useSlider(parameters) {
170 const {
171 'aria-labelledby': ariaLabelledby,
172 defaultValue,
173 disabled = false,
174 disableSwap = false,
175 isRtl = false,
176 marks: marksProp = false,
177 max = 100,
178 min = 0,
179 name,
180 onChange,
181 onChangeCommitted,
182 orientation = 'horizontal',
183 rootRef: ref,
184 scale = Identity,
185 step = 1,
186 shiftStep = 10,
187 tabIndex,
188 value: valueProp
189 } = parameters;
190 const touchId = React.useRef(undefined);
191 // We can't use the :active browser pseudo-classes.
192 // - The active state isn't triggered when clicking on the rail.
193 // - The active state isn't transferred when inversing a range slider.
194 const [active, setActive] = React.useState(-1);
195 const [open, setOpen] = React.useState(-1);
196 const [dragging, setDragging] = React.useState(false);
197 const moveCount = React.useRef(0);
198 const [valueDerived, setValueState] = (0, _utils.unstable_useControlled)({
199 controlled: valueProp,
200 default: defaultValue ?? min,
201 name: 'Slider'
202 });
203 const handleChange = onChange && ((event, value, thumbIndex) => {
204 // Redefine target to allow name and value to be read.
205 // This allows seamless integration with the most popular form libraries.
206 // https://github.com/mui/material-ui/issues/13485#issuecomment-676048492
207 // Clone the event to not override `target` of the original event.
208 const nativeEvent = event.nativeEvent || event;
209 // @ts-ignore The nativeEvent is function, not object
210 const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent);
211 Object.defineProperty(clonedEvent, 'target', {
212 writable: true,
213 value: {
214 value,
215 name
216 }
217 });
218 onChange(clonedEvent, value, thumbIndex);
219 });
220 const range = Array.isArray(valueDerived);
221 let values = range ? valueDerived.slice().sort(asc) : [valueDerived];
222 values = values.map(value => value == null ? min : (0, _utils.clamp)(value, min, max));
223 const marks = marksProp === true && step !== null ? [...Array(Math.floor((max - min) / step) + 1)].map((_, index) => ({
224 value: min + step * index
225 })) : marksProp || [];
226 const marksValues = marks.map(mark => mark.value);
227 const [focusedThumbIndex, setFocusedThumbIndex] = React.useState(-1);
228 const sliderRef = React.useRef(null);
229 const handleRef = (0, _utils.unstable_useForkRef)(ref, sliderRef);
230 const createHandleHiddenInputFocus = otherHandlers => event => {
231 const index = Number(event.currentTarget.getAttribute('data-index'));
232 if ((0, _utils.unstable_isFocusVisible)(event.target)) {
233 setFocusedThumbIndex(index);
234 }
235 setOpen(index);
236 otherHandlers?.onFocus?.(event);
237 };
238 const createHandleHiddenInputBlur = otherHandlers => event => {
239 if (!(0, _utils.unstable_isFocusVisible)(event.target)) {
240 setFocusedThumbIndex(-1);
241 }
242 setOpen(-1);
243 otherHandlers?.onBlur?.(event);
244 };
245 const changeValue = (event, valueInput) => {
246 const index = Number(event.currentTarget.getAttribute('data-index'));
247 const value = values[index];
248 const marksIndex = marksValues.indexOf(value);
249 let newValue = valueInput;
250 if (marks && step == null) {
251 const maxMarksValue = marksValues[marksValues.length - 1];
252 if (newValue > maxMarksValue) {
253 newValue = maxMarksValue;
254 } else if (newValue < marksValues[0]) {
255 newValue = marksValues[0];
256 } else {
257 newValue = newValue < value ? marksValues[marksIndex - 1] : marksValues[marksIndex + 1];
258 }
259 }
260 newValue = (0, _utils.clamp)(newValue, min, max);
261 if (range) {
262 // Bound the new value to the thumb's neighbours.
263 if (disableSwap) {
264 newValue = (0, _utils.clamp)(newValue, values[index - 1] || -Infinity, values[index + 1] || Infinity);
265 }
266 const previousValue = newValue;
267 newValue = setValueIndex({
268 values,
269 newValue,
270 index
271 });
272 let activeIndex = index;
273
274 // Potentially swap the index if needed.
275 if (!disableSwap) {
276 activeIndex = newValue.indexOf(previousValue);
277 }
278 focusThumb({
279 sliderRef,
280 activeIndex
281 });
282 }
283 setValueState(newValue);
284 setFocusedThumbIndex(index);
285 if (handleChange && !areValuesEqual(newValue, valueDerived)) {
286 handleChange(event, newValue, index);
287 }
288 if (onChangeCommitted) {
289 onChangeCommitted(event, newValue);
290 }
291 };
292 const createHandleHiddenInputKeyDown = otherHandlers => event => {
293 // The Shift + Up/Down keyboard shortcuts for moving the slider makes sense to be supported
294 // only if the step is defined. If the step is null, this means tha the marks are used for specifying the valid values.
295 if (step !== null) {
296 const index = Number(event.currentTarget.getAttribute('data-index'));
297 const value = values[index];
298 let newValue = null;
299 if ((event.key === 'ArrowLeft' || event.key === 'ArrowDown') && event.shiftKey || event.key === 'PageDown') {
300 newValue = Math.max(value - shiftStep, min);
301 } else if ((event.key === 'ArrowRight' || event.key === 'ArrowUp') && event.shiftKey || event.key === 'PageUp') {
302 newValue = Math.min(value + shiftStep, max);
303 }
304 if (newValue !== null) {
305 changeValue(event, newValue);
306 event.preventDefault();
307 }
308 }
309 otherHandlers?.onKeyDown?.(event);
310 };
311 (0, _utils.unstable_useEnhancedEffect)(() => {
312 if (disabled && sliderRef.current.contains(document.activeElement)) {
313 // This is necessary because Firefox and Safari will keep focus
314 // on a disabled element:
315 // https://codesandbox.io/p/sandbox/mui-pr-22247-forked-h151h?file=/src/App.js
316 // @ts-ignore
317 document.activeElement?.blur();
318 }
319 }, [disabled]);
320 if (disabled && active !== -1) {
321 setActive(-1);
322 }
323 if (disabled && focusedThumbIndex !== -1) {
324 setFocusedThumbIndex(-1);
325 }
326 const createHandleHiddenInputChange = otherHandlers => event => {
327 otherHandlers.onChange?.(event);
328 // @ts-ignore
329 changeValue(event, event.target.valueAsNumber);
330 };
331 const previousIndex = React.useRef(undefined);
332 let axis = orientation;
333 if (isRtl && orientation === 'horizontal') {
334 axis += '-reverse';
335 }
336 const getFingerNewValue = ({
337 finger,
338 move = false
339 }) => {
340 const {
341 current: slider
342 } = sliderRef;
343 const {
344 width,
345 height,
346 bottom,
347 left
348 } = slider.getBoundingClientRect();
349 let percent;
350 if (axis.startsWith('vertical')) {
351 percent = (bottom - finger.y) / height;
352 } else {
353 percent = (finger.x - left) / width;
354 }
355 if (axis.includes('-reverse')) {
356 percent = 1 - percent;
357 }
358 let newValue;
359 newValue = percentToValue(percent, min, max);
360 if (step) {
361 newValue = roundValueToStep(newValue, step, min);
362 } else {
363 const closestIndex = findClosest(marksValues, newValue);
364 newValue = marksValues[closestIndex];
365 }
366 newValue = (0, _utils.clamp)(newValue, min, max);
367 let activeIndex = 0;
368 if (range) {
369 if (!move) {
370 activeIndex = findClosest(values, newValue);
371 } else {
372 activeIndex = previousIndex.current;
373 }
374
375 // Bound the new value to the thumb's neighbours.
376 if (disableSwap) {
377 newValue = (0, _utils.clamp)(newValue, values[activeIndex - 1] || -Infinity, values[activeIndex + 1] || Infinity);
378 }
379 const previousValue = newValue;
380 newValue = setValueIndex({
381 values,
382 newValue,
383 index: activeIndex
384 });
385
386 // Potentially swap the index if needed.
387 if (!(disableSwap && move)) {
388 activeIndex = newValue.indexOf(previousValue);
389 previousIndex.current = activeIndex;
390 }
391 }
392 return {
393 newValue,
394 activeIndex
395 };
396 };
397 const handleTouchMove = (0, _utils.unstable_useEventCallback)(nativeEvent => {
398 const finger = trackFinger(nativeEvent, touchId);
399 if (!finger) {
400 return;
401 }
402 moveCount.current += 1;
403
404 // Cancel move in case some other element consumed a mouseup event and it was not fired.
405 // @ts-ignore buttons doesn't not exists on touch event
406 if (nativeEvent.type === 'mousemove' && nativeEvent.buttons === 0) {
407 // eslint-disable-next-line @typescript-eslint/no-use-before-define
408 handleTouchEnd(nativeEvent);
409 return;
410 }
411 const {
412 newValue,
413 activeIndex
414 } = getFingerNewValue({
415 finger,
416 move: true
417 });
418 focusThumb({
419 sliderRef,
420 activeIndex,
421 setActive
422 });
423 setValueState(newValue);
424 if (!dragging && moveCount.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) {
425 setDragging(true);
426 }
427 if (handleChange && !areValuesEqual(newValue, valueDerived)) {
428 handleChange(nativeEvent, newValue, activeIndex);
429 }
430 });
431 const handleTouchEnd = (0, _utils.unstable_useEventCallback)(nativeEvent => {
432 const finger = trackFinger(nativeEvent, touchId);
433 setDragging(false);
434 if (!finger) {
435 return;
436 }
437 const {
438 newValue
439 } = getFingerNewValue({
440 finger,
441 move: true
442 });
443 setActive(-1);
444 if (nativeEvent.type === 'touchend') {
445 setOpen(-1);
446 }
447 if (onChangeCommitted) {
448 onChangeCommitted(nativeEvent, newValue);
449 }
450 touchId.current = undefined;
451
452 // eslint-disable-next-line @typescript-eslint/no-use-before-define
453 stopListening();
454 });
455 const handleTouchStart = (0, _utils.unstable_useEventCallback)(nativeEvent => {
456 if (disabled) {
457 return;
458 }
459 // If touch-action: none; is not supported we need to prevent the scroll manually.
460 if (!doesSupportTouchActionNone()) {
461 nativeEvent.preventDefault();
462 }
463 const touch = nativeEvent.changedTouches[0];
464 if (touch != null) {
465 // A number that uniquely identifies the current finger in the touch session.
466 touchId.current = touch.identifier;
467 }
468 const finger = trackFinger(nativeEvent, touchId);
469 if (finger !== false) {
470 const {
471 newValue,
472 activeIndex
473 } = getFingerNewValue({
474 finger
475 });
476 focusThumb({
477 sliderRef,
478 activeIndex,
479 setActive
480 });
481 setValueState(newValue);
482 if (handleChange && !areValuesEqual(newValue, valueDerived)) {
483 handleChange(nativeEvent, newValue, activeIndex);
484 }
485 }
486 moveCount.current = 0;
487 const doc = (0, _utils.unstable_ownerDocument)(sliderRef.current);
488 doc.addEventListener('touchmove', handleTouchMove, {
489 passive: true
490 });
491 doc.addEventListener('touchend', handleTouchEnd, {
492 passive: true
493 });
494 });
495 const stopListening = React.useCallback(() => {
496 const doc = (0, _utils.unstable_ownerDocument)(sliderRef.current);
497 doc.removeEventListener('mousemove', handleTouchMove);
498 doc.removeEventListener('mouseup', handleTouchEnd);
499 doc.removeEventListener('touchmove', handleTouchMove);
500 doc.removeEventListener('touchend', handleTouchEnd);
501 }, [handleTouchEnd, handleTouchMove]);
502 React.useEffect(() => {
503 const {
504 current: slider
505 } = sliderRef;
506 slider.addEventListener('touchstart', handleTouchStart, {
507 passive: doesSupportTouchActionNone()
508 });
509 return () => {
510 slider.removeEventListener('touchstart', handleTouchStart);
511 stopListening();
512 };
513 }, [stopListening, handleTouchStart]);
514 React.useEffect(() => {
515 if (disabled) {
516 stopListening();
517 }
518 }, [disabled, stopListening]);
519 const createHandleMouseDown = otherHandlers => event => {
520 otherHandlers.onMouseDown?.(event);
521 if (disabled) {
522 return;
523 }
524 if (event.defaultPrevented) {
525 return;
526 }
527
528 // Only handle left clicks
529 if (event.button !== 0) {
530 return;
531 }
532
533 // Avoid text selection
534 event.preventDefault();
535 const finger = trackFinger(event, touchId);
536 if (finger !== false) {
537 const {
538 newValue,
539 activeIndex
540 } = getFingerNewValue({
541 finger
542 });
543 focusThumb({
544 sliderRef,
545 activeIndex,
546 setActive
547 });
548 setValueState(newValue);
549 if (handleChange && !areValuesEqual(newValue, valueDerived)) {
550 handleChange(event, newValue, activeIndex);
551 }
552 }
553 moveCount.current = 0;
554 const doc = (0, _utils.unstable_ownerDocument)(sliderRef.current);
555 doc.addEventListener('mousemove', handleTouchMove, {
556 passive: true
557 });
558 doc.addEventListener('mouseup', handleTouchEnd);
559 };
560 const trackOffset = valueToPercent(range ? values[0] : min, min, max);
561 const trackLeap = valueToPercent(values[values.length - 1], min, max) - trackOffset;
562 const getRootProps = (externalProps = {}) => {
563 const externalHandlers = (0, _extractEventHandlers.default)(externalProps);
564 const ownEventHandlers = {
565 onMouseDown: createHandleMouseDown(externalHandlers || {})
566 };
567 const mergedEventHandlers = {
568 ...externalHandlers,
569 ...ownEventHandlers
570 };
571 return {
572 ...externalProps,
573 ref: handleRef,
574 ...mergedEventHandlers
575 };
576 };
577 const createHandleMouseOver = otherHandlers => event => {
578 otherHandlers.onMouseOver?.(event);
579 const index = Number(event.currentTarget.getAttribute('data-index'));
580 setOpen(index);
581 };
582 const createHandleMouseLeave = otherHandlers => event => {
583 otherHandlers.onMouseLeave?.(event);
584 setOpen(-1);
585 };
586 const getThumbProps = (externalProps = {}) => {
587 const externalHandlers = (0, _extractEventHandlers.default)(externalProps);
588 const ownEventHandlers = {
589 onMouseOver: createHandleMouseOver(externalHandlers || {}),
590 onMouseLeave: createHandleMouseLeave(externalHandlers || {})
591 };
592 return {
593 ...externalProps,
594 ...externalHandlers,
595 ...ownEventHandlers
596 };
597 };
598 const getThumbStyle = index => {
599 return {
600 // So the non active thumb doesn't show its label on hover.
601 pointerEvents: active !== -1 && active !== index ? 'none' : undefined
602 };
603 };
604 const getHiddenInputProps = (externalProps = {}) => {
605 const externalHandlers = (0, _extractEventHandlers.default)(externalProps);
606 const ownEventHandlers = {
607 onChange: createHandleHiddenInputChange(externalHandlers || {}),
608 onFocus: createHandleHiddenInputFocus(externalHandlers || {}),
609 onBlur: createHandleHiddenInputBlur(externalHandlers || {}),
610 onKeyDown: createHandleHiddenInputKeyDown(externalHandlers || {})
611 };
612 const mergedEventHandlers = {
613 ...externalHandlers,
614 ...ownEventHandlers
615 };
616 return {
617 tabIndex,
618 'aria-labelledby': ariaLabelledby,
619 'aria-orientation': orientation,
620 'aria-valuemax': scale(max),
621 'aria-valuemin': scale(min),
622 name,
623 type: 'range',
624 min: parameters.min,
625 max: parameters.max,
626 step: parameters.step === null && parameters.marks ? 'any' : parameters.step ?? undefined,
627 disabled,
628 ...externalProps,
629 ...mergedEventHandlers,
630 style: {
631 ..._utils.visuallyHidden,
632 direction: isRtl ? 'rtl' : 'ltr',
633 // So that VoiceOver's focus indicator matches the thumb's dimensions
634 width: '100%',
635 height: '100%'
636 }
637 };
638 };
639 return {
640 active,
641 axis: axis,
642 axisProps,
643 dragging,
644 focusedThumbIndex,
645 getHiddenInputProps,
646 getRootProps,
647 getThumbProps,
648 marks: marks,
649 open,
650 range,
651 rootRef: handleRef,
652 trackLeap,
653 trackOffset,
654 values,
655 getThumbStyle
656 };
657}
\No newline at end of file