Slider reimplemented with hooks and RAF

This commit is contained in:
NikolaBorislavovHristov 2019-10-02 23:58:17 +03:00
parent 7be61e9c9a
commit a1619dee93
2 changed files with 131 additions and 144 deletions

View file

@ -1,121 +1,133 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useFocusable } = require('stremio-router');
const useAnimationFrame = require('stremio/common/useAnimationFrame');
const useLiveRef = require('stremio/common/useLiveRef');
const styles = require('./styles');
class Slider extends React.Component {
constructor(props) {
super(props);
this.sliderContainerRef = React.createRef();
this.orientation = props.orientation;
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.value !== this.props.value ||
nextProps.minimumValue !== this.props.minimumValue ||
nextProps.maximumValue !== this.props.maximumValue ||
nextProps.className !== this.props.className;
}
componentWillUnmount() {
this.releaseThumb();
}
calculateSlidingValue = ({ mouseX, mouseY }) => {
const { x: sliderX, y: sliderY, width: sliderWidth, height: sliderHeight } = this.sliderContainerRef.current.getBoundingClientRect();
const sliderStart = this.orientation === 'horizontal' ? sliderX : sliderY;
const sliderLength = this.orientation === 'horizontal' ? sliderWidth : sliderHeight;
const mouseStart = this.orientation === 'horizontal' ? mouseX : mouseY;
const thumbStart = Math.min(Math.max(mouseStart - sliderStart, 0), sliderLength);
const slidingValueCoef = this.orientation === 'horizontal' ? thumbStart / sliderLength : (sliderLength - thumbStart) / sliderLength;
const slidingValue = slidingValueCoef * (this.props.maximumValue - this.props.minimumValue) + this.props.minimumValue;
return slidingValue;
}
releaseThumb = () => {
window.removeEventListener('blur', this.onBlur);
window.removeEventListener('mouseup', this.onMouseUp);
window.removeEventListener('mousemove', this.onMouseMove);
document.documentElement.style.cursor = 'initial';
document.body.style['pointer-events'] = 'initial';
}
onBlur = () => {
this.releaseThumb();
if (typeof this.props.onCancel === 'function') {
this.props.onCancel();
const Slider = ({ className, value, minimumValue, maximumValue, onSlide, onComplete }) => {
minimumValue = minimumValue !== null && !isNaN(minimumValue) && isFinite(minimumValue) ? minimumValue : 0;
maximumValue = maximumValue !== null && !isNaN(maximumValue) && isFinite(maximumValue) ? maximumValue : 100;
value = value !== null && !isNaN(value) && value >= minimumValue && value <= maximumValue ? value : 0;
const onSlideRef = useLiveRef(onSlide, [onSlide]);
const onCompleteRef = useLiveRef(onComplete, [onComplete]);
const sliderContainerRef = React.useRef(null);
const [active, setActive] = React.useState(false);
const focusable = useFocusable();
const [requestAnimation, cancelAnimation] = useAnimationFrame();
const calculateValueForMouseX = React.useCallback((mouseX) => {
if (sliderContainerRef.current === null) {
return 0;
}
}
onMouseUp = ({ clientX: mouseX, clientY: mouseY }) => {
this.releaseThumb();
const slidingValue = this.calculateSlidingValue({ mouseX, mouseY });
if (typeof this.props.onComplete === 'function') {
this.props.onComplete(slidingValue);
const minimumValue = parseInt(sliderContainerRef.current.getAttribute('aria-valuemin'));
const maximumValue = parseInt(sliderContainerRef.current.getAttribute('aria-valuemax'));
const { x: sliderX, width: sliderWidth } = sliderContainerRef.current.getBoundingClientRect();
const thumbStart = Math.min(Math.max(mouseX - sliderX, 0), sliderWidth);
const value = (thumbStart / sliderWidth) * (maximumValue - minimumValue) + minimumValue;
return value;
}, []);
const onBlur = React.useCallback(() => {
const value = parseInt(sliderContainerRef.current.getAttribute('aria-valuenow'));
if (typeof onSlideRef.current === 'function') {
onSlideRef.current(value);
}
}
onMouseMove = ({ clientX: mouseX, clientY: mouseY }) => {
const slidingValue = this.calculateSlidingValue({ mouseX, mouseY });
if (typeof this.props.onSlide === 'function') {
this.props.onSlide(slidingValue);
if (typeof onCompleteRef.current === 'function') {
onCompleteRef.current(value);
}
}
onStartSliding = ({ clientX: mouseX, clientY: mouseY, button }) => {
if (button !== 0) {
setActive(false);
}, []);
const onMouseUp = React.useCallback((event) => {
const value = calculateValueForMouseX(event.clientX);
if (typeof onCompleteRef.current === 'function') {
onCompleteRef.current(value);
}
setActive(false);
}, []);
const onMouseMove = React.useCallback((event) => {
requestAnimation(() => {
const value = calculateValueForMouseX(event.clientX);
if (typeof onSlideRef.current === 'function') {
onSlideRef.current(value);
}
});
}, []);
const onMouseDown = React.useCallback((event) => {
if (event.button !== 0) {
return;
}
window.addEventListener('blur', this.onBlur);
window.addEventListener('mouseup', this.onMouseUp);
window.addEventListener('mousemove', this.onMouseMove);
document.documentElement.style.cursor = 'pointer';
document.body.style['pointer-events'] = 'none';
this.onMouseMove({ clientX: mouseX, clientY: mouseY });
}
const value = calculateValueForMouseX(event.clientX);
if (typeof onSlideRef.current === 'function') {
onSlideRef.current(value);
}
render() {
const thumbStartProp = this.orientation === 'horizontal' ? 'left' : 'bottom';
const trackBeforeSizeProp = this.orientation === 'horizontal' ? 'width' : 'height';
const thumbStart = Math.max(0, Math.min(1, (this.props.value - this.props.minimumValue) / (this.props.maximumValue - this.props.minimumValue)));
const disabled = this.props.value === null || isNaN(this.props.value) ||
this.props.minimumValue === null || isNaN(this.props.minimumValue) ||
this.props.maximumValue === null || isNaN(this.props.maximumValue) ||
this.props.minimumValue === this.props.maximumValue;
return (
<div ref={this.sliderContainerRef} className={classnames(styles['slider-container'], styles[this.orientation], { 'disabled': disabled }, this.props.className)} onMouseDown={this.onStartSliding}>
setActive(true);
}, []);
const retainThumb = React.useCallback(() => {
window.addEventListener('blur', onBlur);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('mousemove', onMouseMove);
document.documentElement.className = classnames(document.documentElement.className, styles['active-slider-within']);
}, []);
const releaseThumb = React.useCallback(() => {
cancelAnimation();
window.removeEventListener('blur', onBlur);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('mousemove', onMouseMove);
const classList = document.documentElement.className.split(' ');
const classIndex = classList.indexOf(styles['active-slider-within']);
if (classIndex !== -1) {
classList.splice(classIndex, 1);
}
document.documentElement.className = classnames(classList);
}, []);
React.useEffect(() => {
if (active) {
retainThumb();
} else {
releaseThumb();
}
}, [active]);
React.useEffect(() => {
if (!focusable) {
setActive(false);
}
}, [focusable]);
React.useEffect(() => {
return () => {
releaseThumb();
};
}, []);
const thumbPosition = React.useMemo(() => {
return Math.max(0, Math.min(1, (value - minimumValue) / (maximumValue - minimumValue)));
}, [value, minimumValue, maximumValue]);
return (
<div ref={sliderContainerRef} className={classnames(className, styles['slider-container'], { 'active': active })} aria-valuenow={value} aria-valuemin={minimumValue} aria-valuemax={maximumValue} onMouseDown={onMouseDown}>
<div className={styles['layer']}>
<div className={styles['track']} />
{
!disabled ?
<React.Fragment>
<div className={styles['track-before']} style={{ [trackBeforeSizeProp]: `calc(100% * ${thumbStart})` }} />
<div className={styles['thumb']} style={{ [thumbStartProp]: `calc(100% * ${thumbStart})` }} />
</React.Fragment>
:
null
}
</div>
);
}
}
<div className={styles['layer']}>
<div className={styles['track-before']} style={{ width: `calc(100% * ${thumbPosition})` }} />
</div>
<div className={styles['layer']}>
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
</div>
</div>
);
};
Slider.propTypes = {
className: PropTypes.string,
value: PropTypes.number,
minimumValue: PropTypes.number,
maximumValue: PropTypes.number,
orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
onSlide: PropTypes.func,
onComplete: PropTypes.func,
onCancel: PropTypes.func
};
Slider.defaultProps = {
value: 0,
minimumValue: 0,
maximumValue: 100,
orientation: 'horizontal'
onComplete: PropTypes.func
};
module.exports = Slider;

View file

@ -1,71 +1,46 @@
html.active-slider-within {
cursor: grabbing;
body {
pointer-events: none;
}
}
.slider-container {
display: inline-block;
position: relative;
z-index: 0;
overflow: visible;
cursor: pointer;
&.horizontal {
.track {
top: calc(50% - var(--track-size) * 0.5);
right: 0;
left: 0;
height: var(--track-size);
}
.track-before {
top: calc(50% - var(--track-size) * 0.5);
left: 0;
height: var(--track-size);
}
.thumb {
top: calc(50% - var(--thumb-size) * 0.5);
transform: translateX(-50%);
}
}
&.vertical {
.track {
top: 0;
bottom: 0;
left: calc(50% - var(--track-size) * 0.1);
width: var(--track-size);
}
.track-before {
bottom: 0;
left: calc(50% - var(--track-size) * 0.1);
width: var(--track-size);
}
.thumb {
left: calc(50% - var(--thumb-size) * 0.5);
transform: translateY(50%);
}
}
&:global(.disabled) {
pointer-events: none;
.layer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.track {
position: absolute;
z-index: 1;
flex: 1;
height: var(--track-size);
background-color: var(--track-color);
}
.track-before {
position: absolute;
z-index: 2;
flex: none;
height: var(--track-size);
background-color: var(--track-before-color);
}
.thumb {
position: absolute;
z-index: 3;
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;
transform: translateX(-50%);
background-color: var(--thumb-color);
}
}