summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-02 16:55:50 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-02 16:55:50 +0100
commitc2f07e2533153a0a40c0901fe89901c658196514 (patch)
tree3cbe881c250b287820f9729f809381765ca6f855
parent6d2461477b5b34c30a59abf84f43407552a69c11 (diff)
parentac13e1d54d9a7cacf6eeb4909d030ea4d47f2b18 (diff)
downloadmullvadvpn-c2f07e2533153a0a40c0901fe89901c658196514.tar.xz
mullvadvpn-c2f07e2533153a0a40c0901fe89901c658196514.zip
Merge branch 'add-escape-hatch'
-rw-r--r--CHANGELOG.md1
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx27
-rw-r--r--gui/src/renderer/components/Modal.tsx7
-rw-r--r--gui/src/renderer/lib/history.ts34
-rw-r--r--gui/src/renderer/routes.tsx74
5 files changed, 98 insertions, 45 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0d1db2ac3..8f6d2b365a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ Line wrap the file at 100 chars. Th
- Improve accessibility in the desktop app.
- Add `--wait` flag to `connect`, `disconnect` and `reconnect` CLI subcommands to make the CLI wait
for the target state to be reached before exiting.
+- Navigate back to the main view when escape is pressed.
#### Windows
- Add support for custom DNS resolvers (CLI only).
diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx
new file mode 100644
index 0000000000..3aefd7c684
--- /dev/null
+++ b/gui/src/renderer/components/KeyboardNavigation.tsx
@@ -0,0 +1,27 @@
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router';
+import History from '../lib/history';
+
+interface IKeyboardNavigationProps {
+ children: React.ReactElement;
+}
+
+export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
+ const history = useHistory() as History;
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ history.reset();
+ }
+ },
+ [history.reset],
+ );
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [handleKeyDown]);
+
+ return props.children;
+}
diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx
index 4c449e8cf2..858c0be066 100644
--- a/gui/src/renderer/components/Modal.tsx
+++ b/gui/src/renderer/components/Modal.tsx
@@ -152,7 +152,9 @@ class ModalAlertWithContext extends React.Component<IModalAlertProps & IModalCon
public componentDidMount() {
this.props.setActiveModal(true);
- document.addEventListener('keydown', this.handleKeyPress);
+ // 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 = this.props.modalContainerRef.current;
if (modalContainer) {
@@ -170,7 +172,7 @@ class ModalAlertWithContext extends React.Component<IModalAlertProps & IModalCon
public componentWillUnmount() {
this.props.setActiveModal(false);
- document.removeEventListener('keydown', this.handleKeyPress);
+ document.removeEventListener('keydown', this.handleKeyPress, true);
this.appendScheduler.cancel();
this.props.modalContainerRef.current?.removeChild(this.element);
@@ -219,6 +221,7 @@ class ModalAlertWithContext extends React.Component<IModalAlertProps & IModalCon
private handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
+ event.stopPropagation();
this.props.close?.();
}
};
diff --git a/gui/src/renderer/lib/history.ts b/gui/src/renderer/lib/history.ts
index b726e72bbb..68553f77c7 100644
--- a/gui/src/renderer/lib/history.ts
+++ b/gui/src/renderer/lib/history.ts
@@ -1,4 +1,10 @@
-import { Location, Action, LocationListener, LocationDescriptor } from 'history';
+import { Location, Action, LocationDescriptor } from 'history';
+
+type LocationListener<S = unknown> = (
+ location: Location<S>,
+ action: Action,
+ entries: Location<S>[],
+) => void;
// It currently isn't possible to implement this correctly with support for a generic state. State
// can be added as a generic type (<S = unknown>) after this issue has been resolved:
@@ -27,24 +33,32 @@ export default class History {
}
public push = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ const affectedEntries = [this.entries[this.index]];
const location = this.createLocation(nextLocation, nextState);
this.lastAction = 'PUSH';
this.index += 1;
this.entries.splice(this.index, this.entries.length - this.index, location);
- this.notify();
+ this.notify(affectedEntries);
};
public replace = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ const affectedEntries = [this.entries[this.index]];
this.entries[this.index] = this.createLocation(nextLocation, nextState);
this.lastAction = 'REPLACE';
- this.notify();
+ this.notify(affectedEntries);
};
public go = (n: number) => {
if (this.canGo(n)) {
- this.index += n;
+ const nextIndex = this.index + n;
+ const affectedEntries =
+ this.index < nextIndex
+ ? this.entries.slice(this.index, nextIndex)
+ : this.entries.slice(nextIndex + 1, this.index + 1);
+
+ this.index = nextIndex;
this.lastAction = 'POP';
- this.notify();
+ this.notify(affectedEntries);
}
};
@@ -52,16 +66,18 @@ export default class History {
public goForward = () => this.go(1);
public reset = () => {
+ const affectedEntries = this.entries.slice(1);
this.lastAction = 'POP';
this.index = 0;
- this.notify();
+ this.notify(affectedEntries);
};
public resetWith = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ const affectedEntries = [...this.entries];
this.entries = [this.createLocation(nextLocation, nextState)];
this.lastAction = 'REPLACE';
this.index = 0;
- this.notify();
+ this.notify(affectedEntries);
};
public canGo(n: number) {
@@ -82,8 +98,8 @@ export default class History {
throw Error('Not implemented');
}
- private notify() {
- this.listeners.forEach((listener) => listener(this.location, this.action));
+ private notify(affectedEntries: Location<S>[]) {
+ this.listeners.forEach((listener) => listener(this.location, this.action, affectedEntries));
}
private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> {
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
index e4256af994..37c9851daa 100644
--- a/gui/src/renderer/routes.tsx
+++ b/gui/src/renderer/routes.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Route, RouteComponentProps, Switch, withRouter } from 'react-router';
import Launch from './components/Launch';
+import KeyboardNavigation from './components/KeyboardNavigation';
import Focus, { IFocusHandle } from './components/Focus';
import LinuxSplitTunnelingSettings from './components/LinuxSplitTunnelingSettings';
import TransitionContainer, { TransitionView } from './components/TransitionContainer';
@@ -15,6 +16,7 @@ import SelectLocationPage from './containers/SelectLocationPage';
import SettingsPage from './containers/SettingsPage';
import SupportPage from './containers/SupportPage';
import WireguardKeysPage from './containers/WireguardKeysPage';
+import History from './lib/history';
import { getTransitionProps } from './transitions';
interface IAppRoutesState {
@@ -38,12 +40,14 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> {
public componentDidMount() {
// React throttles updates, so it's impossible to capture the intermediate navigation without
// listening to the history directly.
- this.unobserveHistory = this.props.history.listen((location) => {
- this.setState((state) => ({
- previousLocation: state.currentLocation,
- currentLocation: location,
- }));
- });
+ this.unobserveHistory = (this.props.history as History).listen(
+ (location, _action, affectedEntries) => {
+ this.setState({
+ previousLocation: affectedEntries[0],
+ currentLocation: location,
+ });
+ },
+ );
}
public componentWillUnmount() {
@@ -61,34 +65,36 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> {
return (
<PlatformWindowContainer>
- <Focus ref={this.focusRef}>
- <TransitionContainer onTransitionEnd={this.onNavigation} {...transitionProps}>
- <TransitionView viewId={location.key || ''}>
- <Switch key={location.key} location={location}>
- <Route exact={true} path="/" component={Launch} />
- <Route exact={true} path="/login" component={LoginPage} />
- <Route exact={true} path="/connect" component={ConnectPage} />
- <Route exact={true} path="/settings" component={SettingsPage} />
- <Route exact={true} path="/settings/language" component={SelectLanguagePage} />
- <Route exact={true} path="/settings/account" component={AccountPage} />
- <Route exact={true} path="/settings/preferences" component={PreferencesPage} />
- <Route exact={true} path="/settings/advanced" component={AdvancedSettingsPage} />
- <Route
- exact={true}
- path="/settings/advanced/wireguard-keys"
- component={WireguardKeysPage}
- />
- <Route
- exact={true}
- path="/settings/advanced/linux-split-tunneling"
- component={LinuxSplitTunnelingSettings}
- />
- <Route exact={true} path="/settings/support" component={SupportPage} />
- <Route exact={true} path="/select-location" component={SelectLocationPage} />
- </Switch>
- </TransitionView>
- </TransitionContainer>
- </Focus>
+ <KeyboardNavigation>
+ <Focus ref={this.focusRef}>
+ <TransitionContainer onTransitionEnd={this.onNavigation} {...transitionProps}>
+ <TransitionView viewId={location.key || ''}>
+ <Switch key={location.key} location={location}>
+ <Route exact={true} path="/" component={Launch} />
+ <Route exact={true} path="/login" component={LoginPage} />
+ <Route exact={true} path="/connect" component={ConnectPage} />
+ <Route exact={true} path="/settings" component={SettingsPage} />
+ <Route exact={true} path="/settings/language" component={SelectLanguagePage} />
+ <Route exact={true} path="/settings/account" component={AccountPage} />
+ <Route exact={true} path="/settings/preferences" component={PreferencesPage} />
+ <Route exact={true} path="/settings/advanced" component={AdvancedSettingsPage} />
+ <Route
+ exact={true}
+ path="/settings/advanced/wireguard-keys"
+ component={WireguardKeysPage}
+ />
+ <Route
+ exact={true}
+ path="/settings/advanced/linux-split-tunneling"
+ component={LinuxSplitTunnelingSettings}
+ />
+ <Route exact={true} path="/settings/support" component={SupportPage} />
+ <Route exact={true} path="/select-location" component={SelectLocationPage} />
+ </Switch>
+ </TransitionView>
+ </TransitionContainer>
+ </Focus>
+ </KeyboardNavigation>
</PlatformWindowContainer>
);
}