diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-09-21 17:30:41 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-09-29 11:45:50 +0200 |
| commit | 7da2ffb4218b07a622429d039b37607344bcda34 (patch) | |
| tree | 333a128b67bae834a40d4fdac2c84a39b6193fa0 | |
| parent | 09e0229b42515c44bca27c103bafdfd53bacf5ee (diff) | |
| download | mullvadvpn-7da2ffb4218b07a622429d039b37607344bcda34.tar.xz mullvadvpn-7da2ffb4218b07a622429d039b37607344bcda34.zip | |
Reset focus on navigation
| -rw-r--r-- | gui/src/renderer/components/Focus.tsx | 63 | ||||
| -rw-r--r-- | gui/src/renderer/components/TransitionContainer.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/routes.tsx | 61 |
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); |
