Files
NapCatQQ/.pnpm-store/v3/files/1c/2ee4a0079a5d0c3109b3e6be2e4feb454e70676463c17e6b15bc0e5f2e1ae355069e6bcd50ac20c9e8dde7cc24b50b4e85548405725363b24415ee0eff408e
T
TanTan e311ca3275
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:25:58 +08:00

275 lines
8.3 KiB
Plaintext

import {AriaSliderThumbProps} from '@react-types/slider';
import {clamp, focusWithoutScrolling, mergeProps, useFormReset, useGlobalListeners} from '@react-aria/utils';
import {DOMAttributes, RefObject} from '@react-types/shared';
import {getSliderThumbId, sliderData} from './utils';
import React, {ChangeEvent, InputHTMLAttributes, LabelHTMLAttributes, useCallback, useEffect, useRef} from 'react';
import {SliderState} from '@react-stately/slider';
import {useFocusable} from '@react-aria/focus';
import {useKeyboard, useMove} from '@react-aria/interactions';
import {useLabel} from '@react-aria/label';
import {useLocale} from '@react-aria/i18n';
export interface SliderThumbAria {
/** Props for the root thumb element; handles the dragging motion. */
thumbProps: DOMAttributes,
/** Props for the visually hidden range input element. */
inputProps: InputHTMLAttributes<HTMLInputElement>,
/** Props for the label element for this thumb (optional). */
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
/** Whether this thumb is currently being dragged. */
isDragging: boolean,
/** Whether the thumb is currently focused. */
isFocused: boolean,
/** Whether the thumb is disabled. */
isDisabled: boolean
}
export interface AriaSliderThumbOptions extends AriaSliderThumbProps {
/** A ref to the track element. */
trackRef: RefObject<Element | null>,
/** A ref to the thumb input element. */
inputRef: RefObject<HTMLInputElement | null>
}
/**
* Provides behavior and accessibility for a thumb of a slider component.
*
* @param opts Options for this Slider thumb.
* @param state Slider state, created via `useSliderState`.
*/
export function useSliderThumb(
opts: AriaSliderThumbOptions,
state: SliderState
): SliderThumbAria {
let {
index = 0,
isRequired,
validationState,
isInvalid,
trackRef,
inputRef,
orientation = state.orientation,
name
} = opts;
let isDisabled = opts.isDisabled || state.isDisabled;
let isVertical = orientation === 'vertical';
let {direction} = useLocale();
let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
let data = sliderData.get(state)!;
const {labelProps, fieldProps} = useLabel({
...opts,
id: getSliderThumbId(state, index),
'aria-labelledby': `${data.id} ${opts['aria-labelledby'] ?? ''}`.trim()
});
const value = state.values[index];
const focusInput = useCallback(() => {
if (inputRef.current) {
focusWithoutScrolling(inputRef.current);
}
}, [inputRef]);
const isFocused = state.focusedThumb === index;
useEffect(() => {
if (isFocused) {
focusInput();
}
}, [isFocused, focusInput]);
let reverseX = direction === 'rtl';
let currentPosition = useRef<number>(null);
let {keyboardProps} = useKeyboard({
onKeyDown(e) {
let {
getThumbMaxValue,
getThumbMinValue,
decrementThumb,
incrementThumb,
setThumbValue,
setThumbDragging,
pageSize
} = state;
// these are the cases that useMove or useSlider don't handle
if (!/^(PageUp|PageDown|Home|End)$/.test(e.key)) {
e.continuePropagation();
return;
}
// same handling as useMove, stopPropagation to prevent useSlider from handling the event as well.
e.preventDefault();
// remember to set this so that onChangeEnd is fired
setThumbDragging(index, true);
switch (e.key) {
case 'PageUp':
incrementThumb(index, pageSize);
break;
case 'PageDown':
decrementThumb(index, pageSize);
break;
case 'Home':
setThumbValue(index, getThumbMinValue(index));
break;
case 'End':
setThumbValue(index, getThumbMaxValue(index));
break;
}
setThumbDragging(index, false);
}
});
let {moveProps} = useMove({
onMoveStart() {
currentPosition.current = null;
state.setThumbDragging(index, true);
},
onMove({deltaX, deltaY, pointerType, shiftKey}) {
const {
getThumbPercent,
setThumbPercent,
decrementThumb,
incrementThumb,
step,
pageSize
} = state;
if (!trackRef.current) {
return;
}
let {width, height} = trackRef.current.getBoundingClientRect();
let size = isVertical ? height : width;
if (currentPosition.current == null) {
currentPosition.current = getThumbPercent(index) * size;
}
if (pointerType === 'keyboard') {
if ((deltaX > 0 && reverseX) || (deltaX < 0 && !reverseX) || deltaY > 0) {
decrementThumb(index, shiftKey ? pageSize : step);
} else {
incrementThumb(index, shiftKey ? pageSize : step);
}
} else {
let delta = isVertical ? deltaY : deltaX;
if (isVertical || reverseX) {
delta = -delta;
}
currentPosition.current += delta;
setThumbPercent(index, clamp(currentPosition.current / size, 0, 1));
}
},
onMoveEnd() {
state.setThumbDragging(index, false);
}
});
// Immediately register editability with the state
state.setThumbEditable(index, !isDisabled);
const {focusableProps} = useFocusable(
mergeProps(opts, {
onFocus: () => state.setFocusedThumb(index),
onBlur: () => state.setFocusedThumb(undefined)
}),
inputRef
);
let currentPointer = useRef<number | undefined>(undefined);
let onDown = (id?: number) => {
focusInput();
currentPointer.current = id;
state.setThumbDragging(index, true);
addGlobalListener(window, 'mouseup', onUp, false);
addGlobalListener(window, 'touchend', onUp, false);
addGlobalListener(window, 'pointerup', onUp, false);
};
let onUp = (e) => {
let id = e.pointerId ?? e.changedTouches?.[0].identifier;
if (id === currentPointer.current) {
focusInput();
state.setThumbDragging(index, false);
removeGlobalListener(window, 'mouseup', onUp, false);
removeGlobalListener(window, 'touchend', onUp, false);
removeGlobalListener(window, 'pointerup', onUp, false);
}
};
let thumbPosition = state.getThumbPercent(index);
if (isVertical || direction === 'rtl') {
thumbPosition = 1 - thumbPosition;
}
let interactions = !isDisabled ? mergeProps(
keyboardProps,
moveProps,
{
onMouseDown: (e: React.MouseEvent) => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) {
return;
}
onDown();
},
onPointerDown: (e: React.PointerEvent) => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) {
return;
}
onDown(e.pointerId);
},
onTouchStart: (e: React.TouchEvent) => {onDown(e.changedTouches[0].identifier);}
}
) : {};
useFormReset(inputRef, value, (v) => {
state.setThumbValue(index, v);
});
// We install mouse handlers for the drag motion on the thumb div, but
// not the key handler for moving the thumb with the slider. Instead,
// we focus the range input, and let the browser handle the keyboard
// interactions; we then listen to input's onChange to update state.
return {
inputProps: mergeProps(focusableProps, fieldProps, {
type: 'range',
tabIndex: !isDisabled ? 0 : undefined,
min: state.getThumbMinValue(index),
max: state.getThumbMaxValue(index),
step: state.step,
value: value,
name,
disabled: isDisabled,
'aria-orientation': orientation,
'aria-valuetext': state.getThumbValueLabel(index),
'aria-required': isRequired || undefined,
'aria-invalid': isInvalid || validationState === 'invalid' || undefined,
'aria-errormessage': opts['aria-errormessage'],
'aria-describedby': [data['aria-describedby'], opts['aria-describedby']].filter(Boolean).join(' '),
'aria-details': [data['aria-details'], opts['aria-details']].filter(Boolean).join(' '),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
state.setThumbValue(index, parseFloat(e.target.value));
}
}),
thumbProps: {
...interactions,
style: {
position: 'absolute',
[isVertical ? 'top' : 'left']: `${thumbPosition * 100}%`,
transform: 'translate(-50%, -50%)',
touchAction: 'none'
}
},
labelProps,
isDragging: state.isThumbDragging(index),
isDisabled,
isFocused
};
}