summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-06-01 17:31:30 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-06-01 17:31:30 +0200
commit4e03a95e5a21ccea92933ec1c9b6bb07b8bcaf15 (patch)
tree9417db8c804a09b59b8915af31723eec0e327cd6
parent9681b2245e75e36ef2896e0f78bc7994689aa89b (diff)
parent9f73d666727e9a50d8260fc42e1ec707d0041f32 (diff)
downloadmullvadvpn-4e03a95e5a21ccea92933ec1c9b6bb07b8bcaf15.tar.xz
mullvadvpn-4e03a95e5a21ccea92933ec1c9b6bb07b8bcaf15.zip
Merge branch 'convert-marquee-to-styled-component'
-rw-r--r--gui/src/renderer/components/Marquee.tsx119
-rw-r--r--gui/src/renderer/components/TunnelControl.tsx96
2 files changed, 107 insertions, 108 deletions
diff --git a/gui/src/renderer/components/Marquee.tsx b/gui/src/renderer/components/Marquee.tsx
index 8dc6724797..d2d5bcbccd 100644
--- a/gui/src/renderer/components/Marquee.tsx
+++ b/gui/src/renderer/components/Marquee.tsx
@@ -1,82 +1,95 @@
-import * as React from 'react';
-import { Animated, Component, Styles, Types, UserInterface, View } from 'reactxp';
+import React from 'react';
+import styled from 'styled-components';
import { Scheduler } from '../../shared/scheduler';
-const styles = {
- text: Styles.createTextStyle({
- // @ts-ignore
- width: 'fit-content',
+const Container = styled.div({
+ overflow: 'hidden',
+});
+
+const Text = styled.span(
+ {
+ position: 'relative',
whiteSpace: 'nowrap',
+ },
+ (props: { overflow: number; alignRight: boolean }) => ({
+ left: props.alignRight ? -props.overflow + 'px' : '0',
+ transition: `left linear ${props.overflow * 80}ms`,
}),
-};
+);
interface IMarqueeProps {
- style?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+interface IMarqueeState {
+ alignRight: boolean;
+ // uniqueKey is used to force the Text component to remount to achieve the initial position of the
+ // text without using a transition.
+ uniqueKey: number;
}
-export default class Marquee extends Component<IMarqueeProps> {
- private initialLeft = Animated.createValue(0.0);
- private textAnimation = Styles.createAnimatedTextStyle({ left: this.initialLeft });
- private textRef = React.createRef<Animated.Text>();
+export default class Marquee extends React.Component<IMarqueeProps, IMarqueeState> {
+ private textRef = React.createRef<HTMLSpanElement>();
+ private scheduler = new Scheduler();
- private animationScheduler = new Scheduler();
- private animation?: Types.Animated.CompositeAnimation;
+ public state = {
+ alignRight: false,
+ uniqueKey: 0,
+ };
public componentDidMount() {
- this.startAnimation();
+ this.startAnimationIfOverflow();
}
- public componentDidUpdate() {
- this.startAnimation();
+ public componentDidUpdate(prevProps: IMarqueeProps) {
+ if (this.props.children !== prevProps.children) {
+ this.scheduler.cancel();
+ this.setState(
+ (state) => ({
+ alignRight: false,
+ uniqueKey: state.uniqueKey + 1,
+ }),
+ this.startAnimationIfOverflow,
+ );
+ }
}
public componentWillUnmount() {
- this.stopAnimation();
+ this.scheduler.cancel();
}
public render() {
return (
- <View>
- <Animated.Text
+ <Container>
+ <Text
+ key={this.state.uniqueKey}
ref={this.textRef}
- style={[styles.text, this.textAnimation, this.props.style]}>
+ className={this.props.className}
+ overflow={this.calculateOverflow()}
+ alignRight={this.state.alignRight}
+ onTransitionEnd={this.scheduleToggleAlignRight}>
{this.props.children}
- </Animated.Text>
- </View>
+ </Text>
+ </Container>
);
}
- private startAnimation() {
- this.stopAnimation();
-
- this.animationScheduler.schedule(async () => {
- if (this.textRef.current) {
- const textLayout = await UserInterface.measureLayoutRelativeToWindow(this.textRef.current);
- const viewLayout = await UserInterface.measureLayoutRelativeToWindow(this);
- this.startAnimationImpl(textLayout.width - viewLayout.width, false);
- }
- }, 1000);
- }
-
- private startAnimationImpl(length: number, reverse: boolean) {
- if (length >= 0) {
- this.animation = Animated.timing(this.initialLeft, {
- toValue: reverse ? 0.0 : -length,
- duration: length * 80,
- delay: 2000,
- easing: Animated.Easing.Linear(),
- });
-
- this.animation.start(({ finished }) => {
- if (finished) {
- this.startAnimationImpl(length, !reverse);
- }
- });
+ private startAnimationIfOverflow = () => {
+ if (this.calculateOverflow() > 0) {
+ this.scheduleToggleAlignRight();
}
- }
+ };
+
+ private scheduleToggleAlignRight = () => {
+ this.scheduler.schedule(() => {
+ this.setState((state) => ({ alignRight: !state.alignRight }));
+ }, 2000);
+ };
- private stopAnimation() {
- this.animationScheduler.cancel();
- this.animation?.stop();
+ private calculateOverflow() {
+ const textWidth = this.textRef.current?.offsetWidth ?? 0;
+ const parentWidth = this.textRef.current?.parentElement?.offsetWidth ?? 0;
+ return textWidth - parentWidth;
}
}
diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx
index 7043c900c8..5bf00fb9fa 100644
--- a/gui/src/renderer/components/TunnelControl.tsx
+++ b/gui/src/renderer/components/TunnelControl.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Component, Styles, Types, View } from 'reactxp';
+import { Component, Styles, View } from 'reactxp';
import styled from 'styled-components';
import { colors } from '../../config.json';
import { TunnelState } from '../../shared/daemon-rpc-types';
@@ -27,23 +27,12 @@ const SwitchLocationButton = styled(AppButton.TransparentButton)({
});
const styles = {
- body: Styles.createViewStyle({
- paddingTop: 0,
- paddingLeft: 24,
- paddingRight: 24,
- paddingBottom: 0,
- marginTop: 176,
- flex: 1,
- }),
footer: Styles.createViewStyle({
flex: 0,
paddingBottom: 16,
paddingLeft: 24,
paddingRight: 24,
}),
- wrapper: Styles.createViewStyle({
- flex: 1,
- }),
status_security: Styles.createTextStyle({
fontFamily: 'Open Sans',
fontSize: 16,
@@ -51,31 +40,40 @@ const styles = {
lineHeight: 22,
marginBottom: 2,
}),
- status_location: Styles.createTextStyle({
- flexDirection: 'column',
- marginBottom: 2,
- }),
- status_location_text: Styles.createTextStyle({
- fontFamily: 'DINPro',
- fontSize: 34,
- lineHeight: 38,
- fontWeight: '900',
- overflow: 'hidden',
- letterSpacing: -0.9,
- color: colors.white,
- }),
};
+const Body = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ padding: '0 24px',
+ marginTop: '176px',
+ flex: 1,
+});
+
+const Wrapper = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+});
+
+const Location = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ marginBottom: 2,
+});
+
+const StyledMarquee = styled(Marquee)({
+ fontFamily: 'DINPro',
+ fontSize: '34px',
+ lineHeight: '38px',
+ fontWeight: 900,
+ overflow: 'hidden',
+ letterSpacing: -0.9,
+ color: colors.white,
+});
+
export default class TunnelControl extends Component<ITunnelControlProps> {
public render() {
- const Location = ({ children }: { children?: React.ReactNode }) => (
- <View style={styles.status_location}>{children}</View>
- );
- const City = () => <Marquee style={styles.status_location_text}>{this.props.city}</Marquee>;
- const Country = () => (
- <Marquee style={styles.status_location_text}>{this.props.country}</Marquee>
- );
-
const SwitchLocation = () => {
return (
<SwitchLocationButton onClick={this.props.onSelectLocation}>
@@ -153,8 +151,8 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
<Body>
<Secured displayStyle={SecuredDisplayStyle.securing} />
<Location>
- <City />
- <Country />
+ {this.renderCity()}
+ {this.renderCountry()}
</Location>
<ConnectionPanelContainer />
</Body>
@@ -170,8 +168,8 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
<Body>
<Secured displayStyle={SecuredDisplayStyle.secured} />
<Location>
- <City />
- <Country />
+ {this.renderCity()}
+ {this.renderCountry()}
</Location>
<ConnectionPanelContainer />
</Body>
@@ -217,9 +215,7 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
<Wrapper>
<Body>
<Secured displayStyle={SecuredDisplayStyle.secured} />
- <Location>
- <Country />
- </Location>
+ <Location>{this.renderCountry()}</Location>
</Body>
<Footer>
<SelectedLocation />
@@ -233,9 +229,7 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
<Wrapper>
<Body>
<Secured displayStyle={SecuredDisplayStyle.unsecured} />
- <Location>
- <Country />
- </Location>
+ <Location>{this.renderCountry()}</Location>
</Body>
<Footer>
<SelectedLocation />
@@ -248,20 +242,12 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
throw new Error(`Unknown TunnelState: ${this.props.tunnelState}`);
}
}
-}
-
-interface IContainerProps {
- children?: Types.ReactNode;
-}
-class Wrapper extends Component<IContainerProps> {
- public render() {
- return <View style={styles.wrapper}>{this.props.children}</View>;
+ private renderCity() {
+ return <StyledMarquee>{this.props.city}</StyledMarquee>;
}
-}
-class Body extends Component<IContainerProps> {
- public render() {
- return <View style={styles.body}>{this.props.children}</View>;
+ private renderCountry() {
+ return <StyledMarquee>{this.props.country}</StyledMarquee>;
}
}