summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-09-21 17:30:41 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-09-29 11:45:50 +0200
commit7da2ffb4218b07a622429d039b37607344bcda34 (patch)
tree333a128b67bae834a40d4fdac2c84a39b6193fa0 /gui/src/renderer
parent09e0229b42515c44bca27c103bafdfd53bacf5ee (diff)
downloadmullvadvpn-7da2ffb4218b07a622429d039b37607344bcda34.tar.xz
mullvadvpn-7da2ffb4218b07a622429d039b37607344bcda34.zip
Reset focus on navigation
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/components/Focus.tsx63
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx2
-rw-r--r--gui/src/renderer/routes.tsx61
3 files changed, 100 insertions, 26 deletions
diff --git a/gui/src/renderer/components/Focus.tsx b/gui/src/renderer/components/Focus.tsx
new file mode 100644
index 0000000000..a844497b86
--- /dev/null
+++ b/gui/src/renderer/components/Focus.tsx
@@ -0,0 +1,63 @@
+import path from 'path';
+import React, { useImperativeHandle, useState } from 'react';
+import { useLocation } from 'react-router';
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+import { messages } from '../../shared/gettext';
+
+const PageChangeAnnouncer = styled.div({
+ width: 0,
+ height: 0,
+ overflow: 'hidden',
+});
+
+export interface IFocusHandle {
+ resetFocus(): void;
+}
+
+interface IFocusProps {
+ children?: React.ReactElement;
+}
+
+function Focus(props: IFocusProps, ref: React.Ref<IFocusHandle>) {
+ const location = useLocation();
+ const [title, setTitle] = useState<string>();
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ resetFocus: () => {
+ const pageName = path.basename(location.pathname);
+ const titleElement = document.getElementsByTagName('h1')[0];
+ const titleContent = titleElement?.textContent ?? pageName;
+ setTitle(titleContent);
+
+ const focusElement = titleElement ?? document.getElementsByTagName('header')[0];
+ if (focusElement) {
+ focusElement.setAttribute('tabindex', '-1');
+ focusElement.focus();
+ }
+ },
+ }),
+ [location.pathname],
+ );
+
+ return (
+ <>
+ {title && (
+ <PageChangeAnnouncer aria-live="polite">
+ {
+ // TRANSLATORS: This string is used to notify users of screenreaders that the view has
+ // TRANSLATORS: changed, usually as a result of pressing a navigation button.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(title)s - page title
+ sprintf(messages.pgettext('accessibility', '%(title)s, View loaded'), { title })
+ }
+ </PageChangeAnnouncer>
+ )}
+ {props.children}
+ </>
+ );
+}
+
+export default React.memo(React.forwardRef(Focus));
diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx
index 9b4c2520e3..cdb2211c51 100644
--- a/gui/src/renderer/components/TransitionContainer.tsx
+++ b/gui/src/renderer/components/TransitionContainer.tsx
@@ -16,6 +16,7 @@ interface ITransitionQueueItem {
interface IProps extends ITransitionGroupProps {
children: TransitioningView;
+ onTransitionEnd: () => void;
}
interface IItemStyle {
@@ -192,6 +193,7 @@ export default class TransitionContainer extends React.Component<IProps, IState>
private finishCycling() {
this.isCycling = false;
+ this.props.onTransitionEnd();
}
private continueCycling = () => {
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
index 63a5a3a90e..e4256af994 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 Focus, { IFocusHandle } from './components/Focus';
import LinuxSplitTunnelingSettings from './components/LinuxSplitTunnelingSettings';
import TransitionContainer, { TransitionView } from './components/TransitionContainer';
import AccountPage from './containers/AccountPage';
@@ -24,6 +25,8 @@ interface IAppRoutesState {
class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> {
private unobserveHistory?: () => void;
+ private focusRef = React.createRef<IFocusHandle>();
+
constructor(props: RouteComponentProps) {
super(props);
@@ -58,35 +61,41 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> {
return (
<PlatformWindowContainer>
- <TransitionContainer {...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 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>
</PlatformWindowContainer>
);
}
+
+ private onNavigation = () => {
+ this.focusRef.current?.resetFocus();
+ };
}
const AppRoutesWithRouter = withRouter(AppRoutes);