Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slider improvements #211

Merged
merged 41 commits into from
Nov 4, 2020
Merged
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
90b8cb3
Slider improvements
jjenzz Oct 14, 2020
edfdc63
Compose onTouchStart event from props
jjenzz Oct 20, 2020
bfbfa60
Clean up some redundancy
jjenzz Oct 20, 2020
d1018cc
Avoid forwarding `focused` to span in story
jjenzz Oct 20, 2020
8bd5248
Changeset
jjenzz Oct 20, 2020
c78d0e5
Correct comment spelling
jjenzz Oct 20, 2020
8be10e7
Remove redundant thumbs from stories
jjenzz Oct 20, 2020
8fe14ab
Rename `isDisabled` to `disabled` & remove from context
jjenzz Oct 20, 2020
065dae8
Separate unrelated code blocks
jjenzz Oct 20, 2020
2e020e6
Avoid getting rect on every mouse move
jjenzz Oct 20, 2020
ae2b764
Rename `touches` to `touch`
jjenzz Oct 20, 2020
32087b2
Remove redundant ref composition
jjenzz Oct 20, 2020
18e6416
Cleanup timeout
jjenzz Oct 20, 2020
083eb84
Add thumb offset comment
jjenzz Oct 20, 2020
c41475d
Clean up events on unmount
jjenzz Oct 20, 2020
212921b
Remove inherited border-radius from range
jjenzz Oct 20, 2020
c2df807
Add comments to utils and move utils inline that aren't re-used
jjenzz Oct 21, 2020
b7a38e8
Add direction prop
jjenzz Oct 21, 2020
f543395
Add missing disabled attributes
jjenzz Oct 21, 2020
a828d1a
Remove `useChangeEffect`
jjenzz Oct 21, 2020
7c36320
Add thumb positioning span and ensure vertical always increases from …
jjenzz Oct 21, 2020
ef4cc8c
Make sure `onChange` calls only when value changes
jjenzz Oct 21, 2020
06a2296
Add pointer events
jjenzz Oct 22, 2020
45e2753
Convert pointer position to value within orientation components
jjenzz Oct 22, 2020
b9a430d
Move keydown into SliderPart so orientation comps can handle slide di…
jjenzz Oct 22, 2020
427b6dc
Add pointer events
jjenzz Oct 22, 2020
b09d304
Update stories
jjenzz Oct 22, 2020
522c3c9
Add comment to useControlledState hook
jjenzz Oct 22, 2020
433304f
Rename thumb offset variable
jjenzz Oct 22, 2020
28ef1a6
Changeset
jjenzz Oct 22, 2020
83e0bc4
Revert useControlledState changes
jjenzz Oct 22, 2020
2d3e518
Update hook to include itemediary direction changes
jjenzz Oct 27, 2020
8b3ccde
PR feedback
jjenzz Oct 27, 2020
7e3b0ce
Cache slider rect
jjenzz Oct 27, 2020
b0033a9
Fix useControlledState
jjenzz Oct 27, 2020
ced4224
Changeset
jjenzz Oct 27, 2020
3a661d9
Add `React.useCallback`
jjenzz Oct 28, 2020
54ec030
Remove redundant `useRect` import
jjenzz Oct 29, 2020
61bc959
Make sure thumb focuses on value change
jjenzz Nov 3, 2020
29a6df6
Focus thumb in event handlers
jjenzz Nov 3, 2020
1b04b84
Update comment
jjenzz Nov 3, 2020
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
88 changes: 53 additions & 35 deletions packages/react/slider/src/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type SliderContextValue = {
max: number;
values: number[];
valueIndexToChangeRef: React.MutableRefObject<number>;
thumbs: Set<React.ElementRef<typeof SliderThumb>>;
orientation: SliderOwnProps['orientation'];
};

Expand Down Expand Up @@ -91,6 +92,7 @@ const Slider = forwardRef<typeof SLIDER_DEFAULT_TAG, SliderProps, SliderStaticPr
const sliderProps = omit(restProps, ['defaultValue', 'value']) as SliderDOMProps;
const sliderRef = React.useRef<HTMLSpanElement>(null);
const composedRefs = useComposedRefs(forwardedRef, sliderRef);
const thumbRefs = React.useRef<SliderContextValue['thumbs']>(new Set());
const valueIndexToChangeRef = React.useRef<number>(0);
const isHorizontal = orientation === 'horizontal';
const SliderOrientation = isHorizontal ? SliderHorizontal : SliderVertical;
Expand All @@ -111,25 +113,45 @@ const Slider = forwardRef<typeof SLIDER_DEFAULT_TAG, SliderProps, SliderStaticPr

function handleSlideStart(value: number) {
const closestIndex = getClosestValueIndex(values, value);
updateValues(value, closestIndex);
updateValues(value, closestIndex).then((valueIndexToChange) => {
/**
* Browsers fire event handlers before executing their event implementation
* so they can check if `preventDefault` was called first. Therefore,
* if we focus the thumb on slide start (`mousedown`), the browser will execute
* their `mousedown` implementation after our focus which will instantly
* `blur` the thumb again (because it effectively clicks off the thumb).
*
* We use a `setTimeout` to move the focus to the next tick (after the
* mousedown) to ensure focus on mousedown.
*/
window.setTimeout(() => focusThumb(valueIndexToChange), 0);
});
}

function handleSlideMove(value: number) {
updateValues(value, valueIndexToChangeRef.current);
updateValues(value, valueIndexToChangeRef.current).then(focusThumb);
}

