Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 24 additions & 21 deletions packages/components/hooks/useMouseEvent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useEffect, useRef } from 'react';

import useLatest from './useLatest';

export type MouseEventLike = MouseEvent | React.MouseEvent | TouchEvent | React.TouchEvent;
export type MouseCallback = MouseEvent | React.MouseEvent | Touch | React.Touch;

Expand All @@ -10,6 +12,7 @@ export interface MouseCoordinate {
x: number;
y: number;
}

export interface MouseContext {
coordinate: MouseCoordinate;
}
Expand All @@ -26,6 +29,7 @@ type MouseEventOptions = {

const useMouseEvent = (elementRef: React.RefObject<HTMLElement>, options: MouseEventOptions) => {
const { enabled = true, enableTouch = true } = options;
const optionsRef = useLatest(options);
const isMovingRef = useRef(false);

const normalizeEvent = (e: MouseEventLike) => {
Expand Down Expand Up @@ -61,20 +65,21 @@ const useMouseEvent = (elementRef: React.RefObject<HTMLElement>, options: MouseE
const emitMouseChange = (e: MouseEventLike, handler?: (e: MouseCallback, ctx: MouseContext) => void) => {
if (!handler) return;
const event = normalizeEvent(e);
if (!event) return;
const coordinate = getCoordinate(event);
handler(event, { coordinate });
};

const handleMouseMove = (e: MouseEventLike) => {
if (!isMovingRef.current) return;
e.preventDefault();
emitMouseChange(e, options.onMove);
emitMouseChange(e, optionsRef.current.onMove);
};

const handleMouseUp = (e: MouseEventLike) => {
if (!isMovingRef.current) return;
isMovingRef.current = false;
emitMouseChange(e, options.onUp);
emitMouseChange(e, optionsRef.current.onUp);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('touchend', handleMouseUp);
Expand All @@ -87,50 +92,48 @@ const useMouseEvent = (elementRef: React.RefObject<HTMLElement>, options: MouseE
if ('button' in e && e.button !== 0) return;

isMovingRef.current = true;
emitMouseChange(e, options.onDown);
emitMouseChange(e, optionsRef.current.onDown);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
if (!enableTouch) return;
if (!optionsRef.current.enableTouch) return;
document.addEventListener('touchend', handleMouseUp);
document.addEventListener('touchmove', handleMouseMove, { passive: false });
};

const handleMouseEnter = (e: MouseEventLike) => {
emitMouseChange(e, options.onEnter);
emitMouseChange(e, optionsRef.current.onEnter);
};

const handleMouseLeave = (e: MouseEventLike) => {
emitMouseChange(e, options.onLeave);
emitMouseChange(e, optionsRef.current.onLeave);
};

useEffect(() => {
const el = elementRef.current;
if (!el || !enabled) return;

// 基本上只要开启了鼠标事件,就会用到这三个
// 有的组件虽然只需要 mousemove 的回调结果,但也需要 mousedown 和 mouseup 来控制状态
el.addEventListener('mousedown', handleMouseDown);
el.addEventListener('mousemove', handleMouseMove);
el.addEventListener('mouseup', handleMouseUp);
// 下面这两个一般是为了处理 hover 状态,可选性监听
options.onEnter && el.addEventListener('mouseenter', handleMouseEnter);
options.onLeave && el.addEventListener('mouseleave', handleMouseLeave);
optionsRef.current.onEnter && el.addEventListener('mouseenter', handleMouseEnter);
optionsRef.current.onLeave && el.addEventListener('mouseleave', handleMouseLeave);

if (!enableTouch) return;
el.addEventListener('touchstart', handleMouseDown, { passive: false });
el.addEventListener('touchend', handleMouseUp);
if (enableTouch) {
el.addEventListener('touchstart', handleMouseDown, { passive: false });
el.addEventListener('touchend', handleMouseUp);
}

return () => {
el.removeEventListener('mousedown', handleMouseDown);
el.removeEventListener('mouseenter', handleMouseDown);
el.removeEventListener('mouseenter', handleMouseEnter);
el.removeEventListener('mouseleave', handleMouseLeave);
el.removeEventListener('mousemove', handleMouseMove);
el.removeEventListener('mouseup', handleMouseUp);
el.removeEventListener('touchstart', handleMouseDown);
el.removeEventListener('touchend', handleMouseUp);

if (enableTouch) {
el.removeEventListener('touchstart', handleMouseDown);
el.removeEventListener('touchend', handleMouseUp);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef.current, options, enabled]);
}, [options, enabled, enableTouch]);

return {
isMoving: isMovingRef.current,
Expand Down
51 changes: 30 additions & 21 deletions packages/components/slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo, useRef } from 'react';
import classNames from 'classnames';
import { isFunction, isNumber, isString } from 'lodash-es';
import { isEqual, isFunction, isNumber, isString } from 'lodash-es';
import { largeNumberToFixed } from '@tdesign/common-js/input-number/large-number';

import { accAdd, numberToPercent } from '../_util/number';
Expand Down Expand Up @@ -33,11 +33,13 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
max,
min,
range,
showStep,
step,
tooltipProps,
className,
style,
onChange,
onChangeEnd,
} = props;
Comment thread
uyarn marked this conversation as resolved.

const sliderRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -100,21 +102,29 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
const sizeKey = isVertical ? 'height' : 'width';
const renderDots = isVertical ? dots.map((item) => ({ ...item, position: 1 - item.position })) : dots;

const handleInputChange = (newValue: number, nodeIndex: SliderHandleNode) => {
const handleInputChange = (newValue: number, nodeIndex: SliderHandleNode, isEnd?: boolean) => {
const safeValue = Number(newValue.toFixed(32));
let resultValue = Math.max(Math.min(max, safeValue), min);
if (precision) resultValue = Number(largeNumberToFixed(String(resultValue), precision));
// 判断是否出现左值大于右值
if (nodeIndex === LEFT_NODE && value && safeValue > value[RIGHT_NODE]) resultValue = value[RIGHT_NODE];
// 判断是否出现右值大于左值
if (nodeIndex === RIGHT_NODE && value && safeValue < value[LEFT_NODE]) resultValue = value[LEFT_NODE];
let finalValue: SliderValue;
if (Array.isArray(value)) {
const arrValue = value.slice();
arrValue[nodeIndex] = resultValue;
internalOnChange(arrValue);
finalValue = arrValue;
} else {
internalOnChange(resultValue);
finalValue = resultValue;
}
if (isEqual(value, finalValue)) {
if (isEnd) {
onChangeEnd?.(finalValue);
}
return;
}
internalOnChange(finalValue);
Comment thread
uyarn marked this conversation as resolved.
};

const createInput = (nodeIndex: SliderHandleNode) => {
Expand All @@ -139,13 +149,13 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
);
};

const nearbyValueChange = (value: number) => {
const nearbyValueChange = (value: number, isEnd?: boolean) => {
const buttonBias =
Math.abs(value - renderValue[LEFT_NODE]) > Math.abs(value - renderValue[RIGHT_NODE]) ? RIGHT_NODE : LEFT_NODE;
handleInputChange(value, buttonBias);
handleInputChange(value, buttonBias, isEnd);
};

const setPosition = (position: number, nodeIndex?: SliderHandleNode) => {
const setPosition = (position: number, nodeIndex?: SliderHandleNode, isEnd?: boolean) => {
let index = 0;
let minDistance = 1;
for (let i = 0; i < allDots.length; i++) {
Expand All @@ -157,25 +167,25 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
}
const { value } = allDots[index];
if (nodeIndex === undefined && range) {
nearbyValueChange(value);
nearbyValueChange(value, isEnd);
} else {
handleInputChange(value, nodeIndex);
handleInputChange(value, nodeIndex, isEnd);
}
};

const onSliderChange = (event: MouseCallback, nodeIndex?: SliderHandleNode) => {
const onSliderChange = (event: MouseCallback, nodeIndex?: SliderHandleNode, isEnd?: boolean) => {
if (disabled || !sliderRef.current) return;

const clientKey = isVertical ? 'clientY' : 'clientX';
const sliderPositionInfo = sliderRef.current.getBoundingClientRect();
const sliderOffset = sliderPositionInfo[startDirection];
const position = ((event[clientKey] - sliderOffset) / sliderPositionInfo[sizeKey]) * (isVertical ? -1 : 1);
setPosition(position, nodeIndex);
setPosition(position, nodeIndex, isEnd);
};

const handleClickMarks = (event: React.MouseEvent, value: number) => {
event.stopPropagation();
nearbyValueChange(value);
nearbyValueChange(value, true);
};

const createHandleButton = (nodeIndex: SliderHandleNode, style: React.CSSProperties) => {
Expand All @@ -196,8 +206,10 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
toolTipProps={{ content: tipLabel, ...tooltipProps }}
hideTips={label === false}
classPrefix={classPrefix}
layout={layout}
style={style}
onChange={(e) => onSliderChange(e, nodeIndex)}
onChangeEnd={(e) => onSliderChange(e, nodeIndex, true)}
/>
);
};
Expand All @@ -215,7 +227,7 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
[`${classPrefix}-slider--vertical`]: isVertical,
[`${classPrefix}-slider--with-input`]: inputNumberProps,
})}
onClick={onSliderChange}
onClick={(e) => onSliderChange(e, undefined, true)}
>
<div className={classNames(`${classPrefix}-slider__rail`)}>
<div
Expand All @@ -225,18 +237,15 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>((originalProps, ref
{range ? createHandleButton(LEFT_NODE, { [startDirection]: numberToPercent(start) }) : null}
{createHandleButton(RIGHT_NODE, { [startDirection]: numberToPercent(end) })}
<div className={`${classPrefix}-slider__stops`}>
{renderDots.map(({ position, value }) => {
if (position === 0 || position === 1) {
return null;
}
return (
{(showStep ? allDots : renderDots)
.filter(({ position }) => position !== 0 && position !== 1)
.map(({ value: dotValue, position }) => (
<div
key={value}
key={dotValue}
style={{ [stepDirection]: numberToPercent(position) }}
className={classNames(`${classPrefix}-slider__stop`)}
></div>
);
})}
))}
Comment thread
uyarn marked this conversation as resolved.
</div>
<div className={classNames(`${classPrefix}-slider__mark`)}>
{renderDots.map(({ position, value, label }) => (
Expand Down
15 changes: 13 additions & 2 deletions packages/components/slider/SliderHandleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ import Tooltip from '../tooltip/Tooltip';

import type { MouseCallback } from '../hooks/useMouseEvent';
import type { TdTooltipProps } from '../tooltip/type';
import type { SliderProps } from './Slider';

interface SliderHandleButtonProps {
onChange: (event: MouseCallback) => void;
onChangeEnd?: (event: MouseCallback) => void;
classPrefix: string;
style: React.CSSProperties;
toolTipProps: TdTooltipProps;
hideTips: boolean;
layout: SliderProps['layout'];
}
Comment thread
uyarn marked this conversation as resolved.

const SliderHandleButton: React.FC<SliderHandleButtonProps> = ({
onChange,
onChangeEnd,
style,
classPrefix,
toolTipProps,
hideTips,
layout,
}) => {
const sliderNodeRef = useRef<HTMLDivElement>(null);
const [popupVisible, setPopupVisible] = useState(false);
Expand All @@ -42,11 +47,17 @@ const SliderHandleButton: React.FC<SliderHandleButtonProps> = ({
onUp: (e) => {
setPopupVisible(false);
onChange(e);
Comment thread
uyarn marked this conversation as resolved.
onChangeEnd?.(e);
},
});

const handleNode = (
<div ref={sliderNodeRef} style={style} className={`${classPrefix}-slider__button-wrapper`}>
<div
ref={sliderNodeRef}
style={style}
className={`${classPrefix}-slider__button-wrapper`}
onClick={(e) => e.stopPropagation()}
>
<div
className={classNames(`${classPrefix}-slider__button`, {
[`${classPrefix}-slider__button--dragging`]: isMoving,
Expand All @@ -58,7 +69,7 @@ const SliderHandleButton: React.FC<SliderHandleButtonProps> = ({
return hideTips ? (
handleNode
) : (
<Tooltip visible={popupVisible} placement="top" {...toolTipProps}>
<Tooltip visible={popupVisible} placement={layout === 'horizontal' ? 'top' : 'right'} {...toolTipProps}>
{handleNode}
</Tooltip>
);
Expand Down
14 changes: 11 additions & 3 deletions packages/components/slider/_example/step.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React, { useState } from 'react';
import { Slider } from 'tdesign-react';
import { Slider, Space, Switch } from 'tdesign-react';

import type { SliderValue } from 'tdesign-react';

const StepSlider = () => {
const [showStep, setShowStep] = useState<boolean>(false);
const [value, setValue] = useState<SliderValue>(10);
const [rangeValue, setRangeValue] = useState<SliderValue>([10, 80]);

return (
<>
<Slider style={{ marginBottom: 50 }} step={4} value={value} onChange={setValue}></Slider>
<Slider value={rangeValue} step={4} onChange={setRangeValue} range></Slider>
<Space style={{ marginBottom: 60 }}>
<Switch value={showStep} onChange={(v) => setShowStep(v)} />
显示步长刻度
</Space>

<Space direction="vertical" size={50} style={{ width: '100%' }}>
<Slider value={value} step={4} showStep={showStep} onChange={setValue}></Slider>
<Slider value={rangeValue} step={4} showStep={showStep} onChange={setRangeValue} range></Slider>
</Space>
</>
);
};
Expand Down
3 changes: 3 additions & 0 deletions packages/components/slider/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import type { TdSliderProps } from './type';

export const sliderDefaultProps: TdSliderProps = {
disabled: undefined,
inputNumberProps: false,
label: true,
layout: 'horizontal',
max: 100,
min: 0,
range: false,
showStep: false,
step: 1,
defaultValue: 0,
};
9 changes: 5 additions & 4 deletions packages/components/slider/slider.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N
disabled | Boolean | false | \- | N
disabled | Boolean | undefined | \- | N
inputNumberProps | Boolean / Object | false | Typescript: `boolean \| InputNumberProps`,[InputNumber API Documents](./input-number?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
label | TNode | true | Typescript: `string \| boolean \| TNode<{ value: SliderValue; position?: 'start' \| 'end' }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
layout | String | horizontal | optionsvertical/horizontal | N
layout | String | horizontal | options: vertical/horizontal | N
marks | Object / Array | - | Typescript: `Array<number> \| SliderMarks` `interface SliderMarks { [mark: number]: string \| TNode<{ value: number }> }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
max | Number | 100 | \- | N
min | Number | 0 | \- | N
range | Boolean | false | \- | N
showStep | Boolean | false | \- | N
step | Number | 1 | \- | N
tooltipProps | Object | - | Typescript: `TooltipProps`,[Tooltip API Documents](./tooltip?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
value | Number / Array | - | Typescript: `SliderValue` `type SliderValue = number \| Array<number>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
defaultValue | Number / Array | - | uncontrolled property。Typescript: `SliderValue` `type SliderValue = number \| Array<number>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
value | Number / Array | 0 | Typescript: `SliderValue` `type SliderValue = number \| Array<number>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
defaultValue | Number / Array | 0 | uncontrolled property。Typescript: `SliderValue` `type SliderValue = number \| Array<number>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/slider/type.ts) | N
onChange | Function | | Typescript: `(value: SliderValue) => void`<br/> | N
onChangeEnd | Function | | Typescript: `(value: SliderValue) => void`<br/>triggered when the mouse button is released after dragging or clicking on the slider bar. It is suitable for scenarios where you do not want the callback to be triggered frequently during the process of dragging the slider | N
Loading
Loading