summaryrefslogtreecommitdiff
path: root/app/components/switch
diff options
context:
space:
mode:
Diffstat (limited to 'app/components/switch')
-rw-r--r--app/components/switch/switch.props.ts39
-rw-r--r--app/components/switch/switch.story.tsx110
-rw-r--r--app/components/switch/switch.tsx114
3 files changed, 263 insertions, 0 deletions
diff --git a/app/components/switch/switch.props.ts b/app/components/switch/switch.props.ts
new file mode 100644
index 0000000..8235457
--- /dev/null
+++ b/app/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/components/switch/switch.story.tsx b/app/components/switch/switch.story.tsx
new file mode 100644
index 0000000..998d1df
--- /dev/null
+++ b/app/components/switch/switch.story.tsx
@@ -0,0 +1,110 @@
+/* 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/components/switch/switch.tsx b/app/components/switch/switch.tsx
new file mode 100644
index 0000000..0813747
--- /dev/null
+++ b/app/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>
+ )
+}