mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-19 13:52:12 +00:00
Slider reimplemented with hooks and RAF
This commit is contained in:
parent
7be61e9c9a
commit
a1619dee93
2 changed files with 131 additions and 144 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue