Skip to content

Slider - add range slider functionality #2084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 64 additions & 24 deletions demo/src/screens/componentScreens/SliderScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, {Component} from 'react';
import {StyleSheet, ScrollView} from 'react-native';
import {Colors, View, Text, Icon, Slider, GradientSlider, ColorSliderGroup, Constants} from 'react-native-ui-lib';

const INITIAL_VALUE = 0;
const COLOR = Colors.blue30;
import {renderBooleanOption} from '../ExampleScreenPresenter';

interface SliderScreenProps {
componentId: string;
Expand All @@ -13,18 +11,28 @@ interface SliderScreenState {
alpha: number;
color: string;
sliderValue: number;
sliderMinValue: number;
sliderMaxValue: number;
forceLTR: boolean;
}

const INITIAL_VALUE = 0;
const COLOR = Colors.blue30;

export default class SliderScreen extends Component<SliderScreenProps, SliderScreenState> {
constructor(props: SliderScreenProps) {
super(props);

this.state = {
alpha: 1,
color: COLOR,
sliderValue: INITIAL_VALUE
};
}
state = {
alpha: 1,
color: COLOR,
sliderValue: INITIAL_VALUE,
sliderMinValue: 0,
sliderMaxValue: 100,
forceLTR: false
};

onSliderRangeChange = (values: {min: number, max: number}) => {
const {min, max} = values;
this.setState({sliderMinValue: min, sliderMaxValue: max});
};

onSliderValueChange = (value: number) => {
this.setState({sliderValue: value});
Expand All @@ -38,17 +46,26 @@ export default class SliderScreen extends Component<SliderScreenProps, SliderScr
console.warn('onGroupValueChange: ', value);
};

getReverseStyle = () => {
return Constants.isRTL && this.state.forceLTR && styles.ltr;
}

render() {
const {color, alpha, sliderValue} = this.state;
const {color, alpha, sliderValue, sliderMinValue, sliderMaxValue, forceLTR} = this.state;

return (
<ScrollView showsVerticalScrollIndicator={false}>
<View flex padding-20>
<Text titleHuge $textDefault marginB-20>
<Text text40 $textDefault marginB-20>
Sliders
</Text>

<View row centerV style={Constants.isRTL && styles.ltr}>
{Constants.isRTL && renderBooleanOption.call(this, 'Force LTR', 'forceLTR')}

<Text $textDefault text70BO marginB-10>
Default slider
</Text>
<View row centerV style={this.getReverseStyle()}>
<Icon assetName={'search'} style={styles.image}/>
<Slider
onValueChange={this.onSliderValueChange}
Expand All @@ -57,14 +74,14 @@ export default class SliderScreen extends Component<SliderScreenProps, SliderScr
maximumValue={100}
step={1}
containerStyle={styles.sliderContainer}
disableRTL
disableRTL={forceLTR}
/>
<Text bodySmall $textNeutral style={styles.text}>
{sliderValue}%
<Text bodySmall $textNeutral style={styles.text} numberOfLines={1}>
${sliderValue}
</Text>
</View>

<Text $textDefault marginT-30>
<Text $textDefault text70BO marginT-30>
Negatives
</Text>
<Slider
Expand All @@ -84,12 +101,12 @@ export default class SliderScreen extends Component<SliderScreenProps, SliderScr
containerStyle={styles.slider}
/>

<Text $textDefault marginT-20>
<Text $textDefault text70BO marginT-20>
Disabled
</Text>
<Slider minimumValue={100} maximumValue={200} value={120} containerStyle={styles.slider} disabled/>

<Text $textDefault marginT-15>
<Text $textDefault text70BO marginT-15>
Custom with Steps
</Text>
<Slider
Expand All @@ -106,7 +123,28 @@ export default class SliderScreen extends Component<SliderScreenProps, SliderScr
maximumTrackTintColor={Colors.violet70}
/>

<Text $textDefault marginT-15>
<Text $textDefault text70BO marginV-15>
Range slider
</Text>
<View row spread style={this.getReverseStyle()}>
<Text bodySmall $textNeutral>
min. {sliderMinValue}%
</Text>
<Text bodySmall $textNeutral>
max. {sliderMaxValue}%
</Text>
</View>
<Slider
useRange
onRangeChange={this.onSliderRangeChange}
value={INITIAL_VALUE}
minimumValue={0}
maximumValue={100}
step={1}
disableRTL={forceLTR}
/>

<Text $textDefault text70BO marginT-15>
Gradient Sliders
</Text>
<View row centerV>
Expand Down Expand Up @@ -137,7 +175,7 @@ export default class SliderScreen extends Component<SliderScreenProps, SliderScr
</View>
</View>

<Text $textDefault marginT-25 marginB-20>
<Text $textDefault text70BO marginV-15>
Color Slider Group
</Text>
<ColorSliderGroup
Expand Down Expand Up @@ -189,7 +227,9 @@ const styles = StyleSheet.create({
activeThumb: {
width: 40,
height: 40,
borderRadius: 20
borderRadius: 20,
borderColor: Colors.yellow30,
borderWidth: 2
},
box: {
width: 20,
Expand Down
2 changes: 1 addition & 1 deletion src/components/slider/ColorSliderGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'lodash';
import React, {PureComponent, GetDerivedStateFromProps} from 'react';
import {StyleProp, ViewStyle, TextStyle} from 'react-native';
import _ from 'lodash';
import {asBaseComponent} from '../../commons/new';
import GradientSlider, {GradientSliderTypes} from './GradientSlider';
import SliderGroup from './context/SliderGroup';
Expand Down
9 changes: 5 additions & 4 deletions src/components/slider/GradientSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
import {StyleProp, ViewStyle} from 'react-native';
import _ from 'lodash';
import tinycolor from 'tinycolor2';
import React, {Component} from 'react';
import {StyleProp, ViewStyle} from 'react-native';
import {HueGradient, LightnessGradient, SaturationGradient, Gradient} from 'react-native-color';
import {Colors} from '../../style';
import {asBaseComponent} from '../../commons/new';
Expand Down Expand Up @@ -141,12 +141,12 @@ class GradientSlider extends Component<GradientSliderComponentProps, GradientSli

onValueChange = (value: string, alpha: number) => {
// alpha returns for type.DEFAULT
_.invoke(this.props, 'onValueChange', value, alpha);
this.props.onValueChange?.(value, alpha);
};

updateColor(color: tinycolor.ColorFormats.HSLA) {
if (!_.isEmpty(this.props.sliderContext)) {
_.invoke(this.props.sliderContext, 'setValue', color);
this.props.sliderContext.setValue?.(color);
} else {
this.setState({color});
const hex = Colors.getHexString(color);
Expand Down Expand Up @@ -219,6 +219,7 @@ class GradientSlider extends Component<GradientSliderComponentProps, GradientSli
containerStyle={containerStyle}
disabled={disabled}
accessible={accessible}
useRange={false}
/>
);
}
Expand Down
178 changes: 178 additions & 0 deletions src/components/slider/Thumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import _ from 'lodash';
import React, {forwardRef, useRef} from 'react';
import {
StyleSheet,
StyleProp,
ViewStyle,
ViewProps,
View as RNView,
Animated,
GestureResponderEvent
} from 'react-native';
import {useCombinedRefs} from 'hooks';
import {Colors} from '../../style';

export interface ThumbProps extends ViewProps {
/**
* The thumb style
*/
thumbStyle?: ViewStyle;
/**
* The active (during press) thumb style
*/
activeThumbStyle?: ViewStyle;
/**
* If true the Slider will not change it's style on press
*/
disableActiveStyling?: boolean;
/**
* Defines how far a touch event can start away from the thumb.
*/
thumbHitSlop?: ViewProps['hitSlop'];
/**
* Thumb color
*/
thumbTintColor?: string;
/**
* If true the Slider will be disabled and will appear in disabled color
*/
disabled?: boolean;
/** ref to thumb component */
ref?: React.RefObject<RNView>;
}
type ThumbStyle = {style?: StyleProp<ViewStyle>; left?: StyleProp<number>};

const THUMB_SIZE = 24;
const BORDER_WIDTH = 6;
const SHADOW_RADIUS = 4;
const DEFAULT_COLOR = Colors.$backgroundDisabled;
const ACTIVE_COLOR = Colors.$backgroundPrimaryHeavy;

const Thumb = forwardRef((props: ThumbProps, ref: any) => {
const {
disabled,
thumbStyle,
activeThumbStyle,
disableActiveStyling,
thumbTintColor,
thumbHitSlop,
onTouchStart,
onTouchEnd,
...others
} = props;

const thumbRef = useCombinedRefs(ref);

/** Scaling thumb */

const _onTouchStart = (e: GestureResponderEvent) => {
updateThumbStyle(true);
onTouchStart?.(e);
};

const _onTouchEnd = (e: GestureResponderEvent) => {
updateThumbStyle(false);
onTouchEnd?.(e);
};

const thumbStyles: ThumbStyle = {};
const thumbScaleAnimation = useRef(new Animated.Value(1));
const thumbAnimationConstants = {
duration: 100,
defaultScaleFactor: 1.5
};

const updateThumbStyle = (start: boolean) => {
if (!disableActiveStyling) {
const style = thumbStyle || styles.thumb;
const activeStyle = activeThumbStyle || styles.activeThumb;
const activeOrInactiveStyle = !disabled ? (start ? activeStyle : style) : {};

thumbStyles.style = _.omit(activeOrInactiveStyle, 'height', 'width');
//@ts-expect-error
thumbRef.current?.setNativeProps(thumbStyles);

scaleThumb(start);
}
};

const calculateThumbScale = () => {
if (disabled || disableActiveStyling) {
return 1;
}

const {defaultScaleFactor} = thumbAnimationConstants;
if (!activeThumbStyle || !thumbStyle) {
return defaultScaleFactor;
}

const scaleRatioFromSize = Number(activeThumbStyle.height) / Number(thumbStyle.height);
return scaleRatioFromSize || defaultScaleFactor;
};

const animateScale = (toValue: number) => {
const {duration} = thumbAnimationConstants;
Animated.timing(thumbScaleAnimation.current, {
toValue,
duration,
useNativeDriver: true
}).start();
};

const scaleThumb = (start: boolean) => {
const scaleFactor = start ? calculateThumbScale() : 1;
animateScale(scaleFactor);
};

return (
<Animated.View
ref={thumbRef}
{...others}
hitSlop={thumbHitSlop}
onTouchStart={_onTouchStart}
onTouchEnd={_onTouchEnd}
style={[
styles.thumb,
disabled && styles.disabledThumb,
thumbStyle,
{
backgroundColor: disabled ? DEFAULT_COLOR : thumbTintColor || ACTIVE_COLOR
},
{
transform: [
{
scale: thumbScaleAnimation.current
}
]
}
]}
/>
);
});

export default Thumb;

const styles = StyleSheet.create({
thumb: {
position: 'absolute',
width: THUMB_SIZE,
height: THUMB_SIZE,
borderRadius: THUMB_SIZE / 2,
borderWidth: BORDER_WIDTH,
borderColor: Colors.white,
shadowColor: Colors.rgba(Colors.black, 0.3),
shadowOffset: {width: 0, height: 0},
shadowOpacity: 0.9,
shadowRadius: SHADOW_RADIUS,
elevation: 2
},
disabledThumb: {
borderColor: Colors.$backgroundElevated
},
activeThumb: {
width: THUMB_SIZE + 16,
height: THUMB_SIZE + 16,
borderRadius: (THUMB_SIZE + 16) / 2,
borderWidth: BORDER_WIDTH
}
});
Loading