summaryrefslogtreecommitdiff
path: root/app_expo/components/switch
diff options
context:
space:
mode:
authorKirill Rogovoy <[email protected]>2021-07-20 21:24:52 +0300
committerKirill Rogovoy <[email protected]>2021-07-20 21:24:52 +0300
commit5f4611d65e40eae3ca6191a15f68d69ea5a1c4cb (patch)
tree273dfc086444533d86d580961c92ba8d14781a67 /app_expo/components/switch
parentf0bf4e7afdcd8b02a62be45ab3e7d047ed865a79 (diff)
WIP
Diffstat (limited to 'app_expo/components/switch')
-rw-r--r--app_expo/components/switch/switch.props.ts39
-rw-r--r--app_expo/components/switch/switch.story.tsx116
-rw-r--r--app_expo/components/switch/switch.tsx114
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>
+ )
+}