import React from 'react'; import { StyleSheet, Text, TouchableOpacity, Image, View, TextStyle, ViewStyle, ImageStyle, ImageSourcePropType, ColorValue, } from 'react-native'; type HasReachedMaxMin = { hasReachedMax: boolean; hasReachedMin: boolean; }; export type SimpleStepperProps = { initialValue?: number; minimumValue?: number; maximumValue?: number; stepValue?: number; valueChanged?: (value: number) => void; decrementImage?: ImageSourcePropType; incrementImage?: ImageSourcePropType; activeOpacity?: number; disabledOpacity?: number; disabled?: boolean; renderDecrementStep?: (value: number, onDecrement: () => void) => JSX.Element; renderIncrementStep?: (value: number, onIncrement: () => void) => JSX.Element; renderIncrementImage?: (opacity: number) => JSX.Element; renderDecrementImage?: (opacity: number) => JSX.Element; wraps?: boolean; onMin?: (value: number) => void; onMax?: (value: number) => void; onIncrement?: (value: number) => void; onDecrement?: (value: number) => void; showText?: boolean; renderText?: (value: number) => JSX.Element; textStyle?: TextStyle; containerStyle?: ViewStyle; separatorStyle?: ViewStyle; incrementStepStyle?: ViewStyle; decrementStepStyle?: ViewStyle; incrementImageStyle?: ImageStyle; decrementImageStyle?: ImageStyle; textPosition?: 'left' | 'center' | 'right'; disableIncrementImageTintColor?: boolean; disableDecrementImageTintColor?: boolean; useColor?: boolean; color?: ColorValue; textDecimalPlaces?: number; containerTestID?: string; separatorTestID?: string; incrementImageTestID?: string; decrementImageTestID?: string; incrementButtonTestID?: string; decrementButtonTestID?: string; }; const SimpleStepper: React.FunctionComponent<SimpleStepperProps> = ({ initialValue = 0, minimumValue = 0, maximumValue = 10, stepValue = 1, valueChanged = () => {}, decrementImage = require('./assets/decrement.png'), incrementImage = require('./assets/increment.png'), activeOpacity = 0.4, disabledOpacity = 0.5, disabled = false, renderDecrementStep = undefined, renderIncrementStep = undefined, renderIncrementImage = undefined, renderDecrementImage = undefined, wraps = false, onMin = () => {}, onMax = () => {}, onIncrement = () => {}, onDecrement = () => {}, showText = false, renderText = undefined, textStyle = { marginHorizontal: 8, fontSize: 24, }, containerStyle = { flexDirection: 'row', borderWidth: 1, borderRadius: 8, alignItems: 'center', justifyContent: 'space-evenly', }, separatorStyle = { width: StyleSheet.hairlineWidth, backgroundColor: 'black', height: '100%', }, incrementStepStyle = { padding: 4, }, decrementStepStyle = { padding: 4, }, incrementImageStyle = { height: 30, width: 30, }, decrementImageStyle = { height: 30, width: 30, }, textPosition = 'center', disableIncrementImageTintColor = false, disableDecrementImageTintColor = false, useColor = false, color = 'blue', textDecimalPlaces = 2, containerTestID = 'container', separatorTestID = 'separator', incrementImageTestID = 'incrementImage', decrementImageTestID = 'decrementImage', incrementButtonTestID = 'incrementButton', decrementButtonTestID = 'decrementButton', }) => { const [value, setValue] = React.useState(initialValue); function _decrementAction(): void { const nextValue = value - stepValue; const actualValue = _processValue(nextValue); onDecrement(actualValue); setValue(actualValue); valueChanged(actualValue); } function _incrementAction(): void { const nextValue = value + stepValue; const actualValue = _processValue(nextValue); onIncrement(actualValue); setValue(actualValue); valueChanged(actualValue); } function _processValue(actualValue: number): number { if (actualValue > maximumValue) { return wraps ? minimumValue : maximumValue; } else if (actualValue === maximumValue) { return maximumValue; } else if (actualValue < minimumValue) { return wraps ? maximumValue : minimumValue; } else if (actualValue === minimumValue) { return minimumValue; } return actualValue; } function _getHasMinMax(): HasReachedMaxMin { let hasReachedMax = true; let hasReachedMin = true; switch (true) { case wraps: hasReachedMin = false; hasReachedMax = false; break; case stepValue >= 0: hasReachedMax = value >= maximumValue; hasReachedMin = value <= minimumValue; break; case stepValue < 0: hasReachedMax = value <= minimumValue; hasReachedMin = value >= maximumValue; break; } return { hasReachedMax, hasReachedMin, }; } function _renderText(): React.JSX.Element { if (renderText) { return renderText(value); } let _textStyle = textStyle; if (useColor && color) { _textStyle.color = color; } let displayValue = value; if (!Number.isInteger(value)) { displayValue = Number(value.toFixed(textDecimalPlaces)); } return <Text style={textStyle}>{displayValue}</Text>; } function _renderIncrementImage(opacity: number): React.JSX.Element { if (renderIncrementImage) { return renderIncrementImage(opacity); } const _incrementImageStyle = incrementImageStyle; if (useColor && color && !disableIncrementImageTintColor) { _incrementImageStyle.tintColor = color; } if (disableIncrementImageTintColor && _incrementImageStyle.tintColor) { _incrementImageStyle.tintColor = undefined; } return ( <Image testID={incrementImageTestID} style={[_incrementImageStyle, {opacity}]} source={incrementImage} /> ); } function _renderDecrementImage(opacity: number): React.JSX.Element { if (renderDecrementImage) { return renderDecrementImage(opacity); } const _decrementImageStyle = decrementImageStyle; if (useColor && color && !disableDecrementImageTintColor) { _decrementImageStyle.tintColor = color; } if (disableDecrementImageTintColor && _decrementImageStyle.tintColor) { _decrementImageStyle.tintColor = undefined; } return ( <Image testID={decrementImageTestID} style={[_decrementImageStyle, {opacity}]} source={decrementImage} /> ); } const {hasReachedMin, hasReachedMax} = _getHasMinMax(); const decrementOpacity = hasReachedMin || disabled ? disabledOpacity : 1; const incrementOpacity = hasReachedMax || disabled ? disabledOpacity : 1; const isLeft = showText && textPosition === 'left'; const isCenter = showText && textPosition === 'center'; const isRight = showText && textPosition === 'right'; if (hasReachedMin) { onMin(value); } if (hasReachedMax) { onMax(value); } let _containerStyle = containerStyle; let _separatorStyle = separatorStyle; if (useColor && color) { _containerStyle.borderColor = color; _separatorStyle.backgroundColor = color; } return ( <View> <View testID={containerTestID} style={_containerStyle}> {isLeft && _renderText()} {isLeft && <View testID={separatorTestID} style={_separatorStyle} />} {renderDecrementStep ? ( renderDecrementStep(value, _decrementAction) ) : ( <TouchableOpacity testID={decrementButtonTestID} style={decrementStepStyle} activeOpacity={activeOpacity} onPress={_decrementAction} disabled={hasReachedMin || disabled}> {_renderDecrementImage(decrementOpacity)} </TouchableOpacity> )} {isCenter && <View testID={separatorTestID} style={_separatorStyle} />} {isCenter && _renderText()} <View style={_separatorStyle} /> {renderIncrementStep ? ( renderIncrementStep(value, _incrementAction) ) : ( <TouchableOpacity testID={incrementButtonTestID} style={incrementStepStyle} activeOpacity={activeOpacity} onPress={_incrementAction} disabled={hasReachedMax || disabled}> {_renderIncrementImage(incrementOpacity)} </TouchableOpacity> )} {isRight && <View testID={separatorTestID} style={_separatorStyle} />} {isRight && _renderText()} </View> </View> ); }; export default SimpleStepper;