diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-07-14 13:17:48 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-07-15 12:01:23 +0200 |
| commit | e0b3667cea8cc465f3072636cf8138f3a3ec8caa (patch) | |
| tree | acd88fdc38789f98026327ed83f53c7b681d3f15 /gui/src/renderer | |
| parent | bd4f591f5fc56f383c8dbd437c748e36559cbed8 (diff) | |
| download | mullvadvpn-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.tsx | 11 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettingsStyles.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/CustomScrollbars.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBar.tsx | 634 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBarStyles.tsx | 96 | ||||
| -rw-r--r-- | gui/src/renderer/components/ScopeBar.tsx | 77 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLanguage.tsx | 12 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 12 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocationStyles.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/Settings.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/components/SettingsHeader.tsx | 91 | ||||
| -rw-r--r-- | gui/src/renderer/components/SettingsStyles.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeysStyles.tsx | 10 |
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. |
