Skip to content

Commit

Permalink
Merge pull request #72 from js-tool-pack/slider
Browse files Browse the repository at this point in the history
feat(components/slider): 新增 键盘控制增减的功能
  • Loading branch information
mengxinssfd authored Dec 12, 2023
2 parents 7c07f69 + 18b8ccc commit 9fee03e
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 47 deletions.
105 changes: 86 additions & 19 deletions packages/components/src/slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { getClassNames, getSafeNum, strip } from '@tool-pack/basic';
import {
getClassNames,
forEachRight,
getSafeNum,
forEach,
strip,
} from '@tool-pack/basic';
import { HandlersControlRef, Handlers, Marks, Dots } from './components';
import type { RequiredPart, Point } from '@tool-pack/types';
import React, { useEffect, useMemo, useRef } from 'react';
import { useForceUpdate, getClasses } from '@pkg/shared';
import type { SliderStaticProps } from './slider.types';
import { Handlers, Marks, Dots } from './components';
import { SliderFC } from './slider.types';

const cls = getClasses(
Expand All @@ -12,6 +18,7 @@ const cls = getClasses(
['disabled', 'vertical', 'reverse', 'range', 'with-marks'],
);
const defaultProps = {
keyboard: true,
reverse: false,
tooltip: true,
max: 100,
Expand All @@ -31,6 +38,7 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
keepRangeSorted,
attrs = {},
disabled,
keyboard,
vertical,
onChange,
reverse,
Expand All @@ -42,12 +50,12 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
} = props as RequiredPart<SliderStaticProps, keyof typeof defaultProps>;

const forceUpdate = useForceUpdate();

const isRange = Array.isArray(outerValue);
const valuesRef = useRef<number[]>([]);
const sortedValuesRef = useRef<number[]>([]);
const railRef = useRef<HTMLDivElement>(null);
const handlersControlRef = useRef<HandlersControlRef>(null);

const isRange = Array.isArray(outerValue);
const total = max - min;

// 同步外部 value
Expand Down Expand Up @@ -118,13 +126,16 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
/>
<Handlers
getValueFromMousePos={getValueFromMousePos}
onHandlerKeyDown={onHandlerKeyDown}
setValueOfIndex={setValueOfIndex}
keepRangeSorted={keepRangeSorted}
controlRef={handlersControlRef}
tooltipProps={tooltipProps}
valuesRef={valuesRef}
formatter={formatter}
setValues={setValue}
disabled={disabled}
vertical={vertical}
keyboard={keyboard}
reverse={reverse}
tooltip={tooltip}
total={total}
Expand All @@ -136,11 +147,64 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
</div>
);

