Skip to content

Commit

Permalink
feat: add activeHandleRender (#981)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zombieJ authored Apr 19, 2024
1 parent ac66c54 commit a93831d
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 57 deletions.
52 changes: 34 additions & 18 deletions docs/examples/multiple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,38 @@ function log(value) {
console.log(value);
}

export default () => (
<div>
<div style={style}>
<Slider
range
defaultValue={[0, 10, 30]}
onChange={log}
styles={{
tracks: {
background: `linear-gradient(to right, blue, red)`,
},
track: {
background: 'transparent',
},
}}
/>
const NodeWrapper = ({ children }: { children: React.ReactElement }) => {
return <div>{React.cloneElement(children, {}, <div>TOOLTIP</div>)}</div>;
};

export default () => {
const [value, setValue] = React.useState([0, 5, 8]);

return (
<div>
<div style={style}>
<Slider
range
// defaultValue={[0, 10, 30]}
// onChange={log}
min={0}
max={10}
value={value}
onChange={(nextValue) => {
// console.log('>>>', nextValue);
setValue(nextValue as any);
}}
activeHandleRender={(node) => <NodeWrapper>{node}</NodeWrapper>}
styles={{
tracks: {
background: `linear-gradient(to right, blue, red)`,
},
track: {
background: 'transparent',
},
}}
/>
</div>
</div>
</div>
);
);
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rc-slider",
"version": "10.5.0",
"version": "10.6.0-4",
"description": "Slider UI component for React",
"keywords": [
"react",
Expand Down Expand Up @@ -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"
Expand Down
63 changes: 44 additions & 19 deletions src/Handles/Handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@ interface RenderProps {
dragging: boolean;
}

export interface HandleProps {
export interface HandleProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onFocus' | 'onMouseEnter'> {
prefixCls: string;
style?: React.CSSProperties;
value: number;
valueIndex: number;
dragging: boolean;
onStartMove: OnStartMove;
onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void;
render?: (origin: React.ReactElement<HandleProps>, props: RenderProps) => React.ReactElement;
onFocus: (e: React.FocusEvent<HTMLDivElement>, index: number) => void;
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>, index: number) => void;
render?: (
origin: React.ReactElement<React.HTMLAttributes<HTMLDivElement>>,
props: RenderProps,
) => React.ReactElement;
onChangeComplete?: () => void;
mock?: boolean;
}

const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
Expand All @@ -37,6 +42,8 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
dragging,
onOffsetChange,
onChangeComplete,
onFocus,
onMouseEnter,
...restProps
} = props;
const {
Expand All @@ -63,6 +70,14 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
}
};

const onInternalFocus = (e: React.FocusEvent<HTMLDivElement>) => {
onFocus?.(e, valueIndex);
};

const onInternalMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
onMouseEnter(e, valueIndex);
};

// =========================== Keyboard ===========================
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
if (!disabled && keyboard) {
Expand Down Expand Up @@ -131,13 +146,36 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
const positionStyle = getDirectionStyle(direction, value, min, max);

// ============================ Render ============================
let divProps: React.HtmlHTMLAttributes<HTMLDivElement> = {};

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 = (
<div
ref={ref}
className={cls(
handlePrefixCls,
{
[`${handlePrefixCls}-${valueIndex + 1}`]: range,
[`${handlePrefixCls}-${valueIndex + 1}`]: valueIndex !== null && range,
[`${handlePrefixCls}-dragging`]: dragging,
},
classNames.handle,
Expand All @@ -147,20 +185,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((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}
/>
);
Expand Down
52 changes: 47 additions & 5 deletions src/Handles/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export interface HandlesProps {
onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLDivElement>) => 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;
}
Expand All @@ -29,7 +35,9 @@ const Handles = React.forwardRef<HandlesRef, HandlesProps>((props, ref) => {
onOffsetChange,
values,
handleRender,
activeHandleRender,
draggingIndex,
onFocus,
...restProps
} = props;
const handlesRef = React.useRef<Record<number, HTMLDivElement>>({});
Expand All @@ -40,6 +48,30 @@ const Handles = React.forwardRef<HandlesRef, HandlesProps>((props, ref) => {
},
}));

// =========================== Active ===========================
const [activeIndex, setActiveIndex] = React.useState<number>(-1);

const onHandleFocus = (e: React.FocusEvent<HTMLDivElement>, index: number) => {
setActiveIndex(index);
onFocus?.(e);
};

const onHandleMouseEnter = (e: React.MouseEvent<HTMLDivElement>, index: number) => {
setActiveIndex(index);
};

// =========================== Render ===========================
// Handle Props
const handleProps = {
prefixCls,
onStartMove,
onOffsetChange,
render: handleRender,
onFocus: onHandleFocus,
onMouseEnter: onHandleMouseEnter,
...restProps,
};

return (
<>
{values.map<React.ReactNode>((value, index) => (
Expand All @@ -52,17 +84,27 @@ const Handles = React.forwardRef<HandlesRef, HandlesProps>((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 && (
<Handle
key="a11y"
{...handleProps}
value={values[activeIndex]}
valueIndex={null}
dragging={draggingIndex !== -1}
render={activeHandleRender}
style={{ pointerEvents: 'none' }}
tabIndex={null}
aria-hidden
/>
)}
</>
);
});
Expand Down
9 changes: 7 additions & 2 deletions src/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface SliderProps<ValueType = number | number[]> {

// Components
handleRender?: HandlesProps['handleRender'];
activeHandleRender?: HandlesProps['handleRender'];

// Accessibility
tabIndex?: number | number[];
Expand Down Expand Up @@ -158,6 +159,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop

// Components
handleRender,
activeHandleRender,

// Accessibility
tabIndex = 0,
Expand Down Expand Up @@ -289,12 +291,13 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((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(
Expand Down Expand Up @@ -335,6 +338,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
onBeforeChange?.(getTriggerValue(cloneNextValues));
triggerChange(cloneNextValues);
if (e) {
handlesRef.current.focus(valueIndex);
onStartDrag(e, valueIndex, cloneNextValues);
}
}
Expand Down Expand Up @@ -543,6 +547,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
onFocus={onFocus}
onBlur={onBlur}
handleRender={handleRender}
activeHandleRender={activeHandleRender}
onChangeComplete={finishChange}
/>

Expand Down
20 changes: 11 additions & 9 deletions src/hooks/useDrag.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -133,7 +135,7 @@ function useDrag(
default:
offSetPercent = offsetX / width;
}
updateCacheValueRef.current(valueIndex, offSetPercent);
updateCacheValue(valueIndex, offSetPercent);
};

// End
Expand Down
4 changes: 2 additions & 2 deletions tests/Range.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,12 +511,12 @@ describe('Range', () => {
expect(handleFocus).toBeCalled();
});

it('blur', () => {
it('blur()', () => {
const handleBlur = jest.fn();
const { container } = render(<Slider range min={0} max={20} onBlur={handleBlur} />);
container.getElementsByClassName('rc-slider-handle')[0].focus();
container.getElementsByClassName('rc-slider-handle')[0].blur();
expect(handleBlur).toBeCalled();
expect(handleBlur).toHaveBeenCalled();
});
});

Expand Down
Loading

0 comments on commit a93831d

Please sign in to comment.