function updateValues(value: number, atIndex: number) {
function updateValues(value: number, atIndex: number): Promise<number> {
const snapToStep = Math.round((value - min) / step) * step + min;
const nextValue = clamp(snapToStep, [min, max]);

setValues((prevValues = []) => {
const prevValue = prevValues[atIndex];
const nextValues = getNextSortedValues(prevValues, nextValue, atIndex);
valueIndexToChangeRef.current = nextValues.indexOf(nextValue);
return nextValues[atIndex] !== prevValue ? nextValues : prevValues;
return new Promise((resolve) => {
setValues((prevValues = []) => {
const prevValue = prevValues[atIndex];
const nextValues = getNextSortedValues(prevValues, nextValue, atIndex);
valueIndexToChangeRef.current = nextValues.indexOf(nextValue);
resolve(valueIndexToChangeRef.current);
return nextValues[atIndex] !== prevValue ? nextValues : prevValues;
});
});
}

function focusThumb(index: number) {
const thumbs = [...thumbRefs.current];
thumbs[index]?.focus();
}

return (
<SliderOrientation
{...sliderProps}
Expand Down Expand Up @@ -175,6 +197,7 @@ const Slider = forwardRef<typeof SLIDER_DEFAULT_TAG, SliderProps, SliderStaticPr
min,
max,
valueIndexToChangeRef,
thumbs: thumbRefs.current,
values,
orientation,
}),
Expand Down Expand Up @@ -429,9 +452,18 @@ const SliderPart = forwardRef<typeof SLIDER_DEFAULT_TAG, SliderPartProps>(functi
document.addEventListener('mousemove', handleSlideMouseMove);
document.addEventListener('mouseup', removeMouseEventListeners);
}
// We purpoesfully avoid calling `event.preventDefault` here as it will
// also prevent PointerEvents which we need.
})}
onTouchStart={composeEventHandlers(props.onTouchStart, (event) => {
if (!isThumb(event.target)) onSlideTouchStart(event);
if (isThumb(event.target)) {
// Touch devices have a delay before focusing and won't focus if mouse
jjenzz marked this conversation as resolved.
Show resolved Hide resolved
// immediatedly moves away from target. We want thumb to focus regardless.
event.target.focus();
} else {
onSlideTouchStart(event);
}

document.addEventListener('touchmove', handleSlideTouchMove);
document.addEventListener('touchend', removeTouchEventListeners);
// Prevent scrolling for touch events
Expand All @@ -452,6 +484,9 @@ const SliderPart = forwardRef<typeof SLIDER_DEFAULT_TAG, SliderPartProps>(functi
* Prevent pointer events on other elements on the page while sliding.
* For example, stops hover states from triggering on buttons if
* mouse moves over a button during slide.
*
* Also ensures that slider receives all pointer events after mouse down
* even when mouse moves outside the document.
*/
onPointerDown={composeEventHandlers(props.onPointerDown, (event) => {
event.currentTarget.setPointerCapture(event.pointerId);
Expand Down Expand Up @@ -557,8 +592,6 @@ const SliderThumbImpl = forwardRef<typeof THUMB_DEFAULT_TAG, SliderThumbImplProp
const orientation = React.useContext(SliderOrientationContext);
const thumbRef = React.useRef<HTMLSpanElement>(null);
const ref = useComposedRefs(forwardedRef, thumbRef);
const focusTimerRef = React.useRef<number>(0);
const prevValuesRef = React.useRef(context.values);
const size = useSize(thumbRef);
const percent = convertValueToPercentage(value, context.min, context.max);
const label = getLabel(index, context.values.length);
Expand All @@ -568,29 +601,14 @@ const SliderThumbImpl = forwardRef<typeof THUMB_DEFAULT_TAG, SliderThumbImplProp
: 0;

React.useEffect(() => {
/**
* Browsers fire event handlers before executing their event implementation
* so they can check if `preventDefault` was called first. Therefore,
* if we focus the thumb during `mousedown`, the browser will execute
* their `mousedown` implementation after our focus which will instantly
* `blur` the thumb again (because it effectively clicks off the thumb).
*
* We use a `setTimeout` here to move the focus to the next tick (after the
* mousedown) to ensure focus on mousedown.
*/
focusTimerRef.current = window.setTimeout(() => {
const thumb = thumbRef.current;
const hasValuesChanged = prevValuesRef.current !== context.values;
const isActive = context.valueIndexToChangeRef.current === index;
const isFocused = document.activeElement === thumb;

if (thumb && hasValuesChanged && isActive && !isFocused) {
thumb.focus();
prevValuesRef.current = context.values;
}
}, 0);
return () => window.clearTimeout(focusTimerRef.current);
}, [context.values, context.valueIndexToChangeRef, index]);
const thumb = thumbRef.current;
if (thumb) {
context.thumbs.add(thumb);
return () => {
context.thumbs.delete(thumb);
};
}
}, [context.thumbs]);

return (
<span
Expand Down Expand Up @@ -644,7 +662,7 @@ const [styles, interopDataAttrObj] = createStyleObj(SLIDER_NAME, {
display: 'inline-flex',
flexShrink: 0,
userSelect: 'none',
touchAction: 'none', // Prevent parent/window scroll when sliding on touch devices
touchAction: 'none', // Disable browser handling of all panning and zooming gestures on touch devices
},
track: {
...cssReset(TRACK_DEFAULT_TAG),
Expand Down