diff options
Diffstat (limited to 'app/components/switch')
-rw-r--r-- | app/components/switch/switch.props.ts | 39 | ||||
-rw-r--r-- | app/components/switch/switch.story.tsx | 110 | ||||
-rw-r--r-- | app/components/switch/switch.tsx | 114 |
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> + ) +} |