import React, {Ref, useCallback, useEffect, useRef} from 'react'; import clsx from 'clsx'; import {UseSliderReturn} from './use-slider'; import {useGlobalListeners, useObjectRef} from '@react-aria/utils'; import {createEventHandler} from '@common/utils/dom/create-event-handler'; import {BaseSliderProps} from '@common/ui/forms/slider/base-slider'; interface SliderThumb { index: number; slider: UseSliderReturn; isDisabled?: boolean; ariaLabel?: string; inputRef?: Ref; onBlur?: React.FocusEventHandler; fillColor?: BaseSliderProps['fillColor']; } export function SliderThumb({ index, slider, isDisabled: isThumbDisabled, ariaLabel, inputRef, onBlur, fillColor = 'primary', }: SliderThumb) { const inputObjRef = useObjectRef(inputRef); const {addGlobalListener, removeGlobalListener} = useGlobalListeners(); const { step, values, focusedThumb, labelId, thumbIds, isDisabled: isSliderDisabled, getThumbPercent, getThumbMinValue, getThumbMaxValue, getThumbValueLabel, setThumbValue, updateDraggedThumbs, isThumbDragging, setThumbEditable, setFocusedThumb, isPointerOver, showThumbOnHoverOnly, thumbSize = 'w-18 h-18', } = slider; const isDragging = isThumbDragging(index); const value = values[index]; // Immediately register editability with the state setThumbEditable(index, !isThumbDisabled); const isDisabled = isThumbDisabled || isSliderDisabled; const focusInput = useCallback(() => { if (inputObjRef.current) { inputObjRef.current.focus({preventScroll: true}); } }, [inputObjRef]); // we will focus the native range input when slider is clicked or thumb is // focused in some other way, and let browser handle keyboard interactions const isFocused = focusedThumb === index; useEffect(() => { if (isFocused) { focusInput(); } }, [isFocused, focusInput]); const currentPointer = useRef(undefined); const handlePointerUp = (e: PointerEvent) => { if (e.pointerId === currentPointer.current) { focusInput(); updateDraggedThumbs(index, false); removeGlobalListener(window, 'pointerup', handlePointerUp, false); } }; const className = clsx( 'outline-none rounded-full top-1/2 -translate-y-1/2 -translate-x-1/2 absolute inset-0 transition-button duration-200', thumbSize, !isDisabled && 'shadow-md', thumbColor({fillColor, isDisabled, isDragging: isDragging}), // show thumb on hover and while dragging, otherwise "blur" event will fire on thumb and dragging will stop (showThumbOnHoverOnly && isDragging) || isPointerOver ? 'visible' : 'invisible' ); return (
{ if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { return; } focusInput(); currentPointer.current = e.pointerId; updateDraggedThumbs(index, true); addGlobalListener(window, 'pointerup', handlePointerUp, false); }} > { updateDraggedThumbs(index, true); })} onKeyUp={createEventHandler(() => { // make sure "onChangeEnd" is fired on keyboard navigation updateDraggedThumbs(index, false); })} ref={inputObjRef} tabIndex={!isDisabled ? 0 : undefined} min={getThumbMinValue(index)} max={getThumbMaxValue(index)} step={step} value={value} disabled={isDisabled} aria-label={ariaLabel} aria-labelledby={labelId} aria-orientation="horizontal" aria-valuetext={getThumbValueLabel(index)} onFocus={() => { setFocusedThumb(index); }} onBlur={e => { setFocusedThumb(undefined); updateDraggedThumbs(index, false); onBlur?.(e); }} onChange={e => { setThumbValue(index, parseFloat(e.target.value)); }} type="range" className="sr-only" />
); } interface SliderThumbColorProps { isDisabled?: boolean; isDragging: boolean; fillColor?: BaseSliderProps['fillColor']; } function thumbColor({ isDisabled, isDragging, fillColor, }: SliderThumbColorProps): string { if (isDisabled) { return 'bg-slider-disabled cursor-default'; } if (fillColor && fillColor !== 'primary') { return fillColor; } return clsx( 'hover:bg-primary-dark', isDragging ? 'bg-primary-dark' : 'bg-primary' ); }