diff options
author | Kirill Rogovoy <[email protected]> | 2021-07-20 21:24:52 +0300 |
---|---|---|
committer | Kirill Rogovoy <[email protected]> | 2021-07-20 21:24:52 +0300 |
commit | 5f4611d65e40eae3ca6191a15f68d69ea5a1c4cb (patch) | |
tree | 273dfc086444533d86d580961c92ba8d14781a67 /app_expo/components/switch | |
parent | f0bf4e7afdcd8b02a62be45ab3e7d047ed865a79 (diff) |
WIP
Diffstat (limited to 'app_expo/components/switch')
-rw-r--r-- | app_expo/components/switch/switch.props.ts | 39 | ||||
-rw-r--r-- | app_expo/components/switch/switch.story.tsx | 116 | ||||
-rw-r--r-- | app_expo/components/switch/switch.tsx | 114 |
3 files changed, 269 insertions, 0 deletions
diff --git a/app_expo/components/switch/switch.props.ts b/app_expo/components/switch/switch.props.ts new file mode 100644 index 0000000..2549a95 --- /dev/null +++ b/app_expo/components/switch/switch.props.ts @@ -0,0 +1,39 @@ +import { StyleProp, ViewStyle } from 'react-native' + +export interface SwitchProps { + /** + * On or off. + */ + value?: boolean + /** + * Fires when the on/off switch triggers. + * + * @param newValue The new value we're switching to. + */ + onToggle?: (newValue: boolean) => void + + /** + * A style override to apply to the container. Useful for margins and paddings. + */ + style?: StyleProp<ViewStyle> + + /** + * Additional track styling when on. + */ + trackOnStyle?: StyleProp<ViewStyle> + + /** + * Additional track styling when off. + */ + trackOffStyle?: StyleProp<ViewStyle> + + /** + * Additional thumb styling when on. + */ + thumbOnStyle?: StyleProp<ViewStyle> + + /** + * Additional thumb styling when off. + */ + thumbOffStyle?: StyleProp<ViewStyle> +} diff --git a/app_expo/components/switch/switch.story.tsx b/app_expo/components/switch/switch.story.tsx new file mode 100644 index 0000000..b10f8c6 --- /dev/null +++ b/app_expo/components/switch/switch.story.tsx @@ -0,0 +1,116 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ + +import * as React from 'react' +import { View, ViewStyle } from 'react-native' +import { storiesOf } from '@storybook/react-native' +import { StoryScreen, Story, UseCase } from '../../../storybook/views' +import { Toggle } from 'react-powerplug' +import { Switch } from './switch' + +declare let module + +const styleArray: ViewStyle[] = [{ borderColor: '#686868' }] + +const trackOffStyle: ViewStyle[] = [ + { backgroundColor: '#686868' }, + { + height: 80, + borderRadius: 0, + }, +] +const trackOnStyle: ViewStyle[] = [ + { + backgroundColor: '#b1008e', + borderColor: '#686868', + }, + { + height: 80, + borderRadius: 0, + }, +] +const thumbOffStyle: ViewStyle[] = [ + { + backgroundColor: '#b1008e', + borderColor: '#686868', + }, + { + height: 80, + borderRadius: 0, + }, +] +const thumbOnStyle: ViewStyle[] = [ + { backgroundColor: '#f0c' }, + { + height: 80, + borderRadius: 0, + borderColor: '#686868', + }, +] + +storiesOf('Switch', module) + .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>) + .add('Behaviour', () => ( + <Story> + <UseCase text="The Toggle Switch" usage="Use the switch to represent on/off states."> + <Toggle initial={false}> + {({ on, toggle }) => <Switch value={on} onToggle={toggle} />} + </Toggle> + </UseCase> + <UseCase text="value = true" usage="This is permanently on."> + <Switch value={true} /> + </UseCase> + <UseCase text="value = false" usage="This is permanantly off."> + <Switch value={false} /> + </UseCase> + </Story> + )) + .add('Styling', () => ( + <Story> + <UseCase text="Custom Styling" usage="Promise me this won't happen."> + <Toggle initial={false}> + {({ on, toggle }) => ( + <View> + <Switch + trackOnStyle={{ + backgroundColor: 'green', + borderColor: 'black', + }} + trackOffStyle={{ + backgroundColor: 'red', + borderColor: 'maroon', + }} + thumbOnStyle={{ backgroundColor: 'cyan' }} + thumbOffStyle={{ backgroundColor: 'pink' }} + value={on} + onToggle={toggle} + /> + </View> + )} + </Toggle> + </UseCase> + + <UseCase text="Style array" usage="This either."> + <Toggle initial={false}> + {({ on, toggle }) => ( + <View> + <Switch + style={styleArray} + trackOffStyle={trackOffStyle} + trackOnStyle={trackOnStyle} + thumbOffStyle={thumbOffStyle} + thumbOnStyle={thumbOnStyle} + // trackOnStyle={{ backgroundColor: "green", borderColor: "black" }} + // trackOffStyle={{ backgroundColor: "red", borderColor: "maroon" }} + // thumbOnStyle={{ backgroundColor: "cyan" }} + // thumbOffStyle={{ backgroundColor: "pink" }} + + value={on} + onToggle={toggle} + /> + </View> + )} + </Toggle> + </UseCase> + </Story> + )) diff --git a/app_expo/components/switch/switch.tsx b/app_expo/components/switch/switch.tsx new file mode 100644 index 0000000..845d964 --- /dev/null +++ b/app_expo/components/switch/switch.tsx @@ -0,0 +1,114 @@ +import React from 'react' +import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from 'react-native' +import { color } from '../../theme' +import { SwitchProps } from './switch.props' + +// dimensions +const THUMB_SIZE = 30 +const WIDTH = 56 +const MARGIN = 2 +const OFF_POSITION = -0.5 +const ON_POSITION = WIDTH - THUMB_SIZE - MARGIN +const BORDER_RADIUS = (THUMB_SIZE * 3) / 4 + +// colors +const ON_COLOR = color.primary +const OFF_COLOR = color.palette.offWhite +const BORDER_ON_COLOR = ON_COLOR +const BORDER_OFF_COLOR = 'rgba(0, 0, 0, 0.1)' + +// animation +const DURATION = 250 + +// the track always has these props +const TRACK = { + height: THUMB_SIZE + MARGIN, + width: WIDTH, + borderRadius: BORDER_RADIUS, + borderWidth: MARGIN / 2, + backgroundColor: color.background, +} + +// the thumb always has these props +const THUMB: ViewStyle = { + position: 'absolute', + width: THUMB_SIZE, + height: THUMB_SIZE, + borderColor: BORDER_OFF_COLOR, + borderRadius: THUMB_SIZE / 2, + borderWidth: MARGIN / 2, + backgroundColor: color.background, + shadowColor: BORDER_OFF_COLOR, + shadowOffset: { width: 1, height: 2 }, + shadowOpacity: 1, + shadowRadius: 2, + elevation: 2, +} + +const makeAnimatedValue = (switchOn) => new Animated.Value(switchOn ? 1 : 0) + +export function Switch(props: SwitchProps) { + const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value)) + const startAnimation = React.useMemo( + () => (newValue: boolean) => { + const toValue = newValue ? 1 : 0 + const easing = Easing.out(Easing.circle) + Animated.timing(timer, { + toValue, + duration: DURATION, + easing, + useNativeDriver: true, + }).start() + }, + [timer], + ) + + const [previousValue, setPreviousValue] = React.useState<boolean>(props.value) + React.useEffect(() => { + if (props.value !== previousValue) { + startAnimation(props.value) + setPreviousValue(props.value) + } + }, [props.value]) + + const handlePress = React.useMemo( + () => () => props.onToggle && props.onToggle(!props.value), + [props.onToggle, props.value], + ) + + if (!timer) { + return null + } + + const translateX = timer.interpolate({ + inputRange: [0, 1], + outputRange: [OFF_POSITION, ON_POSITION], + }) + + const style = props.style + + const trackStyle = [ + TRACK, + { + backgroundColor: props.value ? ON_COLOR : OFF_COLOR, + borderColor: props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR, + }, + props.value ? props.trackOnStyle : props.trackOffStyle, + ] + + const thumbStyle = [ + THUMB, + { + transform: [{ translateX }], + }, + props.value ? props.thumbOnStyle : props.thumbOffStyle, + ] + + return ( + <TouchableWithoutFeedback onPress={handlePress} style={style}> + <Animated.View style={trackStyle}> + <Animated.View style={thumbStyle} /> + </Animated.View> + </TouchableWithoutFeedback> + ) +} |