diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-02 16:55:50 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-02 16:55:50 +0100 |
| commit | c2f07e2533153a0a40c0901fe89901c658196514 (patch) | |
| tree | 3cbe881c250b287820f9729f809381765ca6f855 | |
| parent | 6d2461477b5b34c30a59abf84f43407552a69c11 (diff) | |
| parent | ac13e1d54d9a7cacf6eeb4909d030ea4d47f2b18 (diff) | |
| download | mullvadvpn-c2f07e2533153a0a40c0901fe89901c658196514.tar.xz mullvadvpn-c2f07e2533153a0a40c0901fe89901c658196514.zip | |
Merge branch 'add-escape-hatch'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/src/renderer/components/KeyboardNavigation.tsx | 27 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/lib/history.ts | 34 | ||||
| -rw-r--r-- | gui/src/renderer/routes.tsx | 74 |
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> ); } |
