summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-07-14 13:17:48 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-07-15 12:01:23 +0200
commite0b3667cea8cc465f3072636cf8138f3a3ec8caa (patch)
treeacd88fdc38789f98026327ed83f53c7b681d3f15 /gui/src/renderer
parentbd4f591f5fc56f383c8dbd437c748e36559cbed8 (diff)
downloadmullvadvpn-e0b3667cea8cc465f3072636cf8138f3a3ec8caa.tar.xz
mullvadvpn-e0b3667cea8cc465f3072636cf8138f3a3ec8caa.zip
Convert NavigationBar and SettingsTitle comopnents from ReactXP
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx11
-rw-r--r--gui/src/renderer/components/AdvancedSettingsStyles.tsx9
-rw-r--r--gui/src/renderer/components/CustomScrollbars.tsx2
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx6
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx9
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx634
-rw-r--r--gui/src/renderer/components/NavigationBarStyles.tsx96
-rw-r--r--gui/src/renderer/components/ScopeBar.tsx77
-rw-r--r--gui/src/renderer/components/SelectLanguage.tsx12
-rw-r--r--gui/src/renderer/components/SelectLocation.tsx12
-rw-r--r--gui/src/renderer/components/SelectLocationStyles.tsx9
-rw-r--r--gui/src/renderer/components/Settings.tsx7
-rw-r--r--gui/src/renderer/components/SettingsHeader.tsx91
-rw-r--r--gui/src/renderer/components/SettingsStyles.tsx9
-rw-r--r--gui/src/renderer/components/WireguardKeys.tsx7
-rw-r--r--gui/src/renderer/components/WireguardKeysStyles.tsx10
16 files changed, 369 insertions, 632 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx
index 38c035de32..21ab4ae88d 100644
--- a/gui/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/src/renderer/components/AdvancedSettings.tsx
@@ -4,7 +4,11 @@ import { sprintf } from 'sprintf-js';
import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { WgKeyState } from '../redux/settings/reducers';
-import styles, { InputFrame, TunnelProtocolSelector } from './AdvancedSettingsStyles';
+import styles, {
+ InputFrame,
+ StyledNavigationScrollbars,
+ TunnelProtocolSelector,
+} from './AdvancedSettingsStyles';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
import { Container, Layout } from './Layout';
@@ -14,7 +18,6 @@ import {
NavigationBar,
NavigationContainer,
NavigationItems,
- NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
import Selector, { ISelectorItem } from './Selector';
@@ -155,7 +158,7 @@ export default class AdvancedSettings extends Component<IProps, IState> {
</NavigationBar>
<View style={styles.advanced_settings__container}>
- <NavigationScrollbars style={styles.advanced_settings__scrollview}>
+ <StyledNavigationScrollbars>
<SettingsHeader>
<HeaderTitle>
{messages.pgettext('advanced-settings-view', 'Advanced')}
@@ -355,7 +358,7 @@ export default class AdvancedSettings extends Component<IProps, IState> {
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
</View>
- </NavigationScrollbars>
+ </StyledNavigationScrollbars>
</View>
</NavigationContainer>
</View>
diff --git a/gui/src/renderer/components/AdvancedSettingsStyles.tsx b/gui/src/renderer/components/AdvancedSettingsStyles.tsx
index 899bb5cc02..9e8897ce16 100644
--- a/gui/src/renderer/components/AdvancedSettingsStyles.tsx
+++ b/gui/src/renderer/components/AdvancedSettingsStyles.tsx
@@ -2,6 +2,7 @@ import { Styles } from 'reactxp';
import styled from 'styled-components';
import { colors } from '../../config.json';
import * as Cell from './Cell';
+import { NavigationScrollbars } from './NavigationBar';
import Selector from './Selector';
export const InputFrame = styled(Cell.InputFrame)({
@@ -12,6 +13,10 @@ export const TunnelProtocolSelector = (styled(Selector)({
marginBottom: 0,
}) as unknown) as new <T>() => Selector<T>;
+export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
+ flex: 1,
+});
+
export default {
advanced_settings: Styles.createViewStyle({
backgroundColor: colors.darkBlue,
@@ -20,10 +25,6 @@ export default {
advanced_settings__container: Styles.createViewStyle({
flex: 1,
}),
- // plain CSS style
- advanced_settings__scrollview: {
- flex: 1,
- },
advanced_settings__content: Styles.createViewStyle({
flex: 0,
}),
diff --git a/gui/src/renderer/components/CustomScrollbars.tsx b/gui/src/renderer/components/CustomScrollbars.tsx
index 87fd68fca2..544dd58604 100644
--- a/gui/src/renderer/components/CustomScrollbars.tsx
+++ b/gui/src/renderer/components/CustomScrollbars.tsx
@@ -7,7 +7,7 @@ interface IProps {
autoHide: boolean;
trackPadding: { x: number; y: number };
onScroll?: (value: IScrollEvent) => void;
- style?: React.CSSProperties;
+ className?: string;
fillContainer?: boolean;
children?: React.ReactNode;
}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index 0854f0ace2..17bfd68123 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -8,11 +8,11 @@ import { messages } from '../../shared/gettext';
import { LoginState } from '../redux/account/reducers';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
-import CustomScrollbars from './CustomScrollbars';
import styles, {
ModalCellContainer,
StyledAccountTokenLabel,
StyledBuyCreditButton,
+ StyledCustomScrollbars,
StyledDisconnectButton,
} from './ExpiredAccountErrorViewStyles';
import ImageView from './ImageView';
@@ -59,7 +59,7 @@ export default class ExpiredAccountErrorView extends Component<
public render() {
return (
- <CustomScrollbars style={styles.scrollview} fillContainer>
+ <StyledCustomScrollbars fillContainer>
<View style={styles.container}>
<View style={styles.body}>{this.renderContent()}</View>
@@ -84,7 +84,7 @@ export default class ExpiredAccountErrorView extends Component<
{this.state.showRedeemVoucherAlert && this.renderRedeemVoucherAlert()}
{this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
</View>
- </CustomScrollbars>
+ </StyledCustomScrollbars>
);
}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
index 7a719729dd..36ac76f1fc 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
@@ -4,6 +4,7 @@ import { colors } from '../../config.json';
import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
+import CustomScrollbars from './CustomScrollbars';
export const StyledAccountTokenLabel = styled(AccountTokenLabel)({
fontFamily: 'Open Sans',
@@ -26,11 +27,11 @@ const buttonStyle = {
export const StyledBuyCreditButton = styled(AppButton.GreenButton)(buttonStyle);
export const StyledDisconnectButton = styled(AppButton.RedButton)(buttonStyle);
+export const StyledCustomScrollbars = styled(CustomScrollbars)({
+ flex: 1,
+});
+
export default {
- // plain CSS style
- scrollview: {
- flex: 1,
- },
container: Styles.createViewStyle({
flex: 1,
paddingTop: 22,
diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx
index 8f45f39aa4..922c7c3c7e 100644
--- a/gui/src/renderer/components/NavigationBar.tsx
+++ b/gui/src/renderer/components/NavigationBar.tsx
@@ -1,134 +1,42 @@
-import * as React from 'react';
-import { Animated, Button, Component, Styles, Text, Types, UserInterface, View } from 'reactxp';
-import styled from 'styled-components';
-import { colors } from '../../config.json';
+import React, { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
import CustomScrollbars, { IScrollEvent } from './CustomScrollbars';
-import ImageView from './ImageView';
+import {
+ StyledBackBarItemButton,
+ StyledBackBarItemIcon,
+ StyledBackBarItemLabel,
+ StyledCloseBarItemIcon,
+ StyledNavigationBar,
+ StyledNavigationBarSeparator,
+ StyledNavigationBarWrapper,
+ StyledTitleBarItemContainer,
+ StyledTitleBarItemLabel,
+ StyledTitleBarItemMeasuringLabel,
+} from './NavigationBarStyles';
-const styles = {
- navigationBar: {
- default: Styles.createViewStyle({
- flex: 0,
- paddingHorizontal: 12,
- paddingBottom: 12,
- }),
- wrapper: Styles.createViewStyle({
- flex: 1,
- flexDirection: 'column',
- }),
- separator: Styles.createViewStyle({
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- height: 1,
- }),
- darwin: Styles.createViewStyle({
- paddingTop: 24,
- }),
- win32: Styles.createViewStyle({
- paddingTop: 12,
- }),
- linux: Styles.createViewStyle({
- paddingTop: 12,
- }),
- },
- navigationItems: Styles.createViewStyle({
- flex: 1,
- flexDirection: 'row',
- }),
- navigationBarTitle: {
- container: Styles.createViewStyle({
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'center',
- }),
- label: Styles.createTextStyle({
- fontFamily: 'Open Sans',
- fontSize: 16,
- fontWeight: '600',
- lineHeight: 22,
- color: colors.white,
- paddingHorizontal: 5,
- textAlign: 'center',
- }),
- measuringLabel: Styles.createTextStyle({
- position: 'absolute',
- opacity: 0,
- }),
- },
- closeBarItem: {
- default: Styles.createViewStyle({
- cursor: 'default',
- }),
- },
- backBarButton: {
- default: Styles.createViewStyle({
- borderWidth: 0,
- padding: 0,
- margin: 0,
- cursor: 'default',
- }),
- content: Styles.createViewStyle({
- flexDirection: 'row',
- alignItems: 'center',
- }),
- label: Styles.createTextStyle({
- fontFamily: 'Open Sans',
- fontSize: 13,
- fontWeight: '600',
- color: colors.white60,
- }),
- },
- scopeBar: {
- container: Styles.createViewStyle({
- flexDirection: 'row',
- backgroundColor: colors.blue40,
- borderRadius: 13,
- }),
- item: {
- base: Styles.createButtonStyle({
- cursor: 'default',
- flex: 1,
- flexBasis: 0,
- paddingHorizontal: 8,
- paddingVertical: 4,
- }),
- selected: Styles.createButtonStyle({
- backgroundColor: colors.green,
- }),
- hover: Styles.createButtonStyle({
- backgroundColor: colors.blue40,
- }),
- label: Styles.createTextStyle({
- fontFamily: 'Open Sans',
- fontSize: 13,
- color: colors.white,
- textAlign: 'center',
- }),
- },
- },
-};
-
-interface INavigationScrollContextValue {
- navigationContainer?: NavigationContainer;
- showsBarTitle: boolean;
- showsBarSeparator: boolean;
-}
+export { StyledNavigationItems as NavigationItems } from './NavigationBarStyles';
interface INavigationContainerProps {
children?: React.ReactNode;
}
-const NavigationScrollContext = React.createContext<INavigationScrollContextValue>({
+interface INavigationContainerState {
+ showsBarTitle: boolean;
+ showsBarSeparator: boolean;
+}
+
+const NavigationScrollContext = React.createContext({
showsBarTitle: false,
showsBarSeparator: false,
+ onScroll(_event: IScrollEvent): void {
+ throw Error('NavigationScrollContext provider missing');
+ },
});
-export class NavigationContainer extends Component<INavigationContainerProps> {
+export class NavigationContainer extends React.Component<
+ INavigationContainerProps,
+ INavigationContainerState
+> {
public state = {
- navigationContainer: this,
showsBarTitle: false,
showsBarSeparator: false,
};
@@ -141,7 +49,11 @@ export class NavigationContainer extends Component<INavigationContainerProps> {
public render() {
return (
- <NavigationScrollContext.Provider value={this.state}>
+ <NavigationScrollContext.Provider
+ value={{
+ ...this.state,
+ onScroll: this.onScroll,
+ }}>
{this.props.children}
</NavigationScrollContext.Provider>
);
@@ -188,7 +100,7 @@ export class NavigationContainer extends Component<INavigationContainerProps> {
interface INavigationScrollbarsProps {
onScroll?: (value: IScrollEvent) => void;
- style?: React.CSSProperties;
+ className?: string;
fillContainer?: boolean;
children?: React.ReactNode;
}
@@ -197,366 +109,59 @@ export const NavigationScrollbars = React.forwardRef(function NavigationScrollba
props: INavigationScrollbarsProps,
ref?: React.Ref<CustomScrollbars>,
) {
- return (
- <PrivateNavigationScrollbars forwardedRef={ref} {...props}>
- {props.children}
- </PrivateNavigationScrollbars>
- );
-});
-
-interface IPrivateNavigationScrollbars extends INavigationScrollbarsProps {
- forwardedRef?: React.Ref<CustomScrollbars>;
-}
-
-class PrivateNavigationScrollbars extends Component<IPrivateNavigationScrollbars> {
- public static contextType = NavigationScrollContext;
- public context!: React.ContextType<typeof NavigationScrollContext>;
-
- public render() {
- return (
- <CustomScrollbars
- ref={this.props.forwardedRef}
- style={this.props.style}
- fillContainer={this.props.fillContainer}
- onScroll={this.onScroll}>
- {this.props.children}
- </CustomScrollbars>
- );
- }
-
- private onScroll = (scroll: IScrollEvent) => {
- if (this.context.navigationContainer) {
- this.context.navigationContainer.onScroll(scroll);
- }
-
- if (this.props.onScroll) {
- this.props.onScroll(scroll);
- }
- };
-}
-
-interface IPrivateTitleBarItemProps {
- visible: boolean;
- titleAdjustment: number;
- measuringTextRef?: React.RefObject<Text>;
- children?: React.ReactText;
-}
-
-class PrivateTitleBarItem extends Component<IPrivateTitleBarItemProps> {
- public shouldComponentUpdate(nextProps: IPrivateTitleBarItemProps) {
- return (
- this.props.visible !== nextProps.visible ||
- this.props.titleAdjustment !== nextProps.titleAdjustment ||
- this.props.children !== nextProps.children
- );
- }
-
- public render() {
- const titleAdjustment = this.props.titleAdjustment;
- const titleAdjustmentStyle = Styles.createViewStyle({ marginLeft: titleAdjustment }, false);
-
- return (
- <View style={styles.navigationBarTitle.container}>
- <PrivateBarItemAnimationContainer visible={this.props.visible}>
- <Text
- style={[styles.navigationBarTitle.label, titleAdjustmentStyle]}
- ellipsizeMode="tail"
- numberOfLines={1}>
- {this.props.children}
- </Text>
- </PrivateBarItemAnimationContainer>
-
- <Text
- style={[styles.navigationBarTitle.label, styles.navigationBarTitle.measuringLabel]}
- numberOfLines={1}
- ref={this.props.measuringTextRef}>
- {this.props.children}
- </Text>
- </View>
- );
- }
-}
-
-interface IPrivateBarItemAnimationContainerProps {
- visible: boolean;
- children?: React.ReactNode;
-}
-
-class PrivateBarItemAnimationContainer extends Component<IPrivateBarItemAnimationContainerProps> {
- private opacityValue: Animated.Value;
- private opacityStyle: Types.AnimatedViewStyleRuleSet;
- private animation?: Types.Animated.CompositeAnimation;
-
- constructor(props: IPrivateBarItemAnimationContainerProps) {
- super(props);
-
- this.opacityValue = Animated.createValue(props.visible ? 1 : 0);
- this.opacityStyle = Styles.createAnimatedViewStyle({
- opacity: this.opacityValue,
- });
- }
-
- public shouldComponentUpdate(nextProps: IPrivateBarItemAnimationContainerProps) {
- return this.props.visible !== nextProps.visible || this.props.children !== nextProps.children;
- }
-
- public componentDidUpdate() {
- this.animateOpacity(this.props.visible);
- }
-
- public componentWillUnmount() {
- if (this.animation) {
- this.animation.stop();
- }
- }
-
- public render() {
- return <Animated.View style={this.opacityStyle}>{this.props.children}</Animated.View>;
- }
-
- private animateOpacity(visible: boolean) {
- const oldAnimation = this.animation;
- if (oldAnimation) {
- oldAnimation.stop();
- }
-
- const animation = Animated.timing(this.opacityValue, {
- toValue: visible ? 1 : 0,
- easing: Animated.Easing.InOut(),
- duration: 250,
- });
-
- animation.start();
-
- this.animation = animation;
- }
-}
-
-interface IScopeBarProps {
- defaultSelectedIndex: number;
- onChange?: (selectedIndex: number) => void;
- style?: Types.StyleRuleSet<Types.ViewStyle>;
- children: React.ReactNode;
-}
-
-interface IScopeBarState {
- selectedIndex: number;
-}
-
-export class ScopeBar extends Component<IScopeBarProps, IScopeBarState> {
- public static defaultProps: Partial<IScopeBarProps> = {
- defaultSelectedIndex: 0,
- };
-
- public state = {
- selectedIndex: 0,
- };
-
- constructor(props: IScopeBarProps) {
- super(props);
-
- this.state = {
- selectedIndex: props.defaultSelectedIndex,
- };
- }
-
- public render() {
- return (
- <View style={[styles.scopeBar.container, this.props.style]}>
- {React.Children.map(this.props.children, (child, index) => {
- if (React.isValidElement(child)) {
- return React.cloneElement(child, {
- ...(child.props || {}),
- selected: index === this.state.selectedIndex,
- onPress: this.makePressHandler(index),
- });
- } else {
- return undefined;
- }
- })}
- </View>
- );
- }
-
- public shouldComponentUpdate(nextProps: IScopeBarProps, nextState: IScopeBarState) {
- return (
- this.props.onChange !== nextProps.onChange ||
- this.props.style !== nextProps.style ||
- this.props.children !== nextProps.children ||
- this.state.selectedIndex !== nextState.selectedIndex
- );
- }
-
- private makePressHandler(index: number) {
- return () => {
- if (this.state.selectedIndex !== index) {
- this.setState({ selectedIndex: index }, () => {
- if (this.props.onChange) {
- this.props.onChange(index);
- }
- });
- }
- };
- }
-}
+ const { onScroll } = useContext(NavigationScrollContext);
-interface IScopeBarItemProps {
- children?: React.ReactText;
- selected?: boolean;
- onPress?: () => void;
-}
+ const handleScroll = useCallback((event: IScrollEvent) => {
+ onScroll(event);
+ props.onScroll?.(event);
+ }, []);
-export class ScopeBarItem extends Component<IScopeBarItemProps> {
- public state = {
- isHovered: false,
- };
-
- public render() {
- const hoverStyle = this.props.selected
- ? styles.scopeBar.item.selected
- : this.state.isHovered
- ? styles.scopeBar.item.hover
- : undefined;
-
- return (
- <Button
- style={[styles.scopeBar.item.base, hoverStyle]}
- onHoverStart={this.onHoverStart}
- onHoverEnd={this.onHoverEnd}
- onPress={this.props.onPress}>
- <Text style={styles.scopeBar.item.label}>{this.props.children}</Text>
- </Button>
- );
- }
-
- private onHoverStart = () => {
- this.setState({ isHovered: true });
- };
-
- private onHoverEnd = () => {
- this.setState({ isHovered: false });
- };
-}
-
-function NavigationBarSeparator() {
- return <View style={styles.navigationBar.separator} />;
-}
-
-interface INavigationBarProps {
- children?: React.ReactNode;
- alwaysDisplayBarTitle?: boolean;
-}
-
-export const NavigationBar = React.forwardRef(function NavigationBarT(
- props: INavigationBarProps,
- ref?: React.Ref<PrivateNavigationBar>,
-) {
return (
- <NavigationScrollContext.Consumer>
- {(context) => (
- <PrivateNavigationBar
- ref={ref}
- showsBarTitle={props.alwaysDisplayBarTitle || context.showsBarTitle}
- showsBarSeparator={context.showsBarSeparator}>
- {props.children}
- </PrivateNavigationBar>
- )}
- </NavigationScrollContext.Consumer>
+ <CustomScrollbars
+ ref={ref}
+ className={props.className}
+ fillContainer={props.fillContainer}
+ onScroll={handleScroll}>
+ {props.children}
+ </CustomScrollbars>
);
});
-interface IPrivateNavigationBarProps {
- showsBarSeparator: boolean;
- showsBarTitle: boolean;
- children?: React.ReactNode;
-}
-
-interface IPrivateNavigationBarState {
- titleAdjustment: number;
-}
-
-const PrivateTitleBarItemContext = React.createContext({
+const TitleBarItemContext = React.createContext({
titleAdjustment: 0,
visible: false,
- titleRef: React.createRef<PrivateTitleBarItem>(),
- measuringTextRef: React.createRef<Text>(),
+ get titleContainerRef(): React.RefObject<HTMLDivElement> {
+ throw Error('Missing TitleBarItemContext provider');
+ },
+ get measuringTitleRef(): React.RefObject<HTMLSpanElement> {
+ throw Error('Missing TitleBarItemContext provider');
+ },
});
-export function NavigationItems(props: { children: React.ReactNode }) {
- return <View style={styles.navigationItems}>{props.children}</View>;
+interface INavigationBarProps {
+ children?: React.ReactNode;
+ alwaysDisplayBarTitle?: boolean;
}
-class PrivateNavigationBar extends Component<
- IPrivateNavigationBarProps,
- IPrivateNavigationBarState
-> {
- public state: IPrivateNavigationBarState = {
- titleAdjustment: 0,
- };
-
- private titleViewRef = React.createRef<PrivateTitleBarItem>();
- private measuringTextRef = React.createRef<Text>();
+export const NavigationBar = function NavigationBarT(props: INavigationBarProps) {
+ const { showsBarSeparator, showsBarTitle } = useContext(NavigationScrollContext);
+ const [titleAdjustment, setTitleAdjustment] = useState(0);
- public shouldComponentUpdate(
- nextProps: IPrivateNavigationBarProps,
- nextState: IPrivateNavigationBarState,
- ) {
- return (
- this.props.children !== nextProps.children ||
- this.props.showsBarSeparator !== nextProps.showsBarSeparator ||
- this.props.showsBarTitle !== nextProps.showsBarTitle ||
- this.state.titleAdjustment !== nextState.titleAdjustment
- );
- }
+ const titleContainerRef = useRef() as React.RefObject<HTMLDivElement>;
+ const measuringTitleRef = useRef() as React.RefObject<HTMLSpanElement>;
+ const navigationBarRef = useRef() as React.RefObject<HTMLDivElement>;
- public render() {
- return (
- <View style={[styles.navigationBar.default, this.getPlatformStyle()]}>
- <View style={styles.navigationBar.wrapper} onLayout={this.onLayout}>
- <PrivateTitleBarItemContext.Provider
- value={{
- titleAdjustment: this.state.titleAdjustment,
- visible: this.props.showsBarTitle,
- titleRef: this.titleViewRef,
- measuringTextRef: this.measuringTextRef,
- }}>
- {this.props.children}
- </PrivateTitleBarItemContext.Provider>
- </View>
- {this.props.showsBarSeparator && <NavigationBarSeparator />}
- </View>
- );
- }
-
- private getPlatformStyle(): Types.ViewStyleRuleSet | undefined {
- switch (process.platform) {
- case 'darwin':
- return styles.navigationBar.darwin;
- case 'win32':
- return styles.navigationBar.win32;
- case 'linux':
- return styles.navigationBar.linux;
- default:
- return undefined;
- }
- }
-
- private onLayout = async (navBarContentLayout: Types.ViewOnLayoutEvent) => {
- const titleViewContainer = this.titleViewRef.current;
- const measuringText = this.measuringTextRef.current;
-
- if (titleViewContainer && measuringText) {
- const titleLayout = await UserInterface.measureLayoutRelativeToAncestor(
- titleViewContainer,
- this,
- );
- const textLayout = await UserInterface.measureLayoutRelativeToAncestor(measuringText, this);
+ useLayoutEffect(() => {
+ const titleContainerRect = titleContainerRef.current?.getBoundingClientRect();
+ const measuringTitleRect = measuringTitleRef.current?.getBoundingClientRect();
+ const navigationBarRect = navigationBarRef.current?.getBoundingClientRect();
+ if (titleContainerRect && measuringTitleRect && navigationBarRect) {
// calculate the width of the elements preceding the title view container
- const leadingSpace = titleLayout.x - navBarContentLayout.x;
+ const leadingSpace = titleContainerRect.x - navigationBarRect.x;
// calculate the width of the elements succeeding the title view container
- const trailingSpace = navBarContentLayout.width - titleLayout.width - leadingSpace;
+ const trailingSpace = navigationBarRect.width - titleContainerRect.width - leadingSpace;
// calculate the adjustment needed to center the title view within navigation bar
const titleAdjustment = Math.floor(trailingSpace - leadingSpace);
@@ -564,7 +169,9 @@ class PrivateNavigationBar extends Component<
// calculate the maximum possible adjustment that when applied should keep the text fully
// visible, unless the title container itself is smaller than the space needed to accommodate
// the text
- const maxTitleAdjustment = Math.floor(Math.max(titleLayout.width - textLayout.width, 0));
+ const maxTitleAdjustment = Math.floor(
+ Math.max(titleContainerRect.width - measuringTitleRect.width, 0),
+ );
// cap the adjustment to remain within the allowed bounds
const cappedTitleAdjustment = Math.min(
@@ -572,76 +179,71 @@ class PrivateNavigationBar extends Component<
maxTitleAdjustment,
);
- if (this.state.titleAdjustment !== cappedTitleAdjustment) {
- this.setState({
- titleAdjustment: cappedTitleAdjustment,
- });
- }
+ setTitleAdjustment(cappedTitleAdjustment);
}
- };
-}
+ });
-interface ITitleBarItemProps {
- children?: React.ReactText;
-}
-export function TitleBarItem(props: ITitleBarItemProps) {
return (
- <PrivateTitleBarItemContext.Consumer>
- {(context) => (
- <PrivateTitleBarItem
- titleAdjustment={context.titleAdjustment}
- visible={context.visible}
- ref={context.titleRef}
- measuringTextRef={context.measuringTextRef}>
+ <StyledNavigationBar>
+ <StyledNavigationBarWrapper ref={navigationBarRef}>
+ <TitleBarItemContext.Provider
+ value={{
+ titleAdjustment: titleAdjustment,
+ visible: props.alwaysDisplayBarTitle || showsBarTitle,
+ titleContainerRef,
+ measuringTitleRef,
+ }}>
{props.children}
- </PrivateTitleBarItem>
- )}
- </PrivateTitleBarItemContext.Consumer>
+ </TitleBarItemContext.Provider>
+ </StyledNavigationBarWrapper>
+ {showsBarSeparator && <StyledNavigationBarSeparator />}
+ </StyledNavigationBar>
);
+};
+
+interface ITitleBarItemProps {
+ children?: React.ReactText;
}
-const CloseBarItemIcon = styled(ImageView)({
- flex: 0,
- opacity: 0.6,
+export const TitleBarItem = React.memo(function TitleBarItemT(props: ITitleBarItemProps) {
+ const { measuringTitleRef, titleAdjustment, titleContainerRef, visible } = useContext(
+ TitleBarItemContext,
+ );
+
+ return (
+ <StyledTitleBarItemContainer ref={titleContainerRef}>
+ <StyledTitleBarItemLabel titleAdjustment={titleAdjustment} visible={visible}>
+ {props.children}
+ </StyledTitleBarItemLabel>
+
+ <StyledTitleBarItemMeasuringLabel titleAdjustment={0} ref={measuringTitleRef}>
+ {props.children}
+ </StyledTitleBarItemMeasuringLabel>
+ </StyledTitleBarItemContainer>
+ );
});
interface ICloseBarItemProps {
action: () => void;
}
-export class CloseBarItem extends Component<ICloseBarItemProps> {
- public render() {
- // Use the arrow down icon on Linux, to avoid confusion with the close button in the window
- // title bar.
- const iconName = process.platform === 'linux' ? 'icon-close-down' : 'icon-close';
-
- return (
- <Button style={[styles.closeBarItem.default]} onPress={this.props.action}>
- <CloseBarItemIcon height={24} width={24} source={iconName} />
- </Button>
- );
- }
+export function CloseBarItem(props: ICloseBarItemProps) {
+ // Use the arrow down icon on Linux, to avoid confusion with the close button in the window
+ // title bar.
+ const iconName = process.platform === 'linux' ? 'icon-close-down' : 'icon-close';
+ return <StyledCloseBarItemIcon height={24} width={24} source={iconName} onClick={props.action} />;
}
-const BackBarItemIcon = styled(ImageView)({
- opacity: 0.6,
- marginRight: '8px',
-});
-
interface IBackBarItemProps {
children?: React.ReactText;
action: () => void;
}
-export class BackBarItem extends Component<IBackBarItemProps> {
- public render() {
- return (
- <Button style={styles.backBarButton.default} onPress={this.props.action}>
- <View style={styles.backBarButton.content}>
- <BackBarItemIcon source="icon-back" />
- <Text style={styles.backBarButton.label}>{this.props.children}</Text>
- </View>
- </Button>
- );
- }
+export function BackBarItem(props: IBackBarItemProps) {
+ return (
+ <StyledBackBarItemButton onClick={props.action}>
+ <StyledBackBarItemIcon source="icon-back" />
+ <StyledBackBarItemLabel>{props.children}</StyledBackBarItemLabel>
+ </StyledBackBarItemButton>
+ );
}
diff --git a/gui/src/renderer/components/NavigationBarStyles.tsx b/gui/src/renderer/components/NavigationBarStyles.tsx
new file mode 100644
index 0000000000..e19f672e74
--- /dev/null
+++ b/gui/src/renderer/components/NavigationBarStyles.tsx
@@ -0,0 +1,96 @@
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import ImageView from './ImageView';
+
+export const StyledNavigationBarSeparator = styled.div({
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: '1px',
+});
+
+export const StyledNavigationItems = styled.div({
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'row',
+});
+
+export const StyledNavigationBar = styled.div({
+ flex: 0,
+ padding: '12px',
+ paddingTop: process.platform === 'darwin' ? '24px' : '12px',
+});
+
+export const StyledNavigationBarWrapper = styled.div({
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ overflow: 'hidden',
+});
+
+export const StyledTitleBarItemContainer = styled.div({
+ display: 'flex',
+ flex: 1,
+ minWidth: 0,
+ flexDirection: 'column',
+ justifyContent: 'center',
+});
+
+interface ITitleBarItemLabelProps {
+ titleAdjustment: number;
+ visible?: boolean;
+}
+
+export const StyledTitleBarItemLabel = styled.span({}, (props: ITitleBarItemLabelProps) => ({
+ fontFamily: 'Open Sans',
+ fontSize: '16px',
+ fontWeight: 600,
+ lineHeight: '22px',
+ color: colors.white,
+ padding: '0 5px',
+ textAlign: 'center',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ marginLeft: props.titleAdjustment + 'px',
+ opacity: props.visible ? 1 : 0,
+ transition: 'opacity 250ms ease-in-out',
+}));
+
+export const StyledTitleBarItemMeasuringLabel = styled(StyledTitleBarItemLabel)({
+ position: 'absolute',
+ opacity: 0,
+});
+
+export const StyledCloseBarItemIcon = styled(ImageView)({
+ flex: 0,
+ opacity: 0.6,
+});
+
+export const StyledBackBarItemButton = styled.button({
+ position: 'relative',
+ borderWidth: 0,
+ padding: 0,
+ margin: 0,
+ cursor: 'default',
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: 'transparent',
+ zIndex: 1,
+});
+
+export const StyledBackBarItemIcon = styled(ImageView)({
+ opacity: 0.6,
+ marginRight: '8px',
+});
+
+export const StyledBackBarItemLabel = styled.span({
+ fontFamily: 'Open Sans',
+ fontSize: '13px',
+ fontWeight: 600,
+ color: colors.white60,
+ whiteSpace: 'nowrap',
+});
diff --git a/gui/src/renderer/components/ScopeBar.tsx b/gui/src/renderer/components/ScopeBar.tsx
new file mode 100644
index 0000000000..dbc5739b2c
--- /dev/null
+++ b/gui/src/renderer/components/ScopeBar.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import { smallText } from './common-styles';
+
+const StyledScopeBar = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ backgroundColor: colors.blue40,
+ borderRadius: '13px',
+ overflow: 'hidden',
+});
+
+interface IScopeBarProps {
+ defaultSelectedIndex?: number;
+ onChange?: (selectedIndex: number) => void;
+ className?: string;
+ children: React.ReactNode;
+}
+
+export function ScopeBar(props: IScopeBarProps) {
+ const [selectedIndex, setSelectedIndex] = useState(props.defaultSelectedIndex ?? 0);
+
+ const onClick = useCallback((index: number) => setSelectedIndex(index), []);
+ useEffect(() => {
+ props.onChange?.(selectedIndex);
+ }, [selectedIndex]);
+
+ const children = React.Children.map(props.children, (child, index) => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, {
+ selected: index === selectedIndex,
+ onClick,
+ index,
+ });
+ } else {
+ return undefined;
+ }
+ });
+
+ return <StyledScopeBar className={props.className}>{children}</StyledScopeBar>;
+}
+
+const StyledScopeBarItem = styled.button(smallText, (props: { selected?: boolean }) => ({
+ cursor: 'default',
+ flex: 1,
+ flexBasis: 0,
+ padding: '4px 8px',
+ color: colors.white,
+ textAlign: 'center',
+ border: 'none',
+ backgroundColor: props.selected ? colors.green : 'transparent',
+ ':hover': {
+ backgroundColor: props.selected ? colors.green : colors.blue40,
+ },
+}));
+
+interface IScopeBarItemProps {
+ index?: number;
+ selected?: boolean;
+ onClick?: (index: number) => void;
+ children?: React.ReactNode;
+}
+
+export function ScopeBarItem(props: IScopeBarItemProps) {
+ const onClick = useCallback(() => {
+ if (props.index !== undefined) {
+ props.onClick?.(props.index);
+ }
+ }, [props.onClick, props.index]);
+
+ return props.index !== undefined ? (
+ <StyledScopeBarItem selected={props.selected} onClick={onClick}>
+ {props.children}
+ </StyledScopeBarItem>
+ ) : null;
+}
diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx
index 1a294a05ba..58f4a7b922 100644
--- a/gui/src/renderer/components/SelectLanguage.tsx
+++ b/gui/src/renderer/components/SelectLanguage.tsx
@@ -36,12 +36,12 @@ const styles = {
container: Styles.createViewStyle({
flex: 1,
}),
- // plain CSS style
- scrollview: {
- flex: 1,
- },
};
+const StyledNavigationScrollbars = styled(NavigationScrollbars)({
+ flex: 1,
+});
+
const StyledSelector = (styled(Selector)({
marginBottom: 0,
}) as unknown) as new <T>() => Selector<T>;
@@ -88,7 +88,7 @@ export default class SelectLanguage extends Component<IProps, IState> {
</NavigationBar>
<View style={styles.container}>
- <NavigationScrollbars style={styles.scrollview}>
+ <StyledNavigationScrollbars>
<SettingsHeader>
<HeaderTitle>
{messages.pgettext('select-language-nav', 'Select language')}
@@ -101,7 +101,7 @@ export default class SelectLanguage extends Component<IProps, IState> {
onSelect={this.props.setPreferredLocale}
selectedCellRef={this.selectedCellRef}
/>
- </NavigationScrollbars>
+ </StyledNavigationScrollbars>
</View>
</NavigationContainer>
</View>
diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx
index de2f98128a..bb416a41ae 100644
--- a/gui/src/renderer/components/SelectLocation.tsx
+++ b/gui/src/renderer/components/SelectLocation.tsx
@@ -1,4 +1,4 @@
-import * as React from 'react';
+import React from 'react';
import ReactDOM from 'react-dom';
import { Component, View } from 'reactxp';
import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types';
@@ -16,11 +16,10 @@ import {
NavigationContainer,
NavigationItems,
NavigationScrollbars,
- ScopeBar,
- ScopeBarItem,
TitleBarItem,
} from './NavigationBar';
-import styles from './SelectLocationStyles';
+import { ScopeBarItem } from './ScopeBar';
+import styles, { StyledScopeBar } from './SelectLocationStyles';
import { HeaderSubTitle } from './SettingsHeader';
interface IProps {
@@ -113,8 +112,7 @@ export default class SelectLocation extends Component<IProps> {
)}
</HeaderSubTitle>
{this.props.allowBridgeSelection && (
- <ScopeBar
- style={styles.scopeBar}
+ <StyledScopeBar
defaultSelectedIndex={this.props.locationScope}
onChange={this.props.onChangeLocationScope}>
<ScopeBarItem>
@@ -123,7 +121,7 @@ export default class SelectLocation extends Component<IProps> {
<ScopeBarItem>
{messages.pgettext('select-location-nav', 'Exit')}
</ScopeBarItem>
- </ScopeBar>
+ </StyledScopeBar>
)}
</View>
</NavigationBar>
diff --git a/gui/src/renderer/components/SelectLocationStyles.tsx b/gui/src/renderer/components/SelectLocationStyles.tsx
index ad6c116436..e9e9b5a944 100644
--- a/gui/src/renderer/components/SelectLocationStyles.tsx
+++ b/gui/src/renderer/components/SelectLocationStyles.tsx
@@ -1,5 +1,11 @@
import { Styles } from 'reactxp';
+import styled from 'styled-components';
import { colors } from '../../config.json';
+import { ScopeBar } from './ScopeBar';
+
+export const StyledScopeBar = styled(ScopeBar)({
+ marginTop: '8px',
+});
export default {
select_location: Styles.createViewStyle({
@@ -17,9 +23,6 @@ export default {
marginTop: 8,
paddingHorizontal: 4,
}),
- scopeBar: Styles.createViewStyle({
- marginTop: 8,
- }),
selectedCell: Styles.createViewStyle({
backgroundColor: colors.green,
}),
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 53bc8aedc8..dd3fef403c 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -11,11 +11,10 @@ import {
NavigationBar,
NavigationContainer,
NavigationItems,
- NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import styles, { CellIcon, OutOfTimeSubText } from './SettingsStyles';
+import styles, { CellIcon, OutOfTimeSubText, StyledNavigationScrollbars } from './SettingsStyles';
import { LoginState } from '../redux/account/reducers';
@@ -60,7 +59,7 @@ export default class Settings extends Component<IProps> {
</NavigationBar>
<View style={styles.container}>
- <NavigationScrollbars style={styles.scrollview}>
+ <StyledNavigationScrollbars>
<View style={styles.content}>
{showLargeTitle && (
<SettingsHeader>
@@ -74,7 +73,7 @@ export default class Settings extends Component<IProps> {
</View>
{this.renderQuitButton()}
</View>
- </NavigationScrollbars>
+ </StyledNavigationScrollbars>
</View>
</NavigationContainer>
</View>
diff --git a/gui/src/renderer/components/SettingsHeader.tsx b/gui/src/renderer/components/SettingsHeader.tsx
index 4cbd1c2077..61caeae9b3 100644
--- a/gui/src/renderer/components/SettingsHeader.tsx
+++ b/gui/src/renderer/components/SettingsHeader.tsx
@@ -1,76 +1,31 @@
import * as React from 'react';
-import { Component, Styles, Text, Types, View } from 'reactxp';
-import { colors } from '../../config.json';
+import styled from 'styled-components';
+import { bigText, smallText } from './common-styles';
-const styles = {
- header: {
- default: Styles.createViewStyle({
- flex: 0,
- paddingTop: 2,
- paddingRight: 20,
- paddingLeft: 20,
- paddingBottom: 20,
- }),
- },
- // TODO: Use bigText in comonStyles when converted from ReactXP
- title: Styles.createTextStyle({
- fontFamily: 'DINPro',
- fontSize: 30,
- fontWeight: '900',
- lineHeight: 34,
- color: colors.white,
- }),
- // TODO: Use smallText in comonStyles when converted from ReactXP
- subtitle: Styles.createTextStyle({
- fontFamily: 'Open Sans',
- fontSize: 13,
- fontWeight: '600',
- overflow: 'visible',
- color: colors.white80,
- lineHeight: 20,
- }),
- spacer: Styles.createViewStyle({
- height: 8,
- }),
-};
-
-interface ISettingsHeaderProps {
- style?: Types.ViewStyleRuleSet;
-}
+export const Container = styled.div({
+ flex: 0,
+ padding: '2px 20px 20px',
+});
-interface ISettingsTextProps {
- style?: Types.TextStyleRuleSet;
-}
+export const ContentWrapper = styled.div({
+ ':not(:first-child)': {
+ paddingTop: '8px',
+ },
+});
-export default class SettingsHeader extends Component<ISettingsHeaderProps> {
- public render() {
- return (
- <View style={[styles.header.default, this.props.style]}>
- {React.Children.map(this.props.children, (child, index) => {
- if (React.isValidElement(child) && index > 0) {
- return (
- <React.Fragment>
- <View style={styles.spacer} />
- {child}
- </React.Fragment>
- );
- } else {
- return child;
- }
- })}
- </View>
- );
- }
-}
+export const HeaderTitle = styled.span(bigText);
+export const HeaderSubTitle = styled.span(smallText);
-export class HeaderTitle extends Component<ISettingsTextProps> {
- public render() {
- return <Text style={[styles.title, this.props.style]}>{this.props.children}</Text>;
- }
+interface ISettingsHeaderProps {
+ children?: React.ReactNode;
}
-export class HeaderSubTitle extends Component<ISettingsTextProps> {
- public render() {
- return <Text style={[styles.subtitle, this.props.style]}>{this.props.children}</Text>;
- }
+export default function SettingsHeader(props: ISettingsHeaderProps) {
+ return (
+ <Container>
+ {React.Children.map(props.children, (child) => {
+ return React.isValidElement(child) ? <ContentWrapper>{child}</ContentWrapper> : undefined;
+ })}
+ </Container>
+ );
}
diff --git a/gui/src/renderer/components/SettingsStyles.tsx b/gui/src/renderer/components/SettingsStyles.tsx
index 806afb45ef..e971273f14 100644
--- a/gui/src/renderer/components/SettingsStyles.tsx
+++ b/gui/src/renderer/components/SettingsStyles.tsx
@@ -2,6 +2,7 @@ import { Styles } from 'reactxp';
import styled from 'styled-components';
import { colors } from '../../config.json';
import * as Cell from './Cell';
+import { NavigationScrollbars } from './NavigationBar';
export const OutOfTimeSubText = styled(Cell.SubText)((props: { isOutOfTime: boolean }) => ({
color: props.isOutOfTime ? colors.red : undefined,
@@ -11,6 +12,10 @@ export const CellIcon = styled(Cell.UntintedIcon)({
marginRight: '8px',
});
+export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
+ flex: 1,
+});
+
export default {
settings: Styles.createViewStyle({
backgroundColor: colors.darkBlue,
@@ -26,10 +31,6 @@ export default {
justifyContent: 'space-between',
overflow: 'visible',
}),
- // plain CSS style
- scrollview: {
- flex: 1,
- },
cellSpacer: Styles.createViewStyle({
height: 20,
flex: 0,
diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx
index 0f8d3407f4..6214b23ea5 100644
--- a/gui/src/renderer/components/WireguardKeys.tsx
+++ b/gui/src/renderer/components/WireguardKeys.tsx
@@ -15,11 +15,10 @@ import {
NavigationBar,
NavigationContainer,
NavigationItems,
- NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import styles from './WireguardKeysStyles';
+import styles, { StyledNavigationScrollbars } from './WireguardKeysStyles';
export interface IProps {
keyState: WgKeyState;
@@ -91,7 +90,7 @@ export default class WireguardKeys extends Component<IProps, IState> {
</NavigationBar>
<View style={styles.wgkeys__container}>
- <NavigationScrollbars style={styles.wgkeys__scrollview} fillContainer>
+ <StyledNavigationScrollbars fillContainer>
<View style={styles.wgkeys__content}>
<SettingsHeader>
<HeaderTitle>
@@ -144,7 +143,7 @@ export default class WireguardKeys extends Component<IProps, IState> {
</AppButton.BlockingButton>
</View>
</View>
- </NavigationScrollbars>
+ </StyledNavigationScrollbars>
</View>
</NavigationContainer>
</View>
diff --git a/gui/src/renderer/components/WireguardKeysStyles.tsx b/gui/src/renderer/components/WireguardKeysStyles.tsx
index b5b18cc6cd..7176f560af 100644
--- a/gui/src/renderer/components/WireguardKeysStyles.tsx
+++ b/gui/src/renderer/components/WireguardKeysStyles.tsx
@@ -1,5 +1,11 @@
import { Styles } from 'reactxp';
+import styled from 'styled-components';
import { colors } from '../../config.json';
+import { NavigationScrollbars } from './NavigationBar';
+
+export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
+ flex: 1,
+});
export default {
wgkeys: Styles.createViewStyle({
@@ -10,10 +16,6 @@ export default {
flexDirection: 'column',
flex: 1,
}),
- // plain CSS style
- wgkeys__scrollview: {
- flex: 1,
- },
wgkeys__content: Styles.createViewStyle({
// ReactXP don't allow setting 'minHeight' and don't allow percentages. This will work well
// without the '@ts-ignore' when moving away from ReactXP.