diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-02-21 14:54:57 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-02-23 10:40:06 +0100 |
| commit | c284ffc798a9a04b53e32f54104a3edbaeda7b17 (patch) | |
| tree | 47bdf5ce0ad83715c8aeb44f75ba715a2d816446 /gui/src | |
| parent | e303176af375ee90c0eb2348352d6c66560723cd (diff) | |
| download | mullvadvpn-c284ffc798a9a04b53e32f54104a3edbaeda7b17.tar.xz mullvadvpn-c284ffc798a9a04b53e32f54104a3edbaeda7b17.zip | |
Use back actions in components
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/components/Account.tsx | 132 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 251 | ||||
| -rw-r--r-- | gui/src/renderer/components/FilterByProvider.tsx | 76 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 64 | ||||
| -rw-r--r-- | gui/src/renderer/components/OpenVPNSettings.tsx | 263 | ||||
| -rw-r--r-- | gui/src/renderer/components/Preferences.tsx | 524 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLanguage.tsx | 69 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 187 | ||||
| -rw-r--r-- | gui/src/renderer/components/Settings.tsx | 74 | ||||
| -rw-r--r-- | gui/src/renderer/components/SplitTunnelingSettings.tsx | 60 | ||||
| -rw-r--r-- | gui/src/renderer/components/Support.tsx | 46 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 158 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardSettings.tsx | 309 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Input.tsx | 116 |
14 files changed, 1176 insertions, 1153 deletions
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx index 69dd014caf..833a06d1c2 100644 --- a/gui/src/renderer/components/Account.tsx +++ b/gui/src/renderer/components/Account.tsx @@ -17,10 +17,11 @@ import AccountTokenLabel from './AccountTokenLabel'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import { Layout } from './Layout'; -import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; +import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { AccountToken } from '../../shared/daemon-rpc-types'; +import { BackAction } from './KeyboardNavigation'; interface IProps { accountToken?: AccountToken; @@ -40,75 +41,78 @@ export default class Account extends React.Component<IProps> { public render() { return ( - <Layout> - <StyledContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('account-view', 'Account') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('account-view', 'Account') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <AccountContainer> - <SettingsHeader> - <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle> - </SettingsHeader> + <AccountContainer> + <SettingsHeader> + <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle> + </SettingsHeader> - <AccountRows> - <AccountRow> - <AccountRowLabel> - {messages.pgettext('account-view', 'Account number')} - </AccountRowLabel> - <AccountRowValue - as={AccountTokenLabel} - accountToken={this.props.accountToken || ''} - /> - </AccountRow> + <AccountRows> + <AccountRow> + <AccountRowLabel> + {messages.pgettext('account-view', 'Account number')} + </AccountRowLabel> + <AccountRowValue + as={AccountTokenLabel} + accountToken={this.props.accountToken || ''} + /> + </AccountRow> - <AccountRow> - <AccountRowLabel>{messages.pgettext('account-view', 'Paid until')}</AccountRowLabel> - <FormattedAccountExpiry - expiry={this.props.accountExpiry} - locale={this.props.expiryLocale} - /> - </AccountRow> - </AccountRows> + <AccountRow> + <AccountRowLabel> + {messages.pgettext('account-view', 'Paid until')} + </AccountRowLabel> + <FormattedAccountExpiry + expiry={this.props.accountExpiry} + locale={this.props.expiryLocale} + /> + </AccountRow> + </AccountRows> - <AccountFooter> - <AppButton.BlockingButton - disabled={this.props.isOffline} - onClick={this.props.onBuyMore}> - <AriaDescriptionGroup> - <AriaDescribed> - <StyledBuyCreditButton> - <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label> - <AriaDescription> - <AppButton.Icon - source="icon-extLink" - height={16} - width={16} - aria-label={messages.pgettext('accessibility', 'Opens externally')} - /> - </AriaDescription> - </StyledBuyCreditButton> - </AriaDescribed> - </AriaDescriptionGroup> - </AppButton.BlockingButton> + <AccountFooter> + <AppButton.BlockingButton + disabled={this.props.isOffline} + onClick={this.props.onBuyMore}> + <AriaDescriptionGroup> + <AriaDescribed> + <StyledBuyCreditButton> + <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label> + <AriaDescription> + <AppButton.Icon + source="icon-extLink" + height={16} + width={16} + aria-label={messages.pgettext('accessibility', 'Opens externally')} + /> + </AriaDescription> + </StyledBuyCreditButton> + </AriaDescribed> + </AriaDescriptionGroup> + </AppButton.BlockingButton> - <StyledRedeemVoucherButton /> + <StyledRedeemVoucherButton /> - <AppButton.RedButton onClick={this.props.onLogout}> - {messages.pgettext('account-view', 'Log out')} - </AppButton.RedButton> - </AccountFooter> - </AccountContainer> - </StyledContainer> - </Layout> + <AppButton.RedButton onClick={this.props.onLogout}> + {messages.pgettext('account-view', 'Log out')} + </AppButton.RedButton> + </AccountFooter> + </AccountContainer> + </StyledContainer> + </Layout> + </BackAction> ); } } diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index d811f04a06..998996e1ab 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -16,16 +16,11 @@ import * as Cell from './cell'; import CustomDnsSettings from './CustomDnsSettings'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; -import { - BackBarItem, - NavigationBar, - NavigationContainer, - NavigationItems, - TitleBarItem, -} from './NavigationBar'; +import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar'; import { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import Switch from './Switch'; +import { BackAction } from './KeyboardNavigation'; type OptionalTunnelProtocol = TunnelProtocol | undefined; @@ -58,137 +53,143 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set'; return ( - <Layout> - <SettingsContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('advanced-settings-nav', 'Advanced') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <SettingsContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('advanced-settings-nav', 'Advanced') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('advanced-settings-view', 'Advanced')} + </HeaderTitle> + </SettingsHeader> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Enable IPv6')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.enableIpv6} + onChange={this.props.setEnableIpv6} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'advanced-settings-view', + 'Enable IPv6 communication through the tunnel.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <StyledNavigationScrollbars> - <SettingsHeader> - <HeaderTitle>{messages.pgettext('advanced-settings-view', 'Advanced')}</HeaderTitle> - </SettingsHeader> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Always require VPN')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + ref={this.blockWhenDisconnectedRef} + isOn={this.props.blockWhenDisconnected} + onChange={this.setBlockWhenDisconnected} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'advanced-settings-view', + 'If you disconnect or quit the app, this setting will block your internet.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('advanced-settings-view', 'Enable IPv6')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={this.props.enableIpv6} onChange={this.props.setEnableIpv6} /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'advanced-settings-view', - 'Enable IPv6 communication through the tunnel.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + {(window.env.platform === 'linux' || window.env.platform === 'win32') && ( + <Cell.CellButtonGroup> + <Cell.CellButton onClick={this.props.onViewSplitTunneling}> + <Cell.Label> + {messages.pgettext('advanced-settings-view', 'Split tunneling')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </Cell.CellButtonGroup> + )} - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('advanced-settings-view', 'Always require VPN')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - ref={this.blockWhenDisconnectedRef} - isOn={this.props.blockWhenDisconnected} - onChange={this.setBlockWhenDisconnected} + <AriaInputGroup> + <StyledTunnelProtocolContainer> + <StyledSelectorForFooter + title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')} + values={this.tunnelProtocolItems(hasWireguardKey)} + value={this.props.tunnelProtocol} + onSelect={this.onSelectTunnelProtocol} /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'advanced-settings-view', - 'If you disconnect or quit the app, this setting will block your internet.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + {!hasWireguardKey && ( + <StyledNoWireguardKeyErrorContainer> + <AriaDescription> + <StyledNoWireguardKeyError> + {messages.pgettext( + 'advanced-settings-view', + 'To enable WireGuard, generate a key under the "WireGuard key" setting below.', + )} + </StyledNoWireguardKeyError> + </AriaDescription> + </StyledNoWireguardKeyErrorContainer> + )} + </StyledTunnelProtocolContainer> + </AriaInputGroup> - {(window.env.platform === 'linux' || window.env.platform === 'win32') && ( <Cell.CellButtonGroup> - <Cell.CellButton onClick={this.props.onViewSplitTunneling}> + <Cell.CellButton + onClick={this.props.onViewWireguardSettings} + disabled={this.props.tunnelProtocol === 'openvpn'}> <Cell.Label> - {messages.pgettext('advanced-settings-view', 'Split tunneling')} + {messages.pgettext('advanced-settings-view', 'WireGuard settings')} </Cell.Label> <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> - </Cell.CellButtonGroup> - )} - - <AriaInputGroup> - <StyledTunnelProtocolContainer> - <StyledSelectorForFooter - title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')} - values={this.tunnelProtocolItems(hasWireguardKey)} - value={this.props.tunnelProtocol} - onSelect={this.onSelectTunnelProtocol} - /> - {!hasWireguardKey && ( - <StyledNoWireguardKeyErrorContainer> - <AriaDescription> - <StyledNoWireguardKeyError> - {messages.pgettext( - 'advanced-settings-view', - 'To enable WireGuard, generate a key under the "WireGuard key" setting below.', - )} - </StyledNoWireguardKeyError> - </AriaDescription> - </StyledNoWireguardKeyErrorContainer> - )} - </StyledTunnelProtocolContainer> - </AriaInputGroup> - - <Cell.CellButtonGroup> - <Cell.CellButton - onClick={this.props.onViewWireguardSettings} - disabled={this.props.tunnelProtocol === 'openvpn'}> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'WireGuard settings')} - </Cell.Label> - <Cell.Icon height={12} width={7} source="icon-chevron" /> - </Cell.CellButton> - <Cell.CellButton - onClick={this.props.onViewOpenVpnSettings} - disabled={this.props.tunnelProtocol === 'wireguard'}> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'OpenVPN settings')} - </Cell.Label> - <Cell.Icon height={12} width={7} source="icon-chevron" /> - </Cell.CellButton> - </Cell.CellButtonGroup> + <Cell.CellButton + onClick={this.props.onViewOpenVpnSettings} + disabled={this.props.tunnelProtocol === 'wireguard'}> + <Cell.Label> + {messages.pgettext('advanced-settings-view', 'OpenVPN settings')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </Cell.CellButtonGroup> - <CustomDnsSettings /> - </StyledNavigationScrollbars> - </NavigationContainer> - </SettingsContainer> + <CustomDnsSettings /> + </StyledNavigationScrollbars> + </NavigationContainer> + </SettingsContainer> - {this.renderConfirmBlockWhenDisconnectedAlert()} - </Layout> + {this.renderConfirmBlockWhenDisconnectedAlert()} + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/FilterByProvider.tsx b/gui/src/renderer/components/FilterByProvider.tsx index cf5789ab65..24aba7a097 100644 --- a/gui/src/renderer/components/FilterByProvider.tsx +++ b/gui/src/renderer/components/FilterByProvider.tsx @@ -8,9 +8,9 @@ import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { normalText } from './common-styles'; import ImageView from './ImageView'; +import { BackAction } from './KeyboardNavigation'; import { Container, Layout } from './Layout'; import { - BackBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -106,39 +106,47 @@ export default function FilterByProvider() { }, [providers, history, updateRelaySettings, selectionStatus]); return ( - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar alwaysDisplayBarTitle={true}> - <NavigationItems> - <BackBarItem action={history.pop} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('filter-by-provider-nav', 'Filter by provider') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> - <StyledNavigationScrollbars> - <ProviderRow - provider={messages.pgettext('filter-by-provider-view', 'All providers')} - bold - checked={selectionStatus === Selection.all} - onCheck={toggleAll} - /> - {Object.entries(providers).map(([provider, checked]) => ( - <ProviderRow key={provider} provider={provider} checked={checked} onCheck={onCheck} /> - ))} - </StyledNavigationScrollbars> - <StyledFooter> - <AppButton.GreenButton disabled={selectionStatus === Selection.none} onClick={onApply}> - {messages.gettext('Apply')} - </AppButton.GreenButton> - </StyledFooter> - </NavigationContainer> - </StyledContainer> - </Layout> + <BackAction action={history.pop}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar alwaysDisplayBarTitle={true}> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('filter-by-provider-nav', 'Filter by provider') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + <StyledNavigationScrollbars> + <ProviderRow + provider={messages.pgettext('filter-by-provider-view', 'All providers')} + bold + checked={selectionStatus === Selection.all} + onCheck={toggleAll} + /> + {Object.entries(providers).map(([provider, checked]) => ( + <ProviderRow + key={provider} + provider={provider} + checked={checked} + onCheck={onCheck} + /> + ))} + </StyledNavigationScrollbars> + <StyledFooter> + <AppButton.GreenButton + disabled={selectionStatus === Selection.none} + onClick={onApply}> + {messages.gettext('Apply')} + </AppButton.GreenButton> + </StyledFooter> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index 6b8a9a67dc..c9cb5e6c8b 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -6,6 +6,7 @@ import log from '../../shared/logging'; import CustomScrollbars from './CustomScrollbars'; import { tinyText } from './common-styles'; import ImageView from './ImageView'; +import { BackAction } from './KeyboardNavigation'; const MODAL_CONTAINER_ID = 'modal-container'; @@ -205,9 +206,6 @@ class ModalAlertImpl extends React.Component<IModalAlertImplProps, IModalAlertSt public componentDidMount() { this.props.setActiveModal(true); - // The `true` argument specifies that the event should be dispatched in the capture phase. This - // makes sure that this component catches the event before the escape hatch. - document.addEventListener('keydown', this.handleKeyPress, true); const modalContainer = document.getElementById(MODAL_CONTAINER_ID); if (modalContainer) { @@ -222,7 +220,6 @@ class ModalAlertImpl extends React.Component<IModalAlertImplProps, IModalAlertSt public componentWillUnmount() { this.props.setActiveModal(false); - document.removeEventListener('keydown', this.handleKeyPress, true); const modalContainer = document.getElementById(MODAL_CONTAINER_ID); modalContainer?.removeChild(this.element); @@ -234,33 +231,39 @@ class ModalAlertImpl extends React.Component<IModalAlertImplProps, IModalAlertSt private renderModal() { return ( - <ModalBackground visible={this.state.visible && !this.props.closing}> - <ModalAlertContainer> - <StyledModalAlert - ref={this.modalRef} - tabIndex={-1} - role="dialog" - aria-modal - visible={this.state.visible} - closing={this.props.closing} - onTransitionEnd={this.onTransitionEnd}> - <StyledCustomScrollbars> - {this.props.type && ( - <ModalAlertIcon>{this.renderTypeIcon(this.props.type)}</ModalAlertIcon> - )} - {this.props.message && <ModalMessage>{this.props.message}</ModalMessage>} - {this.props.children} - </StyledCustomScrollbars> + <BackAction action={this.close}> + <ModalBackground visible={this.state.visible && !this.props.closing}> + <ModalAlertContainer> + <StyledModalAlert + ref={this.modalRef} + tabIndex={-1} + role="dialog" + aria-modal + visible={this.state.visible} + closing={this.props.closing} + onTransitionEnd={this.onTransitionEnd}> + <StyledCustomScrollbars> + {this.props.type && ( + <ModalAlertIcon>{this.renderTypeIcon(this.props.type)}</ModalAlertIcon> + )} + {this.props.message && <ModalMessage>{this.props.message}</ModalMessage>} + {this.props.children} + </StyledCustomScrollbars> - {this.props.buttons.map((button, index) => ( - <ModalAlertButtonContainer key={index}>{button}</ModalAlertButtonContainer> - ))} - </StyledModalAlert> - </ModalAlertContainer> - </ModalBackground> + {this.props.buttons.map((button, index) => ( + <ModalAlertButtonContainer key={index}>{button}</ModalAlertButtonContainer> + ))} + </StyledModalAlert> + </ModalAlertContainer> + </ModalBackground> + </BackAction> ); } + private close = () => { + this.props.close?.(); + }; + private renderTypeIcon(type: ModalAlertType) { let source = ''; let color = ''; @@ -283,13 +286,6 @@ class ModalAlertImpl extends React.Component<IModalAlertImplProps, IModalAlertSt ); } - private handleKeyPress = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - event.stopPropagation(); - this.props.close?.(); - } - }; - private onTransitionEnd = (event: React.TransitionEvent<HTMLDivElement>) => { if (event.target === this.modalRef.current) { this.props.onTransitionEnd(); diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx index 55157a9bfe..d32a60ed36 100644 --- a/gui/src/renderer/components/OpenVPNSettings.tsx +++ b/gui/src/renderer/components/OpenVPNSettings.tsx @@ -9,7 +9,6 @@ import * as Cell from './cell'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { - BackBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -19,6 +18,7 @@ import { import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { formatMarkdown } from '../markdown-formatter'; +import { BackAction } from './KeyboardNavigation'; const MIN_MSSFIX_VALUE = 1000; const MAX_MSSFIX_VALUE = 1450; @@ -86,146 +86,147 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> { public render() { return ( - <Layout> - <SettingsContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('openvpn-settings-nav', 'OpenVPN settings') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <SettingsContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('openvpn-settings-nav', 'OpenVPN settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('openvpn-settings-view', 'OpenVPN settings')} - </HeaderTitle> - </SettingsHeader> + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('openvpn-settings-view', 'OpenVPN settings')} + </HeaderTitle> + </SettingsHeader> - <StyledSelectorContainer> - <AriaInputGroup> - <Selector - title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} - values={this.protocolItems(this.props.bridgeState !== 'on')} - value={this.props.openvpn.protocol} - onSelect={this.onSelectOpenvpnProtocol} - hasFooter={this.props.bridgeState === 'on'} - /> - {this.props.bridgeState === 'on' && ( - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {formatMarkdown( - // TRANSLATORS: This is used to instruct users how to make UDP mode - // TRANSLATORS: available. - messages.pgettext( - 'openvpn-settings-view', - 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.', - ), - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - )} - </AriaInputGroup> - </StyledSelectorContainer> - - <StyledSelectorContainer> - <AriaInputGroup> - {this.props.openvpn.protocol ? ( + <StyledSelectorContainer> + <AriaInputGroup> <Selector - title={sprintf( - // TRANSLATORS: The title for the port selector section. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - messages.pgettext('openvpn-settings-view', '%(portType)s port'), - { - portType: this.props.openvpn.protocol.toUpperCase(), - }, - )} - values={this.portItems[this.props.openvpn.protocol]} - value={this.props.openvpn.port} - onSelect={this.onSelectOpenVpnPort} + title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} + values={this.protocolItems(this.props.bridgeState !== 'on')} + value={this.props.openvpn.protocol} + onSelect={this.onSelectOpenvpnProtocol} + hasFooter={this.props.bridgeState === 'on'} /> - ) : undefined} - </AriaInputGroup> - </StyledSelectorContainer> + {this.props.bridgeState === 'on' && ( + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {formatMarkdown( + // TRANSLATORS: This is used to instruct users how to make UDP mode + // TRANSLATORS: available. + messages.pgettext( + 'openvpn-settings-view', + 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.', + ), + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + )} + </AriaInputGroup> + </StyledSelectorContainer> - <AriaInputGroup> <StyledSelectorContainer> - <Selector - title={ - // TRANSLATORS: The title for the shadowsocks bridge selector section. - messages.pgettext('openvpn-settings-view', 'Bridge mode') - } - values={this.bridgeStateItems( - this.props.bridgeModeAvailablity === BridgeModeAvailability.available, - )} - value={this.props.bridgeState} - onSelect={this.onSelectBridgeState} - hasFooter - /> + <AriaInputGroup> + {this.props.openvpn.protocol ? ( + <Selector + title={sprintf( + // TRANSLATORS: The title for the port selector section. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) + messages.pgettext('openvpn-settings-view', '%(portType)s port'), + { + portType: this.props.openvpn.protocol.toUpperCase(), + }, + )} + values={this.portItems[this.props.openvpn.protocol]} + value={this.props.openvpn.port} + onSelect={this.onSelectOpenVpnPort} + /> + ) : undefined} + </AriaInputGroup> </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('openvpn-settings-view', 'Mssfix')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.AutoSizingTextInput - value={this.props.mssfix ? this.props.mssfix.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.gettext('Default')} - onSubmitValue={this.onMssfixSubmit} - validateValue={OpenVpnSettings.mssfixIsValid} - submitOnBlur={true} - modifyValue={OpenVpnSettings.removeNonNumericCharacters} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the Mssfix input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible mssfix value - // TRANSLATORS: %(min)d - the minimum possible mssfix value - messages.pgettext( - 'openvpn-settings-view', - 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', - ), - { - min: MIN_MSSFIX_VALUE, - max: MAX_MSSFIX_VALUE, - }, + <AriaInputGroup> + <StyledSelectorContainer> + <Selector + title={ + // TRANSLATORS: The title for the shadowsocks bridge selector section. + messages.pgettext('openvpn-settings-view', 'Bridge mode') + } + values={this.bridgeStateItems( + this.props.bridgeModeAvailablity === BridgeModeAvailability.available, )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </StyledNavigationScrollbars> - </NavigationContainer> - </SettingsContainer> + value={this.props.bridgeState} + onSelect={this.onSelectBridgeState} + hasFooter + /> + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('openvpn-settings-view', 'Mssfix')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.mssfix ? this.props.mssfix.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.gettext('Default')} + onSubmitValue={this.onMssfixSubmit} + validateValue={OpenVpnSettings.mssfixIsValid} + submitOnBlur={true} + modifyValue={OpenVpnSettings.removeNonNumericCharacters} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the Mssfix input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible mssfix value + // TRANSLATORS: %(min)d - the minimum possible mssfix value + messages.pgettext( + 'openvpn-settings-view', + 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_MSSFIX_VALUE, + max: MAX_MSSFIX_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </SettingsContainer> - {this.renderBridgeStateConfirmation()} - </Layout> + {this.renderBridgeStateConfirmation()} + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx index e3e4ccd270..5cdd110023 100644 --- a/gui/src/renderer/components/Preferences.tsx +++ b/gui/src/renderer/components/Preferences.tsx @@ -8,10 +8,10 @@ import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import ImageView from './ImageView'; +import { BackAction } from './KeyboardNavigation'; import { Layout } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { - BackBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -53,223 +53,198 @@ export default class Preferences extends React.Component<IProps, IState> { public render() { return ( - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('preferences-nav', 'Preferences') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('preferences-nav', 'Preferences') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <NavigationScrollbars> - <SettingsHeader> - <HeaderTitle>{messages.pgettext('preferences-view', 'Preferences')}</HeaderTitle> - </SettingsHeader> + <NavigationScrollbars> + <SettingsHeader> + <HeaderTitle>{messages.pgettext('preferences-view', 'Preferences')}</HeaderTitle> + </SettingsHeader> - <StyledContent> - <Cell.CellButton onClick={this.showKillSwitchInfo}> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Kill switch')} - </Cell.InputLabel> - <ImageView source="icon-info" width={18} tintColor={colors.white} /> - </Cell.CellButton> - <StyledSeparator height={20} /> + <StyledContent> + <Cell.CellButton onClick={this.showKillSwitchInfo}> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Kill switch')} + </Cell.InputLabel> + <ImageView source="icon-info" width={18} tintColor={colors.white} /> + </Cell.CellButton> + <StyledSeparator height={20} /> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Launch app on start-up')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={this.props.autoStart} onChange={this.props.setAutoStart} /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> - <StyledSeparator /> - - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Auto-connect')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={this.props.autoConnect} - onChange={this.props.setAutoConnect} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Automatically connect to a server when the app launches.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Launch app on start-up')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.autoStart} + onChange={this.props.setAutoStart} + /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> + <StyledSeparator /> - <AriaInputGroup> - <Cell.Container disabled={this.props.dns.state === 'custom'}> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Block ads')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={ - this.props.dns.state === 'default' && - this.props.dns.defaultOptions.blockAds - } - onChange={this.setBlockAds} - /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> - <StyledSeparator /> - <AriaInputGroup> - <Cell.Container disabled={this.props.dns.state === 'custom'}> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Block trackers')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={ - this.props.dns.state === 'default' && - this.props.dns.defaultOptions.blockTrackers - } - onChange={this.setBlockTrackers} - /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> - <StyledSeparator /> - <AriaInputGroup> - <Cell.Container disabled={this.props.dns.state === 'custom'}> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Block malware')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={ - this.props.dns.state === 'default' && - this.props.dns.defaultOptions.blockMalware - } - onChange={this.setBlockMalware} - /> - </AriaInput> - </Cell.Container> - {this.props.dns.state === 'custom' && <CustomDnsEnabledFooter />} - </AriaInputGroup> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Auto-connect')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.autoConnect} + onChange={this.props.setAutoConnect} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Automatically connect to a server when the app launches.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - {this.props.dns.state !== 'custom' && <StyledSeparator height={20} />} + <AriaInputGroup> + <Cell.Container disabled={this.props.dns.state === 'custom'}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Block ads')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={ + this.props.dns.state === 'default' && + this.props.dns.defaultOptions.blockAds + } + onChange={this.setBlockAds} + /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> + <StyledSeparator /> + <AriaInputGroup> + <Cell.Container disabled={this.props.dns.state === 'custom'}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Block trackers')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={ + this.props.dns.state === 'default' && + this.props.dns.defaultOptions.blockTrackers + } + onChange={this.setBlockTrackers} + /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> + <StyledSeparator /> + <AriaInputGroup> + <Cell.Container disabled={this.props.dns.state === 'custom'}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Block malware')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={ + this.props.dns.state === 'default' && + this.props.dns.defaultOptions.blockMalware + } + onChange={this.setBlockMalware} + /> + </AriaInput> + </Cell.Container> + {this.props.dns.state === 'custom' && <CustomDnsEnabledFooter />} + </AriaInputGroup> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Local network sharing')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Allows access to other devices on the same network for sharing, printing etc.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + {this.props.dns.state !== 'custom' && <StyledSeparator height={20} />} - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Notifications')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={this.props.enableSystemNotifications} - onChange={this.props.setEnableSystemNotifications} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Enable or disable system notifications. The critical notifications will always be displayed.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Local network sharing')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Allows access to other devices on the same network for sharing, printing etc.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Monochromatic tray icon')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={this.props.monochromaticIcon} - onChange={this.props.setMonochromaticIcon} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Use a monochromatic tray icon instead of a colored one.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Notifications')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.enableSystemNotifications} + onChange={this.props.setEnableSystemNotifications} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Enable or disable system notifications. The critical notifications will always be displayed.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - {(window.env.platform === 'win32' || - (window.env.platform === 'darwin' && window.env.development)) && ( <AriaInputGroup> <Cell.Container> <AriaLabel> <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Unpin app from taskbar')} + {messages.pgettext('preferences-view', 'Monochromatic tray icon')} </Cell.InputLabel> </AriaLabel> <AriaInput> <Cell.Switch - isOn={this.props.unpinnedWindow} - onChange={this.props.setUnpinnedWindow} + isOn={this.props.monochromaticIcon} + onChange={this.props.setMonochromaticIcon} /> </AriaInput> </Cell.Container> @@ -278,27 +253,26 @@ export default class Preferences extends React.Component<IProps, IState> { <Cell.FooterText> {messages.pgettext( 'preferences-view', - 'Enable to move the app around as a free-standing window.', + 'Use a monochromatic tray icon instead of a colored one.', )} </Cell.FooterText> </AriaDescription> </Cell.Footer> </AriaInputGroup> - )} - {this.props.unpinnedWindow && ( - <React.Fragment> + {(window.env.platform === 'win32' || + (window.env.platform === 'darwin' && window.env.development)) && ( <AriaInputGroup> <Cell.Container> <AriaLabel> <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Start minimized')} + {messages.pgettext('preferences-view', 'Unpin app from taskbar')} </Cell.InputLabel> </AriaLabel> <AriaInput> <Cell.Switch - isOn={this.props.startMinimized} - onChange={this.props.setStartMinimized} + isOn={this.props.unpinnedWindow} + onChange={this.props.setUnpinnedWindow} /> </AriaInput> </Cell.Container> @@ -307,65 +281,95 @@ export default class Preferences extends React.Component<IProps, IState> { <Cell.FooterText> {messages.pgettext( 'preferences-view', - 'Show only the tray icon when the app starts.', + 'Enable to move the app around as a free-standing window.', )} </Cell.FooterText> </AriaDescription> </Cell.Footer> </AriaInputGroup> - </React.Fragment> - )} + )} - <AriaInputGroup> - <Cell.Container disabled={this.props.isBeta}> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('preferences-view', 'Beta program')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={this.props.showBetaReleases} - onChange={this.props.setShowBetaReleases} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {this.props.isBeta - ? messages.pgettext( - 'preferences-view', - 'This option is unavailable while using a beta version.', - ) - : messages.pgettext( - 'preferences-view', - 'Enable to get notified when new beta versions of the app are released.', - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </StyledContent> - </NavigationScrollbars> - </NavigationContainer> - </StyledContainer> + {this.props.unpinnedWindow && ( + <React.Fragment> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Start minimized')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.startMinimized} + onChange={this.props.setStartMinimized} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Show only the tray icon when the app starts.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </React.Fragment> + )} + + <AriaInputGroup> + <Cell.Container disabled={this.props.isBeta}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Beta program')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.showBetaReleases} + onChange={this.props.setShowBetaReleases} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {this.props.isBeta + ? messages.pgettext( + 'preferences-view', + 'This option is unavailable while using a beta version.', + ) + : messages.pgettext( + 'preferences-view', + 'Enable to get notified when new beta versions of the app are released.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledContent> + </NavigationScrollbars> + </NavigationContainer> + </StyledContainer> - <ModalAlert - isOpen={this.state.showKillSwitchInfo} - message={messages.pgettext( - 'preferences-view', - 'The app has a built in kill switch that is enabled by default and cannot be disabled. This is to prevent your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails for any reason. Mullvad automatically protects your data until your connection is reestablished.', - )} - type={ModalAlertType.info} - buttons={[ - <AppButton.BlueButton key="back" onClick={this.hideKillSwitchInfo}> - {messages.gettext('Got it!')} - </AppButton.BlueButton>, - ]} - close={this.hideKillSwitchInfo} - /> - </Layout> + <ModalAlert + isOpen={this.state.showKillSwitchInfo} + message={messages.pgettext( + 'preferences-view', + 'The app has a built in kill switch that is enabled by default and cannot be disabled. This is to prevent your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails for any reason. Mullvad automatically protects your data until your connection is reestablished.', + )} + type={ModalAlertType.info} + buttons={[ + <AppButton.BlueButton key="back" onClick={this.hideKillSwitchInfo}> + {messages.gettext('Got it!')} + </AppButton.BlueButton>, + ]} + close={this.hideKillSwitchInfo} + /> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx index d6ef71ef61..a65a02153b 100644 --- a/gui/src/renderer/components/SelectLanguage.tsx +++ b/gui/src/renderer/components/SelectLanguage.tsx @@ -6,7 +6,6 @@ import { AriaInputGroup } from './AriaGroup'; import { CustomScrollbarsRef } from './CustomScrollbars'; import { Container, Layout } from './Layout'; import { - BackBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -15,6 +14,7 @@ import { } from './NavigationBar'; import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import { BackAction } from './KeyboardNavigation'; interface IProps { preferredLocale: string; @@ -59,40 +59,41 @@ export default class SelectLanguage extends React.Component<IProps, IState> { public render() { return ( - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('select-language-nav', 'Select language') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('select-language-nav', 'Select language') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars ref={this.scrollView}> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('select-language-nav', 'Select language')} - </HeaderTitle> - </SettingsHeader> - <AriaInputGroup> - <StyledSelector - title="" - values={this.state.source} - value={this.props.preferredLocale} - onSelect={this.props.setPreferredLocale} - selectedCellRef={this.selectedCellRef} - /> - </AriaInputGroup> - </StyledNavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + <StyledNavigationScrollbars ref={this.scrollView}> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('select-language-nav', 'Select language')} + </HeaderTitle> + </SettingsHeader> + <AriaInputGroup> + <StyledSelector + title="" + values={this.state.source} + value={this.props.preferredLocale} + onSelect={this.props.setPreferredLocale} + selectedCellRef={this.selectedCellRef} + /> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index b6698fcbc7..48d50084dc 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -15,7 +15,6 @@ import LocationList, { LocationSelectionType, } from './LocationList'; import { - CloseBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -38,6 +37,7 @@ import { StyledSettingsHeader, } from './SelectLocationStyles'; import { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; +import { BackAction } from './KeyboardNavigation'; interface IProps { locale: string; @@ -132,101 +132,102 @@ export default class SelectLocation extends React.Component<IProps, IState> { public render() { return ( - <Layout onClick={this.onClickAnywhere}> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <CloseBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('select-location-nav', 'Select location') - } - </TitleBarItem> + <BackAction icon="close" action={this.props.onClose}> + <Layout onClick={this.onClickAnywhere}> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('select-location-nav', 'Select location') + } + </TitleBarItem> - <StyledFilterContainer ref={this.filterButtonRef}> - <StyledFilterIconButton - onClick={this.toggleFilterMenu} - aria-label={messages.gettext('Filter')}> - <ImageView - source="icon-filter-round" - tintColor={colors.white40} - tintHoverColor={colors.white60} - height={24} - width={24} - /> - </StyledFilterIconButton> - {this.state.showFilterMenu && ( - <StyledFilterMenu> - <StyledFilterByProviderButton onClick={this.props.onViewFilterByProvider}> - {messages.pgettext('select-location-view', 'Filter by provider')} - </StyledFilterByProviderButton> - </StyledFilterMenu> - )} - </StyledFilterContainer> - </NavigationItems> - </NavigationBar> - <NavigationScrollbars ref={this.scrollView}> - <SpacePreAllocationView ref={this.spacePreAllocationViewRef}> - <StyledNavigationBarAttachment top={-this.state.headingHeight}> - <StyledSettingsHeader ref={this.headerRef}> - <HeaderTitle> - { - // TRANSLATORS: Heading in select location view - messages.pgettext('select-location-view', 'Select location') - } - </HeaderTitle> - {this.renderHeaderSubtitle()} - </StyledSettingsHeader> + <StyledFilterContainer ref={this.filterButtonRef}> + <StyledFilterIconButton + onClick={this.toggleFilterMenu} + aria-label={messages.gettext('Filter')}> + <ImageView + source="icon-filter-round" + tintColor={colors.white40} + tintHoverColor={colors.white60} + height={24} + width={24} + /> + </StyledFilterIconButton> + {this.state.showFilterMenu && ( + <StyledFilterMenu> + <StyledFilterByProviderButton onClick={this.props.onViewFilterByProvider}> + {messages.pgettext('select-location-view', 'Filter by provider')} + </StyledFilterByProviderButton> + </StyledFilterMenu> + )} + </StyledFilterContainer> + </NavigationItems> + </NavigationBar> + <NavigationScrollbars ref={this.scrollView}> + <SpacePreAllocationView ref={this.spacePreAllocationViewRef}> + <StyledNavigationBarAttachment top={-this.state.headingHeight}> + <StyledSettingsHeader ref={this.headerRef}> + <HeaderTitle> + { + // TRANSLATORS: Heading in select location view + messages.pgettext('select-location-view', 'Select location') + } + </HeaderTitle> + {this.renderHeaderSubtitle()} + </StyledSettingsHeader> - {this.props.providers.length > 0 && ( - <StyledProviderCountRow> - {messages.pgettext('select-location-view', 'Filtered:')} - <StyledProvidersCount> - {sprintf( - messages.pgettext( - 'select-location-view', - 'Providers: %(numberOfProviders)d', - ), - { - numberOfProviders: this.props.providers.length, - }, - )} - <StyledClearProvidersButton - aria-label={messages.gettext('Clear')} - onClick={this.props.onClearProviders}> - <ImageView - height={16} - width={16} - source="icon-close" - tintColor={colors.white60} - tintHoverColor={colors.white80} - /> - </StyledClearProvidersButton> - </StyledProvidersCount> - </StyledProviderCountRow> - )} - {this.props.allowEntrySelection && ( - <StyledScopeBar - defaultSelectedIndex={this.state.locationScope} - onChange={this.onChangeLocationScope}> - <ScopeBarItem> - {messages.pgettext('select-location-view', 'Entry')} - </ScopeBarItem> - <ScopeBarItem> - {messages.pgettext('select-location-view', 'Exit')} - </ScopeBarItem> - </StyledScopeBar> - )} - </StyledNavigationBarAttachment> + {this.props.providers.length > 0 && ( + <StyledProviderCountRow> + {messages.pgettext('select-location-view', 'Filtered:')} + <StyledProvidersCount> + {sprintf( + messages.pgettext( + 'select-location-view', + 'Providers: %(numberOfProviders)d', + ), + { + numberOfProviders: this.props.providers.length, + }, + )} + <StyledClearProvidersButton + aria-label={messages.gettext('Clear')} + onClick={this.props.onClearProviders}> + <ImageView + height={16} + width={16} + source="icon-close" + tintColor={colors.white60} + tintHoverColor={colors.white80} + /> + </StyledClearProvidersButton> + </StyledProvidersCount> + </StyledProviderCountRow> + )} + {this.props.allowEntrySelection && ( + <StyledScopeBar + defaultSelectedIndex={this.state.locationScope} + onChange={this.onChangeLocationScope}> + <ScopeBarItem> + {messages.pgettext('select-location-view', 'Entry')} + </ScopeBarItem> + <ScopeBarItem> + {messages.pgettext('select-location-view', 'Exit')} + </ScopeBarItem> + </StyledScopeBar> + )} + </StyledNavigationBarAttachment> - <StyledContent>{this.renderLocationList()}</StyledContent> - </SpacePreAllocationView> - </NavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + <StyledContent>{this.renderLocationList()}</StyledContent> + </SpacePreAllocationView> + </NavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx index 110449c36d..2d7abb19cf 100644 --- a/gui/src/renderer/components/Settings.tsx +++ b/gui/src/renderer/components/Settings.tsx @@ -6,13 +6,7 @@ import History from '../lib/history'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import * as Cell from './cell'; import { Layout } from './Layout'; -import { - CloseBarItem, - NavigationBar, - NavigationContainer, - NavigationItems, - TitleBarItem, -} from './NavigationBar'; +import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { StyledCellIcon, @@ -26,6 +20,7 @@ import { } from './SettingsStyles'; import { LoginState } from '../redux/account/reducers'; +import { BackAction } from './KeyboardNavigation'; export interface IProps { preferredLocaleDisplayName: string; @@ -60,41 +55,42 @@ export default class Settings extends React.Component<IProps> { const showLargeTitle = this.props.loginState.type !== 'ok'; return ( - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar alwaysDisplayBarTitle={!showLargeTitle}> - <NavigationItems> - <CloseBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('navigation-bar', 'Settings') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction icon="close" action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar alwaysDisplayBarTitle={!showLargeTitle}> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('navigation-bar', 'Settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars fillContainer> - <StyledContent> - {showLargeTitle && ( - <SettingsHeader> - <HeaderTitle>{messages.pgettext('navigation-bar', 'Settings')}</HeaderTitle> - </SettingsHeader> - )} + <StyledNavigationScrollbars fillContainer> + <StyledContent> + {showLargeTitle && ( + <SettingsHeader> + <HeaderTitle>{messages.pgettext('navigation-bar', 'Settings')}</HeaderTitle> + </SettingsHeader> + )} - <StyledSettingsContent> - {this.renderTopButtons()} - {this.renderMiddleButtons()} - {this.renderBottomButtons()} - </StyledSettingsContent> - </StyledContent> + <StyledSettingsContent> + {this.renderTopButtons()} + {this.renderMiddleButtons()} + {this.renderBottomButtons()} + </StyledSettingsContent> + </StyledContent> - {this.renderQuitButton()} - </StyledNavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + {this.renderQuitButton()} + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx index ace08cd694..017e226b1a 100644 --- a/gui/src/renderer/components/SplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx @@ -16,13 +16,7 @@ import ImageView from './ImageView'; import { Layout } from './Layout'; import List from './List'; import { ModalAlert, ModalAlertType } from './Modal'; -import { - BackBarItem, - NavigationBar, - NavigationContainer, - NavigationItems, - TitleBarItem, -} from './NavigationBar'; +import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; import { StyledPageCover, @@ -48,6 +42,7 @@ import { StyledListContainer, } from './SplitTunnelingSettingsStyles'; import { formatMarkdown } from '../markdown-formatter'; +import { BackAction } from './KeyboardNavigation'; export default function SplitTunneling() { const { pop } = useHistory(); @@ -59,32 +54,33 @@ export default function SplitTunneling() { return ( <> <StyledPageCover show={browsing} /> - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={pop} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('split-tunneling-nav', 'Split tunneling') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={pop}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('split-tunneling-nav', 'Split tunneling') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars ref={scrollbarsRef}> - <StyledContent> - <PlatformSpecificSplitTunnelingSettings - setBrowsing={setBrowsing} - scrollToTop={scrollToTop} - /> - </StyledContent> - </StyledNavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + <StyledNavigationScrollbars ref={scrollbarsRef}> + <StyledContent> + <PlatformSpecificSplitTunnelingSettings + setBrowsing={setBrowsing} + scrollToTop={scrollToTop} + /> + </StyledContent> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> </> ); } diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx index b3b7a3e6cf..2246f1110b 100644 --- a/gui/src/renderer/components/Support.tsx +++ b/gui/src/renderer/components/Support.tsx @@ -6,7 +6,7 @@ import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGrou import ImageView from './ImageView'; import { Layout } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; -import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; +import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; import { StyledBlueButton, @@ -28,6 +28,7 @@ import { import { AccountToken } from '../../shared/daemon-rpc-types'; import { ISupportReportForm } from '../redux/support/actions'; +import { BackAction } from './KeyboardNavigation'; enum SendState { initial, @@ -150,28 +151,29 @@ export default class Support extends React.Component<ISupportProps, ISupportStat const content = this.renderContent(); return ( - <Layout> - <StyledContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('support-view', 'Report a problem') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> - <StyledContentContainer> - {header} - {content} - </StyledContentContainer> + <BackAction action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('support-view', 'Report a problem') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + <StyledContentContainer> + {header} + {content} + </StyledContentContainer> - {this.renderNoEmailDialog()} - {this.renderOutdateVersionWarningDialog()} - </StyledContainer> - </Layout> + {this.renderNoEmailDialog()} + {this.renderOutdateVersionWarningDialog()} + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx index f1defec20a..1a4dab38a6 100644 --- a/gui/src/renderer/components/WireguardKeys.tsx +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -9,14 +9,9 @@ import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import ClipboardLabel from './ClipboardLabel'; import ImageView from './ImageView'; +import { BackAction } from './KeyboardNavigation'; import { Layout } from './Layout'; -import { - BackBarItem, - NavigationBar, - NavigationContainer, - NavigationItems, - TitleBarItem, -} from './NavigationBar'; +import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { StyledButtonRow, @@ -95,85 +90,86 @@ export default class WireguardKeys extends React.Component<IProps, IState> { public render() { return ( - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('wireguard-keys-nav', 'WireGuard key') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('wireguard-keys-nav', 'WireGuard key') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars fillContainer> - <StyledContent> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('wireguard-keys-nav', 'WireGuard key')} - </HeaderTitle> - </SettingsHeader> + <StyledNavigationScrollbars fillContainer> + <StyledContent> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('wireguard-keys-nav', 'WireGuard key')} + </HeaderTitle> + </SettingsHeader> - <StyledRow> - <StyledRowLabel> - <span>{messages.pgettext('wireguard-key-view', 'Public key')}</span> - <StyledRowLabelSpacer /> - <span>{this.keyValidityLabel()}</span> - </StyledRowLabel> + <StyledRow> + <StyledRowLabel> + <span>{messages.pgettext('wireguard-key-view', 'Public key')}</span> + <StyledRowLabelSpacer /> + <span>{this.keyValidityLabel()}</span> + </StyledRowLabel> - <StyledRowValue>{this.getKeyText()}</StyledRowValue> - </StyledRow> - <StyledRow> - <StyledRowLabel> - {messages.pgettext('wireguard-key-view', 'Key generated')} - </StyledRowLabel> - <StyledRowValue>{this.state.ageOfKeyString}</StyledRowValue> - </StyledRow> + <StyledRowValue>{this.getKeyText()}</StyledRowValue> + </StyledRow> + <StyledRow> + <StyledRowLabel> + {messages.pgettext('wireguard-key-view', 'Key generated')} + </StyledRowLabel> + <StyledRowValue>{this.state.ageOfKeyString}</StyledRowValue> + </StyledRow> - <StyledMessages>{this.getStatusMessage()}</StyledMessages> + <StyledMessages>{this.getStatusMessage()}</StyledMessages> - <StyledButtonRow>{this.getGenerateButton()}</StyledButtonRow> - <StyledButtonRow> - <AppButton.BlueButton - disabled={this.isVerifyButtonDisabled()} - onClick={this.handleVerifyKeyPress}> - <AppButton.Label> - {messages.pgettext('wireguard-key-view', 'Verify key')} - </AppButton.Label> - </AppButton.BlueButton> - </StyledButtonRow> - <StyledLastButtonRow> - <AppButton.BlockingButton - disabled={this.props.isOffline} - onClick={this.props.onVisitWebsiteKey}> - <AriaDescriptionGroup> - <AriaDescribed> - <AppButton.BlueButton> - <AppButton.Label> - {messages.pgettext('wireguard-key-view', 'Manage keys')} - </AppButton.Label> - <AriaDescription> - <AppButton.Icon - source="icon-extLink" - height={16} - width={16} - aria-label={messages.pgettext('accessibility', 'Opens externally')} - /> - </AriaDescription> - </AppButton.BlueButton> - </AriaDescribed> - </AriaDescriptionGroup> - </AppButton.BlockingButton> - </StyledLastButtonRow> - </StyledContent> - </StyledNavigationScrollbars> - </NavigationContainer> - </StyledContainer> - </Layout> + <StyledButtonRow>{this.getGenerateButton()}</StyledButtonRow> + <StyledButtonRow> + <AppButton.BlueButton + disabled={this.isVerifyButtonDisabled()} + onClick={this.handleVerifyKeyPress}> + <AppButton.Label> + {messages.pgettext('wireguard-key-view', 'Verify key')} + </AppButton.Label> + </AppButton.BlueButton> + </StyledButtonRow> + <StyledLastButtonRow> + <AppButton.BlockingButton + disabled={this.props.isOffline} + onClick={this.props.onVisitWebsiteKey}> + <AriaDescriptionGroup> + <AriaDescribed> + <AppButton.BlueButton> + <AppButton.Label> + {messages.pgettext('wireguard-key-view', 'Manage keys')} + </AppButton.Label> + <AriaDescription> + <AppButton.Icon + source="icon-extLink" + height={16} + width={16} + aria-label={messages.pgettext('accessibility', 'Opens externally')} + /> + </AriaDescription> + </AppButton.BlueButton> + </AriaDescribed> + </AriaDescriptionGroup> + </AppButton.BlockingButton> + </StyledLastButtonRow> + </StyledContent> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 7456db290d..f799cff335 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -9,7 +9,6 @@ import * as Cell from './cell'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { - BackBarItem, NavigationBar, NavigationContainer, NavigationItems, @@ -19,6 +18,7 @@ import { import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import Switch from './Switch'; +import { BackAction } from './KeyboardNavigation'; const MIN_WIREGUARD_MTU_VALUE = 1280; const MAX_WIREGUARD_MTU_VALUE = 1420; @@ -97,167 +97,168 @@ export default class WireguardSettings extends React.Component<IProps, IState> { public render() { return ( - <Layout> - <SettingsContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose} /> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('wireguard-settings-nav', 'WireGuard settings') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <BackAction action={this.props.onClose}> + <Layout> + <SettingsContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('wireguard-settings-nav', 'WireGuard settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('wireguard-settings-view', 'WireGuard settings')} - </HeaderTitle> - </SettingsHeader> + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('wireguard-settings-view', 'WireGuard settings')} + </HeaderTitle> + </SettingsHeader> - <AriaInputGroup> - <StyledSelectorContainer> - <StyledSelectorForFooter - // TRANSLATORS: The title for the WireGuard port selector. - title={messages.pgettext('wireguard-settings-view', 'Port')} - values={this.wireguardPortItems} - value={this.props.wireguard.port} - onSelect={this.props.setWireguardPort} - /> - </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - { - // TRANSLATORS: The hint displayed below the WireGuard port selector. - messages.pgettext( - 'wireguard-settings-view', - 'The automatic setting will randomly choose from a wide range of ports.', - ) - } - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - { - // TRANSLATORS: The label next to the multihop settings toggle. - messages.pgettext('advanced-settings-view', 'Enable multihop') - } - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - ref={this.multihopRef} - isOn={this.props.wireguardMultihop} - onChange={this.setWireguardMultihop} + <AriaInputGroup> + <StyledSelectorContainer> + <StyledSelectorForFooter + // TRANSLATORS: The title for the WireGuard port selector. + title={messages.pgettext('wireguard-settings-view', 'Port')} + values={this.wireguardPortItems} + value={this.props.wireguard.port} + onSelect={this.props.setWireguardPort} /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - { - // TRANSLATORS: Description for multihop settings toggle. - messages.pgettext( - 'advanced-settings-view', - 'Increases anonymity by routing your traffic into one WireGuard server and out another, making it harder to trace.', - ) - } - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - - <AriaInputGroup> - <StyledSelectorContainer> - <StyledSelectorForFooter - // TRANSLATORS: The title for the WireGuard IP version selector. - title={messages.pgettext('wireguard-settings-view', 'IP version')} - values={this.wireguardIpVersionItems} - value={this.props.wireguard.ipVersion} - onSelect={this.props.setWireguardIpVersion} - /> - </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - { - // TRANSLATORS: The hint displayed below the WireGuard IP version selector. - messages.pgettext( - 'wireguard-settings-view', - 'This allows access to WireGuard for devices that only support IPv6.', - ) - } - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + { + // TRANSLATORS: The hint displayed below the WireGuard port selector. + messages.pgettext( + 'wireguard-settings-view', + 'The automatic setting will randomly choose from a wide range of ports.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <Cell.CellButtonGroup> - <Cell.CellButton onClick={this.props.onViewWireguardKeys}> - <Cell.Label> - {messages.pgettext('wireguard-settings-view', 'WireGuard key')} - </Cell.Label> - <Cell.Icon height={12} width={7} source="icon-chevron" /> - </Cell.CellButton> - </Cell.CellButtonGroup> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + { + // TRANSLATORS: The label next to the multihop settings toggle. + messages.pgettext('advanced-settings-view', 'Enable multihop') + } + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + ref={this.multihopRef} + isOn={this.props.wireguardMultihop} + onChange={this.setWireguardMultihop} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + { + // TRANSLATORS: Description for multihop settings toggle. + messages.pgettext( + 'advanced-settings-view', + 'Increases anonymity by routing your traffic into one WireGuard server and out another, making it harder to trace.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('wireguard-settings-view', 'MTU')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.AutoSizingTextInput - value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.gettext('Default')} - onSubmitValue={this.onWireguardMtuSubmit} - validateValue={WireguardSettings.wireguarMtuIsValid} - submitOnBlur={true} - modifyValue={WireguardSettings.removeNonNumericCharacters} + <AriaInputGroup> + <StyledSelectorContainer> + <StyledSelectorForFooter + // TRANSLATORS: The title for the WireGuard IP version selector. + title={messages.pgettext('wireguard-settings-view', 'IP version')} + values={this.wireguardIpVersionItems} + value={this.props.wireguard.ipVersion} + onSelect={this.props.setWireguardIpVersion} /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard MTU input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value - // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value - messages.pgettext( - 'wireguard-settings-view', - 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', - ), + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> { - min: MIN_WIREGUARD_MTU_VALUE, - max: MAX_WIREGUARD_MTU_VALUE, - }, - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </StyledNavigationScrollbars> - </NavigationContainer> - </SettingsContainer> + // TRANSLATORS: The hint displayed below the WireGuard IP version selector. + messages.pgettext( + 'wireguard-settings-view', + 'This allows access to WireGuard for devices that only support IPv6.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + + <Cell.CellButtonGroup> + <Cell.CellButton onClick={this.props.onViewWireguardKeys}> + <Cell.Label> + {messages.pgettext('wireguard-settings-view', 'WireGuard key')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </Cell.CellButtonGroup> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('wireguard-settings-view', 'MTU')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.gettext('Default')} + onSubmitValue={this.onWireguardMtuSubmit} + validateValue={WireguardSettings.wireguarMtuIsValid} + submitOnBlur={true} + modifyValue={WireguardSettings.removeNonNumericCharacters} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard MTU input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value + // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value + messages.pgettext( + 'wireguard-settings-view', + 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_WIREGUARD_MTU_VALUE, + max: MAX_WIREGUARD_MTU_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </SettingsContainer> - {this.renderMultihopConfirmation()} - </Layout> + {this.renderMultihopConfirmation()} + </Layout> + </BackAction> ); } diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx index 913fb26ece..007ac9cdb6 100644 --- a/gui/src/renderer/components/cell/Input.tsx +++ b/gui/src/renderer/components/cell/Input.tsx @@ -6,6 +6,7 @@ import { CellDisabledContext, Container } from './Container'; import StandaloneSwitch from '../Switch'; import ImageView from '../ImageView'; import { useBoolean } from '../../lib/utilityHooks'; +import { BackAction } from '../KeyboardNavigation'; export const Switch = React.forwardRef(function SwitchT( props: StandaloneSwitch['props'], @@ -113,6 +114,10 @@ export class Input extends React.Component<IInputProps, IInputState> { ); } + public blur = () => { + this.inputRef.current?.blur(); + }; + private onChange = (event: React.ChangeEvent<HTMLInputElement>) => { const value = this.props.modifyValue?.(event.target.value) ?? event.target.value; this.setState({ value }); @@ -176,6 +181,7 @@ export function AutoSizingTextInput(props: IInputProps) { const [value, setValue] = useState(otherProps.value ?? ''); const [focused, setFocused, setBlurred] = useBoolean(false); + const inputRef = useRef() as React.RefObject<Input>; const onChangeValueWrapper = useCallback( (value: string) => { @@ -201,22 +207,27 @@ export function AutoSizingTextInput(props: IInputProps) { [onFocus], ); + const blur = useCallback(() => inputRef.current?.blur(), []); + return ( - <InputFrame focused={focused}> - <StyledAutoSizingTextInputContainer> - <StyledAutoSizingTextInputWrapper> - <Input - onChangeValue={onChangeValueWrapper} - onBlur={onBlurWrapper} - onFocus={onFocusWrapper} - {...otherProps} - /> - </StyledAutoSizingTextInputWrapper> - <StyledAutoSizingTextInputFiller className={otherProps.className} aria-hidden={true}> - {value === '' ? otherProps.placeholder : value} - </StyledAutoSizingTextInputFiller> - </StyledAutoSizingTextInputContainer> - </InputFrame> + <BackAction disabled={!focused} action={blur}> + <InputFrame focused={focused}> + <StyledAutoSizingTextInputContainer> + <StyledAutoSizingTextInputWrapper> + <Input + ref={inputRef} + onChangeValue={onChangeValueWrapper} + onBlur={onBlurWrapper} + onFocus={onFocusWrapper} + {...otherProps} + /> + </StyledAutoSizingTextInputWrapper> + <StyledAutoSizingTextInputFiller className={otherProps.className} aria-hidden={true}> + {value === '' ? otherProps.placeholder : value} + </StyledAutoSizingTextInputFiller> + </StyledAutoSizingTextInputContainer> + </InputFrame> + </BackAction> ); } @@ -282,6 +293,7 @@ interface IRowInputProps { export function RowInput(props: IRowInputProps) { const [value, setValue] = useState(props.initialValue ?? ''); const textAreaRef = useRef() as React.RefObject<HTMLTextAreaElement>; + const [focused, setFocused, setBlurred] = useBoolean(false); const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]); const onChange = useCallback( @@ -302,12 +314,17 @@ export function RowInput(props: IRowInputProps) { [submit], ); - const globalKeyListener = useCallback( - (event: KeyboardEvent) => { - if (event.key === 'Escape') { - event.stopPropagation(); - props.onBlur?.(); - } + const onFocus = useCallback( + (event: React.FocusEvent<HTMLTextAreaElement>) => { + setFocused(); + props.onFocus?.(event); + }, + [props.onFocus], + ); + const onBlur = useCallback( + (event: React.FocusEvent<HTMLTextAreaElement>) => { + setBlurred(); + props.onBlur?.(event); }, [props.onBlur], ); @@ -320,6 +337,8 @@ export function RowInput(props: IRowInputProps) { } }, [textAreaRef, value.length]); + const blur = useCallback(() => textAreaRef.current?.blur(), []); + useEffect(() => { if (props.autofocus) { focus(); @@ -332,35 +351,32 @@ export function RowInput(props: IRowInputProps) { } }, [props.invalid, focus]); - useEffect(() => { - document.addEventListener('keydown', globalKeyListener, true); - return () => document.removeEventListener('keydown', globalKeyListener, true); - }, []); - return ( - <StyledCellInputRowContainer> - <StyledInputWrapper marginLeft={props.paddingLeft ?? 0}> - <StyledInputFiller>{value}</StyledInputFiller> - <StyledTextArea - ref={textAreaRef} - onChange={onChange} - onKeyDown={onKeyDown} - rows={1} - value={value} - invalid={props.invalid} - onFocus={props.onFocus} - onBlur={props.onBlur} - placeholder={props.placeholder} - /> - </StyledInputWrapper> - <StyledSubmitButton onClick={submit}> - <ImageView - source="icon-check" - height={18} - tintColor={value === '' ? colors.blue60 : colors.blue} - tintHoverColor={value === '' ? colors.blue60 : colors.blue80} - /> - </StyledSubmitButton> - </StyledCellInputRowContainer> + <BackAction disabled={!focused} action={blur}> + <StyledCellInputRowContainer> + <StyledInputWrapper marginLeft={props.paddingLeft ?? 0}> + <StyledInputFiller>{value}</StyledInputFiller> + <StyledTextArea + ref={textAreaRef} + onChange={onChange} + onKeyDown={onKeyDown} + rows={1} + value={value} + invalid={props.invalid} + onFocus={onFocus} + onBlur={onBlur} + placeholder={props.placeholder} + /> + </StyledInputWrapper> + <StyledSubmitButton onClick={submit}> + <ImageView + source="icon-check" + height={18} + tintColor={value === '' ? colors.blue60 : colors.blue} + tintHoverColor={value === '' ? colors.blue60 : colors.blue80} + /> + </StyledSubmitButton> + </StyledCellInputRowContainer> + </BackAction> ); } |
