import React, { useRef, useState, useEffect } from 'react'; import { StyleProp, StyleSheet, View, ViewStyle, LayoutChangeEvent, GestureResponderEvent, PanResponder, ImageBackground, } from 'react-native'; import useAsyncReference from '../../hooks/useAsyncReference'; import type { Theme, $RemoveChildren } from '../../types'; import { withTheme } from '../../core/theming'; import { buildBorderStyles } from '../../styles/styles'; import { Border } from '../../styles/styleElements'; import { clamp, roundValueToStep, findClosest } from '../../utils'; import { Panel, Text, Label } from '../..'; function percentToValue(percent: number, min: number, max: number) { return (max - min) * percent + min; } type Mark = { value: number; label?: string }; type Props = $RemoveChildren & { disabled?: boolean; marks?: Mark[] | boolean; max?: number; min?: number; onChange?: (a: number) => void; onChangeCommitted?: (a: number) => void; step?: number | null; style?: StyleProp; theme: Theme; value?: number; showToolTip?: boolean; }; const Slider = ({ disabled = false, marks: marksProp, max = 100, min = 0, onChange, onChangeCommitted, step = 1, style, theme, value = 0, showToolTip = true, ...rest }: Props) => { const [isUsed, setIsUsed] = useState(false); let marks: Mark[]; // if marks is set to true, we only show ticks at specified steps if (marksProp === true && step !== null) { marks = [...Array(Math.round((max - min) / step) + 1)].map((_, index) => ({ value: min + step * index, })); // otherwise we display ticks and optional labels specified by the developer } else if (Array.isArray(marksProp)) { marks = marksProp; // otherwise don't display any marks or labels } else { marks = []; } const [sliderSize, setSliderSize] = useAsyncReference(0); const handleLayout = (e: LayoutChangeEvent) => { setSliderSize(e.nativeEvent.layout.width); }; const getNewValue = (e: GestureResponderEvent) => { const percent = clamp(e.nativeEvent.locationX / sliderSize.current, 0, 1); let newValue = percentToValue(percent, min, max); if (step) { newValue = roundValueToStep(newValue, step, min); } else { const marksValues = marks.map(mark => mark.value); const closestIndex = findClosest(marksValues, newValue); newValue = marksValues[closestIndex]; } newValue = clamp(newValue, min, max); return newValue; }; // TODO: is this a good approach? const valueRef = useRef(value); useEffect(() => { valueRef.current = value; }, [value]); const handleChange = (newValue: number) => { if (valueRef.current !== newValue) { onChange?.(newValue); } }; const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onStartShouldSetPanResponderCapture: () => true, onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderStart: e => { setIsUsed(true); const newValue = getNewValue(e); handleChange(newValue); }, onPanResponderMove: e => { const newValue = getNewValue(e); handleChange(newValue); }, onPanResponderEnd: e => { setIsUsed(false); const newValue = getNewValue(e); onChangeCommitted?.(newValue); }, onPanResponderTerminationRequest: () => true, }), ).current; const borderStyles = buildBorderStyles(theme); return ( {/* THUMB */} {disabled && ( )} {showToolTip && isUsed && ( )} {marks && ( {marks.map((m: Mark, index: number) => ( {m.label ? ( {m.label} ) : null} ))} )} ); }; const thumbWidth = 18; const thumbHeight = 32; const tickSize = 6; const styles = StyleSheet.create({ wrapper: { paddingVertical: 4, }, inner: { position: 'relative', paddingHorizontal: 16, }, track: { paddingVertical: thumbHeight / 2, }, trackInner: { height: 8, width: '100%', alignItems: 'center', justifyContent: 'center', }, thumb: { position: 'absolute', width: thumbWidth, height: thumbHeight, transform: [ { translateX: -thumbWidth / 2, }, ], }, tooltipWrapper: { position: 'absolute', top: -44, left: thumbWidth / 2, alignItems: 'center', }, tooltip: { position: 'absolute', alignItems: 'center', justifyContent: 'center', }, marksWrapper: { position: 'relative', }, mark: { transform: [{ translateX: -1 }], }, tick: { height: tickSize, marginBottom: 2, width: 2, }, markText: { fontSize: 14, // TODO: is there a way to translate by 0.5 of a character width? // tick should centered above transform: [{ translateX: -2 }], }, }); export default withTheme(Slider);