Add Slider component (based on video volume slider)
This commit is contained in:
parent
8a14dfefe7
commit
faf513c049
2 changed files with 125 additions and 0 deletions
|
@ -41,6 +41,7 @@ export { default as PhoneInput } from './phone-input/phone-input';
|
||||||
export { default as ProgressBar } from './progress-bar/progress-bar';
|
export { default as ProgressBar } from './progress-bar/progress-bar';
|
||||||
export { default as RadioButton } from './radio-button/radio-button';
|
export { default as RadioButton } from './radio-button/radio-button';
|
||||||
export { default as Select } from './select/select';
|
export { default as Select } from './select/select';
|
||||||
|
export { default as Slider } from './slider/slider';
|
||||||
export { default as Spinner } from './spinner/spinner';
|
export { default as Spinner } from './spinner/spinner';
|
||||||
export { default as Stack } from './stack/stack';
|
export { default as Stack } from './stack/stack';
|
||||||
export { default as Streamfield } from './streamfield/streamfield';
|
export { default as Streamfield } from './streamfield/streamfield';
|
||||||
|
|
124
app/soapbox/components/ui/slider/slider.tsx
Normal file
124
app/soapbox/components/ui/slider/slider.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
type Point = { x: number, y: number };
|
||||||
|
|
||||||
|
interface ISlider {
|
||||||
|
/** Value between 0 and 1. */
|
||||||
|
value: number
|
||||||
|
/** Callback when the value changes. */
|
||||||
|
onChange(value: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draggable slider component. */
|
||||||
|
const Slider: React.FC<ISlider> = ({ value, onChange }) => {
|
||||||
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMouseDown: React.MouseEventHandler = e => {
|
||||||
|
document.addEventListener('mousemove', handleMouseSlide, true);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp, true);
|
||||||
|
document.addEventListener('touchmove', handleMouseSlide, true);
|
||||||
|
document.addEventListener('touchend', handleMouseUp, true);
|
||||||
|
|
||||||
|
handleMouseSlide(e);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseSlide, true);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp, true);
|
||||||
|
document.removeEventListener('touchmove', handleMouseSlide, true);
|
||||||
|
document.removeEventListener('touchend', handleMouseUp, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseSlide = throttle(e => {
|
||||||
|
if (node.current) {
|
||||||
|
const { x } = getPointerPosition(node.current, e);
|
||||||
|
|
||||||
|
if (!isNaN(x)) {
|
||||||
|
let slideamt = x;
|
||||||
|
|
||||||
|
if (x > 1) {
|
||||||
|
slideamt = 1;
|
||||||
|
} else if (x < 0) {
|
||||||
|
slideamt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(slideamt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='inline-flex cursor-pointer h-6 relative transition'
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
ref={node}
|
||||||
|
>
|
||||||
|
<div className='w-full h-1 bg-white/30 absolute top-1/2 -translate-y-1/2 rounded-full' />
|
||||||
|
<div className='h-1 bg-accent-500 absolute top-1/2 -translate-y-1/2 rounded-full' style={{ width: `${value * 100}%` }} />
|
||||||
|
<span
|
||||||
|
className='bg-accent-500 absolute rounded-full w-3 h-3 -ml-1.5 top-1/2 -translate-y-1/2 z-10 shadow'
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ left: `${value * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findElementPosition = (el: HTMLElement) => {
|
||||||
|
let box;
|
||||||
|
|
||||||
|
if (el.getBoundingClientRect && el.parentNode) {
|
||||||
|
box = el.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!box) {
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const docEl = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
|
||||||
|
const scrollLeft = window.pageXOffset || body.scrollLeft;
|
||||||
|
const left = (box.left + scrollLeft) - clientLeft;
|
||||||
|
|
||||||
|
const clientTop = docEl.clientTop || body.clientTop || 0;
|
||||||
|
const scrollTop = window.pageYOffset || body.scrollTop;
|
||||||
|
const top = (box.top + scrollTop) - clientTop;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: Math.round(left),
|
||||||
|
top: Math.round(top),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => {
|
||||||
|
const box = findElementPosition(el);
|
||||||
|
const boxW = el.offsetWidth;
|
||||||
|
const boxH = el.offsetHeight;
|
||||||
|
const boxY = box.top;
|
||||||
|
const boxX = box.left;
|
||||||
|
|
||||||
|
let pageY = event.pageY;
|
||||||
|
let pageX = event.pageX;
|
||||||
|
|
||||||
|
if (event.changedTouches) {
|
||||||
|
pageX = event.changedTouches[0].pageX;
|
||||||
|
pageY = event.changedTouches[0].pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
y: Math.max(0, Math.min(1, (pageY - boxY) / boxH)),
|
||||||
|
x: Math.max(0, Math.min(1, (pageX - boxX) / boxW)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Slider;
|
Loading…
Reference in a new issue