diff options
Diffstat (limited to 'gui/src/renderer')
| -rw-r--r-- | gui/src/renderer/components/Switch.tsx | 242 |
1 files changed, 80 insertions, 162 deletions
diff --git a/gui/src/renderer/components/Switch.tsx b/gui/src/renderer/components/Switch.tsx index 3e693706f9..ceda826fb0 100644 --- a/gui/src/renderer/components/Switch.tsx +++ b/gui/src/renderer/components/Switch.tsx @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { Animated, Component, GestureView, Styles, Types, View } from 'reactxp'; +import React from 'react'; +import styled from 'styled-components'; import { colors } from '../../config.json'; interface IProps { @@ -12,80 +12,43 @@ interface IState { isPressed: boolean; } -const styles = { - holder: Styles.createViewStyle({ - width: 52, - height: 32, - borderColor: colors.white, - borderWidth: 2, - borderStyle: 'solid', - borderRadius: 16, - padding: 2, - }), - knob: Styles.createViewStyle({ - height: 24, - borderRadius: 24, - }), -}; +const PAN_DISTANCE = 10; -interface IPosition { - x: number; - y: number; -} - -const SWITCH_DEFAULT_WIDTH = 24; -const SWITCH_PRESSED_WIDTH = 28; +const SwitchContainer = styled.div({ + position: 'relative', + width: '52px', + height: '32px', + borderColor: colors.white, + borderWidth: '2px', + borderStyle: 'solid', + borderRadius: '16px', + padding: '2px', +}); -export default class Switch extends Component<IProps, IState> { - public static defaultProps: Partial<IProps> = { - isOn: false, - onChange: undefined, - }; +const Knob = styled.div({}, (props: { isOn: boolean; isPressed: boolean }) => ({ + position: 'absolute', + height: '24px', + borderRadius: '12px', + transition: 'all 200ms linear', + width: props.isPressed ? '28px' : '24px', + backgroundColor: props.isOn ? colors.green : colors.red, + // When enabled the button should be placed all the way to the right (100%) minus padding (2px). + left: props.isOn ? 'calc(100% - 2px)' : '2px', + // This moves the knob to the left making the right side aligned with the parent's right side. + transform: `translateX(${props.isOn ? '-100%' : '0'})`, +})); +export default class Switch extends React.Component<IProps, IState> { public state: IState = { - isOn: false, + isOn: this.props.isOn, isPressed: false, }; - private isPanning = false; - private startPos = { x: 0, y: 0 }; - private startValue = false; - - private translationValue = Animated.createValue(0); - private widthValue = Animated.createValue(SWITCH_DEFAULT_WIDTH); - private colorValue = Animated.createValue(0); - private interpolatedColorValue = Animated.interpolate( - this.colorValue, - [0, 1], - [colors.red, colors.green], - ); - private animatedStyle = Styles.createAnimatedViewStyle({ - width: this.widthValue, - backgroundColor: this.interpolatedColorValue, - transform: [ - { - translateX: this.translationValue, - }, - ], - }); - private animation?: Types.Animated.CompositeAnimation; - - constructor(props: IProps) { - super(props); + private containerRef = React.createRef<HTMLDivElement>(); - this.state.isOn = props.isOn; - - if (props.isOn) { - this.translationValue.setValue(this.computeTranslation(props.isOn, false)); - this.colorValue.setValue(1); - } - } - - public componentWillUnmount() { - if (this.animation) { - this.animation.stop(); - } - } + private isPanning = false; + private startPos = 0; + private changedDuringPan = false; public shouldComponentUpdate(nextProps: IProps, nextState: IState) { return ( @@ -95,131 +58,86 @@ export default class Switch extends Component<IProps, IState> { ); } - public componentDidUpdate(prevProps: IProps, prevState: IState) { + public componentDidUpdate(prevProps: IProps, _prevState: IState) { if ( this.props.isOn !== prevProps.isOn && this.props.isOn !== this.state.isOn && !this.isPanning ) { this.setState({ isOn: this.props.isOn }); - } else if (prevState.isOn !== this.state.isOn || prevState.isPressed !== this.state.isPressed) { - this.animate(); } } public render() { return ( - <GestureView - preferredPan={Types.PreferredPanGesture.Horizontal} - onPanHorizontal={this.onPanHorizontal} - onTap={this.onTap}> - <View style={styles.holder}> - <Animated.View style={[styles.knob, this.animatedStyle]} /> - </View> - </GestureView> + <SwitchContainer ref={this.containerRef} onClick={this.handleClick}> + <Knob + isOn={this.state.isOn} + isPressed={this.state.isPressed} + onMouseDown={this.handleMouseDown} + /> + </SwitchContainer> ); } - private onTap = (_gesture: Types.TapGestureState) => { - this.setState( - (state) => ({ isOn: !state.isOn, isPressed: false }), - () => { - this.notify(); - }, - ); + private handleClick = () => { + if (!this.changedDuringPan) { + this.setState((state) => ({ isOn: !state.isOn }), this.notify); + } + + // Needs to be reset to allow clicks on container after panning. + this.changedDuringPan = false; }; - private onPanHorizontal = (gesture: Types.PanGestureState) => { - if (this.isPanning) { - if (gesture.isComplete) { - this.isPanning = false; + private handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => { + this.isPanning = true; + this.startPos = event.clientX; + this.changedDuringPan = false; - this.setState({ isPressed: false }, () => { - if (this.startValue !== this.state.isOn) { - this.notify(); - } - }); - } else { - const currentPos = { x: gesture.clientX, y: gesture.clientY }; - const nextOn = this.computeNextState(this.startPos, currentPos); + document.addEventListener('mouseup', this.handleMouseUp); + document.addEventListener('mousemove', this.handleMouseMove); + }; - if (this.state.isOn !== nextOn) { - this.startPos = currentPos; + private handleMouseUp = (event: MouseEvent) => { + document.removeEventListener('mouseup', this.handleMouseUp); + document.removeEventListener('mousemove', this.handleMouseMove); - this.setState({ isOn: nextOn }); - } - } - } else { - if (gesture.isComplete) { - return; - } + this.setState({ isPressed: false }); + this.isPanning = false; + // Reset changedDuringPan when onClick wont be called. + if (event.target instanceof Element && !this.containerRef.current?.contains(event.target)) { + this.changedDuringPan = false; + } - this.isPanning = true; - this.startPos = { x: gesture.clientX, y: gesture.clientY }; - this.startValue = this.state.isOn; + if (this.props.isOn !== this.state.isOn) { + this.notify(); + } + }; + + private handleMouseMove = (event: MouseEvent) => { + if (this.isPanning) { this.setState({ isPressed: true }); + + const nextOn = this.computeNextState(event.clientX); + if (this.state.isOn !== nextOn) { + this.startPos = event.clientX; + this.changedDuringPan = true; + this.setState({ isOn: nextOn }); + } } }; - private computeNextState(initialPos: IPosition, currentPos: IPosition): boolean { - if (currentPos.x < initialPos.x && this.state.isOn) { + private computeNextState(currentPos: number): boolean { + if (currentPos + PAN_DISTANCE < this.startPos && this.state.isOn) { return false; - } else if (currentPos.x > initialPos.x && !this.state.isOn) { + } else if (currentPos - PAN_DISTANCE > this.startPos && !this.state.isOn) { return true; } else { return this.state.isOn; } } - private computeKnobWidth(isPressed: boolean) { - return isPressed ? SWITCH_PRESSED_WIDTH : SWITCH_DEFAULT_WIDTH; - } - - private computeTranslation(isOn: boolean, isPressed: boolean) { - if (isOn) { - return isPressed ? 16 : 20; - } else { - return 0; - } - } - - private animate(onFinish?: (done: boolean) => void) { - const duration = 200; - const animation = Animated.parallel([ - Animated.timing(this.translationValue, { - toValue: this.computeTranslation(this.state.isOn, this.state.isPressed), - duration, - }), - Animated.timing(this.widthValue, { - toValue: this.computeKnobWidth(this.state.isPressed), - duration, - }), - Animated.timing(this.colorValue, { - toValue: this.state.isOn ? 1 : 0, - duration, - }), - ]); - - if (this.animation) { - this.animation.stop(); - } - - animation.start((options) => { - if (options.finished) { - this.animation = undefined; - } - - if (onFinish) { - onFinish(options.finished); - } - }); - - this.animation = animation; - } - private notify() { - if (this.props.onChange) { - this.props.onChange(this.state.isOn); - } + this.props.onChange?.(this.state.isOn); } } |