function onHandlerKeyDown(stepScale: -1 | 1, index: number): void {
if (!keyboard) return;
const values = valuesRef.current;
const curr = values[index] as number;
setValueOfIndex(getLimitValue(getStepValue()), index);

function getStepValue(): number {
if (step !== 'mark') return stepScale * step + curr;
return getByMark();

function getByMark(): number {
const [markValue, markValues] = findClosestFromObjectNumberKeys(
marks,
curr,
);

const index = markValues.indexOf(markValue);
if (index === -1) return curr;

const next = getSafeNum(index + stepScale, 0, markValues.length - 1);
return markValues[next] as number;
}
}
}
function getMinAndMaxFromValues(): readonly [min: number, max: number] {
const sortedValues = sortedValuesRef.current;
if (!isRange) return [min, valuesRef.current.at(-1) ?? min];
return [sortedValues[0] ?? min, sortedValues.at(-1) ?? min];
}
function setValueOfIndex(value: number, index: number): void {
const values = valuesRef.current;

const prevChunk = values.slice(0, index);
const nextChunk = values.slice(index + 1);

if (keepRangeSorted && values.length > 1) {
if (prevChunk.length > 0) keepPrevChunkSorted();
if (nextChunk.length > 0) keepNextChunkSorted();
}

setValue([...prevChunk, strip(value), ...nextChunk]);
setTimeout(() => handlersControlRef.current?.focus(index));

function keepPrevChunkSorted() {
forEachRight(prevChunk, (v, i): false | void => {
if (v > value) {
prevChunk[i] = value;
} else return false;
});
}
function keepNextChunkSorted() {
forEach(nextChunk, (v, i): false | void => {
if (v < value) {
nextChunk[i] = value;
} else return false;
});
}
}
function setValue(values: number[], emit = true): void {
valuesRef.current = values;
sortedValuesRef.current = valuesRef.current.toSorted((a, b) => a - b);
Expand All @@ -154,14 +218,10 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
// 需要先找到最接近的数字,然后改成目标数字
const v = findClosestFromSortedArr(sortedValuesRef.current, compareValue);
const index = innerValues.indexOf(v);
setValue([
...innerValues.slice(0, index),
value,
...innerValues.slice(index + 1),
]);
setValueOfIndex(value, index);
return;
}
setValue([...innerValues.slice(0, -1), value]);
setValueOfIndex(value, 0);
}
function handleRailClick(e: React.MouseEvent<HTMLDivElement>): void {
if (disabled) return;
Expand Down Expand Up @@ -205,21 +265,28 @@ const _Slider: React.FC<SliderStaticProps> = React.forwardRef<
return getByMark();

function getByMark(): number {
if (!marks) return value;

const keys = Object.keys(marks);
const len = keys.length;
if (!len) return value;

const marksValues = keys.map(Number).sort((a, b) => a - b);
return findClosestFromSortedArr(marksValues, value);
return findClosestFromObjectNumberKeys(marks, value)[0];
}
}
function getScale() {
if (!vertical) return (x - rect.x) / rect.width;
return (y - rect.y) / rect.height;
}
}
function findClosestFromObjectNumberKeys(
obj: Record<number, unknown> | undefined,
defaults: number,
): [closest: number, sortedKeys: number[]] {
if (!obj) return [defaults, []];

const keys = Object.keys(obj);
const len = keys.length;
if (!len) return [defaults, []];

const sortedKeys = keys.map(Number).sort((a, b) => a - b);
const closest = findClosestFromSortedArr(sortedKeys, defaults);
return [closest, sortedKeys];
}
function findClosestFromSortedArr(arr: number[], value: number): number {
const len = arr.length;
if (!len) return value;
Expand Down
87 changes: 59 additions & 28 deletions packages/components/src/slider/components/Handlers.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import React, { MutableRefObject, useEffect, useMemo, useRef } from 'react';
import React, {
useImperativeHandle,
MutableRefObject,
useEffect,
useMemo,
useRef,
Ref,
} from 'react';
import type { ConvertOptional, Point } from '@tool-pack/types';
import type { SliderStaticProps } from '../slider.types';
import { forEachRight, forEach } from '@tool-pack/basic';
import { getClasses, Placement } from '@pkg/shared';
import { onDragEvent } from '@tool-pack/dom';
import { Tooltip } from '~/tooltip';

export interface HandlersControlRef {
focus(index: number): void;
}

interface Props
extends ConvertOptional<
Pick<
Expand All @@ -14,6 +24,7 @@ interface Props
| 'tooltipProps'
| 'formatter'
| 'disabled'
| 'keyboard'
| 'vertical'
| 'tooltip'
| 'reverse'
Expand All @@ -22,9 +33,11 @@ interface Props
| 'min'
>
> {
onHandlerKeyDown: (stepScale: -1 | 1, index: number) => void;
setValueOfIndex: (value: number, index: number) => void;
getValueFromMousePos: (pos: Point) => number;
setValues: (values: number[]) => void;
valuesRef: MutableRefObject<number[]>;
controlRef: Ref<HandlersControlRef>;
total: number;
}

Expand All @@ -35,11 +48,14 @@ export const Handlers: React.FC<Props> = (props) => {
getValueFromMousePos,
formatter = (v) => v,
tooltipProps = {},
onHandlerKeyDown,
keepRangeSorted,
setValues,
setValueOfIndex,
controlRef,
valuesRef,
vertical,
disabled,
keyboard,
reverse,
tooltip,
total,
Expand All @@ -53,6 +69,15 @@ export const Handlers: React.FC<Props> = (props) => {
const tooltipVisible = tooltip === 'always' ? true : undefined;
const tooltipDisabled = tooltip === 'always' ? false : !tooltip;

useImperativeHandle(controlRef, () => {
return {
focus(index: number) {
(
handlersRef.current?.children[index] as HTMLDivElement | undefined
)?.focus();
},
};
});
// 拖动事件
useEffect(() => {
const handlersEl = handlersRef.current;
Expand All @@ -64,31 +89,8 @@ export const Handlers: React.FC<Props> = (props) => {
return onDragEvent(
({ onMove }) => {
onMove((_e, currentXY) => {
const values = valuesRef.current;
const prev = values.slice(0, index);
const next = values.slice(index + 1);
const curr = getValueFromMousePos([currentXY.x, currentXY.y]);

if (keepRangeSorted && values.length > 1) {
if (prev.length > 0) keepPrevSorted();
if (next.length > 0) keepNextSorted();
}
setValues([...prev, curr, ...next]);

function keepPrevSorted() {
forEachRight(prev, (v, i): false | void => {
if (v > curr) {
prev[i] = curr;
} else return false;
});
}
function keepNextSorted() {
forEach(next, (v, i): false | void => {
if (v < curr) {
next[i] = curr;
} else return false;
});
}
setValueOfIndex(curr, index);
});
},
{ el: child },
Expand Down Expand Up @@ -146,7 +148,10 @@ export const Handlers: React.FC<Props> = (props) => {
placement={_placement}
key={index}
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
onKeyDown={keyboard ? (e) => handleKeyDown(e, index) : undefined}
tabIndex={disabled ? undefined : 1}
className={cls.__.handler}
style={styles[index]}
draggable={false}
Expand All @@ -155,4 +160,30 @@ export const Handlers: React.FC<Props> = (props) => {
))}
</div>
);

function handleKeyDown(
e: React.KeyboardEvent<HTMLDivElement>,
index: number,
) {
const key = e.key as (typeof ArrowKeys)[number];
if (!ArrowKeys.includes(key)) return;
e.preventDefault();

let direct = 1;
if (vertical) {
if (reverse && ['ArrowDown', 'ArrowUp'].includes(key)) direct = -1;
} else {
if (reverse && ['ArrowRight', 'ArrowLeft'].includes(key)) direct = -1;
}

onHandlerKeyDown((ArrowMap[key] * direct) as -1 | 1, index);
}
};

const ArrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const;
const ArrowMap: Record<(typeof ArrowKeys)[number], -1 | 1> = {
ArrowDown: -1,
ArrowLeft: -1,
ArrowRight: 1,
ArrowUp: 1,
};
28 changes: 28 additions & 0 deletions packages/components/src/slider/demo/keyboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* title: 按键操作
* description: 可以使用键盘上的上下左右箭头控制滑块的增减。
*/

import { Slider, Switch } from '@tool-pack/react-ui';
import React, { useState } from 'react';

const App: React.FC = () => {
const [value, setValue] = useState(50);
const [keyboard, setKeyboard] = useState(true);
return (
<>
keyboard:
<Switch onChange={setKeyboard} checked={keyboard} size="small" />
<Slider
onChange={(v) => {
setValue(v);
}}
keyboard={keyboard}
value={value}
/>
{value}
</>
);
};

export default App;
3 changes: 3 additions & 0 deletions packages/components/src/slider/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,8 @@ $hh: 16px;
0 1px 4px 0 var(--t-color-info-a-4),
inset 0 0 1px 0 var(--t-color-info-a-7);
user-select: none;
&:focus-visible {
outline: 0;
}
}
}
2 changes: 2 additions & 0 deletions packages/components/src/slider/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Slider 滑块。
<code src="./demo/tooltip.tsx"></code>
<code src="./demo/formatter.tsx"></code>
<code src="./demo/disabled.tsx"></code>
<code src="./demo/keyboard.tsx"></code>
<code src="./demo/vertical.tsx"></code>
<code src="./demo/range.tsx"></code>
<code src="./demo/marks.tsx"></code>
Expand All @@ -39,6 +40,7 @@ Slider 的属性说明如下:
| step | 步长。当 step 为 'mark' 并且 marks 不为空时,步长为 marks 的 key 值;当 step 为 number 时不能小于 0 | number \| 'mark' | 1 | -- |
| reverse | 反向 | boolean | -- | -- |
| disabled | 禁用 | boolean | -- | -- |
| keyboard | 可以使用键盘上的上下左右箭头控制滑块的增减 | boolean | true | -- |
| vertical | 垂直排列 | boolean | -- | -- |
| tooltip | 设置 tooltip 的显隐 | boolean | true | -- |
| tooltipProps | tooltip 参数,见 [Tooltip](./tooltip#api) | -- | -- | -- |
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/slider/slider.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface SliderStaticProps
value?: number[] | number;
step?: number | 'mark';
marks?: SliderMarks;
keyboard?: boolean;
vertical?: boolean;
disabled?: boolean;
reverse?: boolean;
Expand Down

0 comments on commit 9fee03e

Please sign in to comment.