From a93831d76299f4f735b6dbebab4a1b32bb909ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 19 Apr 2024 14:15:49 +0800 Subject: [PATCH] feat: add activeHandleRender (#981) * docs: update demo * feat: add activeHandleRender * chore: tmp unlock * 10.6.0-0 * chore: reorder to avoid break change * 10.6.0-1 * chore: fix logic * 10.6.0-2 * 10.6.0-3 * chore: opt drag * 10.6.0-4 * fix: blur miss * chore: cleanup * test: coverage --- docs/examples/multiple.tsx | 52 ++++++++++++++++++++----------- package.json | 4 +-- src/Handles/Handle.tsx | 63 ++++++++++++++++++++++++++------------ src/Handles/index.tsx | 52 ++++++++++++++++++++++++++++--- src/Slider.tsx | 9 ++++-- src/hooks/useDrag.ts | 20 ++++++------ tests/Range.test.js | 4 +-- tests/Tooltip.test.js | 28 +++++++++++++++++ 8 files changed, 175 insertions(+), 57 deletions(-) create mode 100644 tests/Tooltip.test.js diff --git a/docs/examples/multiple.tsx b/docs/examples/multiple.tsx index 2b251ba3e..55bc91d25 100644 --- a/docs/examples/multiple.tsx +++ b/docs/examples/multiple.tsx @@ -12,22 +12,38 @@ function log(value) { console.log(value); } -export default () => ( -
-
- +const NodeWrapper = ({ children }: { children: React.ReactElement }) => { + return
{React.cloneElement(children, {},
TOOLTIP
)}
; +}; + +export default () => { + const [value, setValue] = React.useState([0, 5, 8]); + + return ( +
+
+ { + // console.log('>>>', nextValue); + setValue(nextValue as any); + }} + activeHandleRender={(node) => {node}} + styles={{ + tracks: { + background: `linear-gradient(to right, blue, red)`, + }, + track: { + background: 'transparent', + }, + }} + /> +
-
-); + ); +}; diff --git a/package.json b/package.json index 6a82e098b..e23f32590 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-slider", - "version": "10.5.0", + "version": "10.6.0-4", "description": "Slider UI component for React", "keywords": [ "react", @@ -35,7 +35,7 @@ "docs:deploy": "gh-pages -d .doc", "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md", "now-build": "npm run docs:build", - "prepublishOnly": "npm run compile && np --yolo --no-publish", + "prepublishOnly": "npm run compile && np --yolo --no-publish --any-branch", "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "start": "dumi dev", "test": "rc-test" diff --git a/src/Handles/Handle.tsx b/src/Handles/Handle.tsx index fbf9c12a7..bf802145e 100644 --- a/src/Handles/Handle.tsx +++ b/src/Handles/Handle.tsx @@ -12,7 +12,8 @@ interface RenderProps { dragging: boolean; } -export interface HandleProps { +export interface HandleProps + extends Omit, 'onFocus' | 'onMouseEnter'> { prefixCls: string; style?: React.CSSProperties; value: number; @@ -20,10 +21,14 @@ export interface HandleProps { dragging: boolean; onStartMove: OnStartMove; onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; - render?: (origin: React.ReactElement, props: RenderProps) => React.ReactElement; + onFocus: (e: React.FocusEvent, index: number) => void; + onMouseEnter: (e: React.MouseEvent, index: number) => void; + render?: ( + origin: React.ReactElement>, + props: RenderProps, + ) => React.ReactElement; onChangeComplete?: () => void; + mock?: boolean; } const Handle = React.forwardRef((props, ref) => { @@ -37,6 +42,8 @@ const Handle = React.forwardRef((props, ref) => { dragging, onOffsetChange, onChangeComplete, + onFocus, + onMouseEnter, ...restProps } = props; const { @@ -63,6 +70,14 @@ const Handle = React.forwardRef((props, ref) => { } }; + const onInternalFocus = (e: React.FocusEvent) => { + onFocus?.(e, valueIndex); + }; + + const onInternalMouseEnter = (e: React.MouseEvent) => { + onMouseEnter(e, valueIndex); + }; + // =========================== Keyboard =========================== const onKeyDown: React.KeyboardEventHandler = (e) => { if (!disabled && keyboard) { @@ -131,13 +146,36 @@ const Handle = React.forwardRef((props, ref) => { const positionStyle = getDirectionStyle(direction, value, min, max); // ============================ Render ============================ + let divProps: React.HtmlHTMLAttributes = {}; + + if (valueIndex !== null) { + divProps = { + tabIndex: disabled ? null : getIndex(tabIndex, valueIndex), + role: 'slider', + 'aria-valuemin': min, + 'aria-valuemax': max, + 'aria-valuenow': value, + 'aria-disabled': disabled, + 'aria-label': getIndex(ariaLabelForHandle, valueIndex), + 'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex), + 'aria-valuetext': getIndex(ariaValueTextFormatterForHandle, valueIndex)?.(value), + 'aria-orientation': direction === 'ltr' || direction === 'rtl' ? 'horizontal' : 'vertical', + onMouseDown: onInternalStartMove, + onTouchStart: onInternalStartMove, + onFocus: onInternalFocus, + onMouseEnter: onInternalMouseEnter, + onKeyDown, + onKeyUp: handleKeyUp, + }; + } + let handleNode = (
((props, ref) => { ...style, ...styles.handle, }} - onMouseDown={onInternalStartMove} - onTouchStart={onInternalStartMove} - onKeyDown={onKeyDown} - onKeyUp={handleKeyUp} - tabIndex={disabled ? null : getIndex(tabIndex, valueIndex)} - role="slider" - aria-valuemin={min} - aria-valuemax={max} - aria-valuenow={value} - aria-disabled={disabled} - aria-label={getIndex(ariaLabelForHandle, valueIndex)} - aria-labelledby={getIndex(ariaLabelledByForHandle, valueIndex)} - aria-valuetext={getIndex(ariaValueTextFormatterForHandle, valueIndex)?.(value)} - aria-orientation={direction === 'ltr' || direction === 'rtl' ? 'horizontal' : 'vertical'} + {...divProps} {...restProps} /> ); diff --git a/src/Handles/index.tsx b/src/Handles/index.tsx index d3ee91d9b..c412c9c96 100644 --- a/src/Handles/index.tsx +++ b/src/Handles/index.tsx @@ -13,6 +13,12 @@ export interface HandlesProps { onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; handleRender?: HandleProps['render']; + /** + * When config `activeHandleRender`, + * it will render another hidden handle for active usage. + * This is useful for accessibility or tooltip usage. + */ + activeHandleRender?: HandleProps['render']; draggingIndex: number; onChangeComplete?: () => void; } @@ -29,7 +35,9 @@ const Handles = React.forwardRef((props, ref) => { onOffsetChange, values, handleRender, + activeHandleRender, draggingIndex, + onFocus, ...restProps } = props; const handlesRef = React.useRef>({}); @@ -40,6 +48,30 @@ const Handles = React.forwardRef((props, ref) => { }, })); + // =========================== Active =========================== + const [activeIndex, setActiveIndex] = React.useState(-1); + + const onHandleFocus = (e: React.FocusEvent, index: number) => { + setActiveIndex(index); + onFocus?.(e); + }; + + const onHandleMouseEnter = (e: React.MouseEvent, index: number) => { + setActiveIndex(index); + }; + + // =========================== Render =========================== + // Handle Props + const handleProps = { + prefixCls, + onStartMove, + onOffsetChange, + render: handleRender, + onFocus: onHandleFocus, + onMouseEnter: onHandleMouseEnter, + ...restProps, + }; + return ( <> {values.map((value, index) => ( @@ -52,17 +84,27 @@ const Handles = React.forwardRef((props, ref) => { } }} dragging={draggingIndex === index} - prefixCls={prefixCls} style={getIndex(style, index)} key={index} value={value} valueIndex={index} - onStartMove={onStartMove} - onOffsetChange={onOffsetChange} - render={handleRender} - {...restProps} + {...handleProps} /> ))} + + {activeHandleRender && ( + + )} ); }); diff --git a/src/Slider.tsx b/src/Slider.tsx index 44b169f82..4cdbe6fcc 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -93,6 +93,7 @@ export interface SliderProps { // Components handleRender?: HandlesProps['handleRender']; + activeHandleRender?: HandlesProps['handleRender']; // Accessibility tabIndex?: number | number[]; @@ -158,6 +159,7 @@ const Slider = React.forwardRef>((prop // Components handleRender, + activeHandleRender, // Accessibility tabIndex = 0, @@ -289,12 +291,13 @@ const Slider = React.forwardRef>((prop }; const finishChange = () => { - onAfterChange?.(getTriggerValue(rawValuesRef.current)); + const finishValue = getTriggerValue(rawValuesRef.current); + onAfterChange?.(finishValue); warning( !onAfterChange, '[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.', ); - onChangeComplete?.(getTriggerValue(rawValuesRef.current)); + onChangeComplete?.(finishValue); }; const [draggingIndex, draggingValue, cacheValues, onStartDrag] = useDrag( @@ -335,6 +338,7 @@ const Slider = React.forwardRef>((prop onBeforeChange?.(getTriggerValue(cloneNextValues)); triggerChange(cloneNextValues); if (e) { + handlesRef.current.focus(valueIndex); onStartDrag(e, valueIndex, cloneNextValues); } } @@ -543,6 +547,7 @@ const Slider = React.forwardRef>((prop onFocus={onFocus} onBlur={onBlur} handleRender={handleRender} + activeHandleRender={activeHandleRender} onChangeComplete={finishChange} /> diff --git a/src/hooks/useDrag.ts b/src/hooks/useDrag.ts index 3fbbe307e..49fb8fc80 100644 --- a/src/hooks/useDrag.ts +++ b/src/hooks/useDrag.ts @@ -1,3 +1,4 @@ +import { useEvent } from 'rc-util'; import * as React from 'react'; import type { Direction, OnStartMove } from '../interface'; import type { OffsetValues } from './useOffset'; @@ -18,7 +19,12 @@ function useDrag( triggerChange: (values: number[]) => void, finishChange: () => void, offsetValues: OffsetValues, -): [number, number, number[], OnStartMove] { +): [ + draggingIndex: number, + draggingValue: number, + returnValues: number[], + onStartMove: OnStartMove, +] { const [draggingValue, setDraggingValue] = React.useState(null); const [draggingIndex, setDraggingIndex] = React.useState(-1); const [cacheValues, setCacheValues] = React.useState(rawValues); @@ -27,7 +33,7 @@ function useDrag( const mouseMoveEventRef = React.useRef<(event: MouseEvent) => void>(null); const mouseUpEventRef = React.useRef<(event: MouseEvent) => void>(null); - React.useEffect(() => { + React.useLayoutEffect(() => { if (draggingIndex === -1) { setCacheValues(rawValues); } @@ -55,7 +61,7 @@ function useDrag( } }; - const updateCacheValue = (valueIndex: number, offsetPercent: number) => { + const updateCacheValue = useEvent((valueIndex: number, offsetPercent: number) => { // Basic point offset if (valueIndex === -1) { @@ -87,11 +93,7 @@ function useDrag( flushValues(next.values, next.value); } - }; - - // Resolve closure - const updateCacheValueRef = React.useRef(updateCacheValue); - updateCacheValueRef.current = updateCacheValue; + }); const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => { e.stopPropagation(); @@ -133,7 +135,7 @@ function useDrag( default: offSetPercent = offsetX / width; } - updateCacheValueRef.current(valueIndex, offSetPercent); + updateCacheValue(valueIndex, offSetPercent); }; // End diff --git a/tests/Range.test.js b/tests/Range.test.js index 5ef60ec30..638e22d93 100644 --- a/tests/Range.test.js +++ b/tests/Range.test.js @@ -511,12 +511,12 @@ describe('Range', () => { expect(handleFocus).toBeCalled(); }); - it('blur', () => { + it('blur()', () => { const handleBlur = jest.fn(); const { container } = render(); container.getElementsByClassName('rc-slider-handle')[0].focus(); container.getElementsByClassName('rc-slider-handle')[0].blur(); - expect(handleBlur).toBeCalled(); + expect(handleBlur).toHaveBeenCalled(); }); }); diff --git a/tests/Tooltip.test.js b/tests/Tooltip.test.js new file mode 100644 index 000000000..a36f5fc16 --- /dev/null +++ b/tests/Tooltip.test.js @@ -0,0 +1,28 @@ +import '@testing-library/jest-dom'; +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import Slider from '../src/Slider'; + +describe('Slider.Tooltip', () => { + it('internal activeHandleRender support', () => { + const { container } = render( + + React.cloneElement(node, { + 'data-test': 'bamboo', + 'data-value': info.value, + }) + } + />, + ); + expect(container.querySelector('.rc-slider-handle[data-test]')).toBeTruthy(); + + // Click second + fireEvent.mouseEnter(container.querySelectorAll('.rc-slider-handle')[1]); + expect( + container.querySelector('.rc-slider-handle[data-value]').getAttribute('data-value'), + ).toBe('50'); + }); +});