Files
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

175 lines
4.7 KiB
TypeScript
Executable File

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<HTMLInputElement>;
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<number | undefined>(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 ||
(showThumbOnHoverOnly && isDragging) ||
isPointerOver
? 'visible'
: 'invisible',
);
return (
<div
role="presentation"
className={className}
style={{
left: `${Math.max(getThumbPercent(index) * 100, 0)}%`,
}}
onPointerDown={e => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) {
return;
}
focusInput();
currentPointer.current = e.pointerId;
updateDraggedThumbs(index, true);
addGlobalListener(window, 'pointerup', handlePointerUp, false);
}}
>
<input
id={thumbIds[index]}
onKeyDown={createEventHandler(() => {
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"
/>
</div>
);
}
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',
);
}