summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-09-24 15:11:18 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-09-24 15:11:18 +0200
commit1051fb8f3310685e93ebd9f0b228b7126191aae4 (patch)
tree755bfadcb90693eefb386e2efc9c94f11b46ae9d /gui/src
parent1245208413a091de9e07744bd10ec53e2ab00d49 (diff)
parent6a8ce1507a99b6cfaf6b722f4881350a8ebc9b05 (diff)
downloadmullvadvpn-1051fb8f3310685e93ebd9f0b228b7126191aae4.tar.xz
mullvadvpn-1051fb8f3310685e93ebd9f0b228b7126191aae4.zip
Merge branch 'improve-accessibility4' into master
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/Account.tsx28
-rw-r--r--gui/src/renderer/components/AppButton.tsx146
-rw-r--r--gui/src/renderer/components/AriaGroup.tsx80
-rw-r--r--gui/src/renderer/components/ErrorBoundary.tsx2
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx20
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx2
-rw-r--r--gui/src/renderer/components/Launch.tsx2
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx9
-rw-r--r--gui/src/renderer/components/NavigationBarStyles.tsx2
-rw-r--r--gui/src/renderer/components/Settings.tsx55
-rw-r--r--gui/src/renderer/components/SettingsHeader.tsx2
-rw-r--r--gui/src/renderer/components/Support.tsx56
-rw-r--r--gui/src/renderer/components/TunnelControl.tsx8
-rw-r--r--gui/src/renderer/components/WireguardKeys.tsx24
-rw-r--r--gui/src/renderer/lib/utilityHooks.ts15
15 files changed, 295 insertions, 156 deletions
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 3d344887bd..7905431f1d 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -14,9 +14,10 @@ import {
} from './AccountStyles';
import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { Layout } from './Layout';
import { ModalContainer } from './Modal';
-import { BackBarItem, NavigationBar, NavigationItems } from './NavigationBar';
+import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
import { AccountToken } from '../../shared/daemon-rpc-types';
@@ -45,6 +46,12 @@ export default class Account extends React.Component<IProps> {
messages.pgettext('navigation-bar', 'Settings')
}
</BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('account-view', 'Account')
+ }
+ </TitleBarItem>
</NavigationItems>
</NavigationBar>
@@ -75,10 +82,21 @@ export default class Account extends React.Component<IProps> {
<AppButton.BlockingButton
disabled={this.props.isOffline}
onClick={this.props.onBuyMore}>
- <StyledBuyCreditButton>
- <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </StyledBuyCreditButton>
+ <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 />
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx
index 5b2c7fa62e..f35f7b2049 100644
--- a/gui/src/renderer/components/AppButton.tsx
+++ b/gui/src/renderer/components/AppButton.tsx
@@ -1,7 +1,8 @@
import log from 'electron-log';
-import React, { useContext } from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { colors } from '../../config.json';
+import { useMounted } from '../lib/utilityHooks';
import {
StyledButton,
StyledButtonContent,
@@ -10,9 +11,13 @@ import {
} from './AppButtonStyles';
import ImageView from './ImageView';
-const ButtonContext = React.createContext({
+interface IButtonContext {
+ textAdjustment: number;
+ textRef?: React.Ref<HTMLDivElement>;
+}
+
+const ButtonContext = React.createContext<IButtonContext>({
textAdjustment: 0,
- textRef: React.createRef<HTMLDivElement>(),
});
interface ILabelProps {
@@ -46,51 +51,19 @@ export interface IProps extends React.HTMLAttributes<HTMLButtonElement> {
textOffset?: number;
}
-interface IState {
- textAdjustment: number;
-}
-
-class BaseButton extends React.Component<IProps, IState> {
- public state: IState = {
- textAdjustment: 0,
- };
+const BaseButton = React.memo(function BaseButtonT(props: IProps) {
+ const { children, disabled, onClick, ...otherProps } = props;
- private buttonRef = React.createRef<HTMLButtonElement>();
- private textRef = React.createRef<HTMLDivElement>();
+ const blockingContext = useContext(BlockingContext);
+ const [textAdjustment, setTextAdjustment] = useState(0);
+ const buttonRef = useRef() as React.RefObject<HTMLButtonElement>;
+ const textRef = useRef() as React.RefObject<HTMLDivElement>;
- public componentDidMount() {
- this.updateTextAdjustment();
- }
+ const contextValue = useMemo(() => ({ textAdjustment, textRef }), [textAdjustment, textRef]);
- public componentDidUpdate() {
- this.updateTextAdjustment();
- }
-
- public render() {
- const { children, ...otherProps } = this.props;
-
- return (
- <ButtonContext.Provider
- value={{
- textAdjustment: this.state.textAdjustment,
- textRef: this.textRef,
- }}>
- <StyledButton ref={this.buttonRef} {...otherProps}>
- <StyledButtonContent>
- {React.Children.map(children, (child) =>
- typeof child === 'string' ? <Label>{child as string}</Label> : child,
- )}
- </StyledButtonContent>
- </StyledButton>
- </ButtonContext.Provider>
- );
- }
-
- private updateTextAdjustment() {
- const textOffset = this.props.textOffset ?? 0;
-
- const buttonRect = this.buttonRef.current?.getBoundingClientRect();
- const textRect = this.textRef.current?.getBoundingClientRect();
+ useEffect(() => {
+ const buttonRect = buttonRef.current?.getBoundingClientRect();
+ const textRect = textRef.current?.getBoundingClientRect();
if (buttonRect && textRect) {
const leftDiff = textRect.left - buttonRect.left;
@@ -99,55 +72,70 @@ class BaseButton extends React.Component<IProps, IState> {
const trailingSpace = buttonRect.width - (leftDiff + textRect.width);
// calculate text adjustment
+ const textOffset = props.textOffset ?? 0;
const textAdjustment = leftDiff - trailingSpace - textOffset;
// re-render the view with the new text adjustment if it changed
- if (this.state.textAdjustment !== textAdjustment) {
- this.setState({ textAdjustment });
- }
+ setTextAdjustment(textAdjustment);
}
- }
-}
+ });
-interface IBlockingState {
- isBlocked: boolean;
+ return (
+ <ButtonContext.Provider value={contextValue}>
+ <StyledButton
+ ref={buttonRef}
+ disabled={blockingContext.disabled || disabled}
+ onClick={blockingContext.onClick ?? onClick}
+ {...otherProps}>
+ <StyledButtonContent>
+ {React.Children.map(children, (child) =>
+ typeof child === 'string' ? <Label>{child as string}</Label> : child,
+ )}
+ </StyledButtonContent>
+ </StyledButton>
+ </ButtonContext.Provider>
+ );
+});
+
+interface IBlockingContext {
+ disabled?: boolean;
+ onClick?: () => Promise<void>;
}
+const BlockingContext = React.createContext<IBlockingContext>({});
+
interface IBlockingProps {
children?: React.ReactNode;
onClick: () => Promise<void>;
disabled?: boolean;
}
-export class BlockingButton extends React.Component<IBlockingProps, IBlockingState> {
- public state = {
- isBlocked: false,
- };
+export function BlockingButton(props: IBlockingProps) {
+ const isMounted = useMounted();
+ const [isBlocked, setIsBlocked] = useState(false);
- public render() {
- return React.Children.map(this.props.children, (child) => {
- if (React.isValidElement(child)) {
- return React.cloneElement(child as React.ReactElement, {
- ...child.props,
- disabled: this.state.isBlocked || this.props.disabled,
- onClick: this.onClick,
- });
- } else {
- return child;
- }
- });
- }
+ const onClick = useCallback(async () => {
+ setIsBlocked(true);
+ try {
+ await props.onClick();
+ } catch (error) {
+ log.error(`onClick() failed - ${error}`);
+ }
+
+ if (isMounted()) {
+ setIsBlocked(false);
+ }
+ }, [props.onClick]);
+
+ const contextValue = useMemo(
+ () => ({
+ disabled: isBlocked || props.disabled,
+ onClick,
+ }),
+ [isBlocked, props.disabled, onClick],
+ );
- private onClick = () => {
- this.setState({ isBlocked: true }, async () => {
- try {
- await this.props.onClick();
- } catch (error) {
- log.error(`onClick() failed - ${error}`);
- }
- this.setState({ isBlocked: false });
- });
- };
+ return <BlockingContext.Provider value={contextValue}>{props.children}</BlockingContext.Provider>;
}
export const RedButton = styled(BaseButton)({
diff --git a/gui/src/renderer/components/AriaGroup.tsx b/gui/src/renderer/components/AriaGroup.tsx
index 19913171ac..bf5b2d7439 100644
--- a/gui/src/renderer/components/AriaGroup.tsx
+++ b/gui/src/renderer/components/AriaGroup.tsx
@@ -28,12 +28,40 @@ export function AriaControlGroup(props: IAriaGroupProps) {
);
}
+interface IAriaDescriptionContext {
+ descriptionId?: string;
+ setHasDescription: (value: boolean) => void;
+}
+
+const AriaDescriptionContext = React.createContext<IAriaDescriptionContext>({
+ setHasDescription(_value) {
+ throw new Error('Missing AriaDescriptionContext.Provider');
+ },
+});
+
+export function AriaDescriptionGroup(props: IAriaGroupProps) {
+ const id = useMemo(getNewId, []);
+ const [hasDescription, setHasDescription] = useState(false);
+
+ const contextValue = useMemo(
+ () => ({
+ descriptionId: hasDescription ? `${id}-description` : undefined,
+ setHasDescription,
+ }),
+ [hasDescription],
+ );
+
+ return (
+ <AriaDescriptionContext.Provider value={contextValue}>
+ {props.children}
+ </AriaDescriptionContext.Provider>
+ );
+}
+
interface IAriaInputContext {
inputId: string;
labelId?: string;
- descriptionId?: string;
setHasLabel: (value: boolean) => void;
- setHasDescription: (value: boolean) => void;
}
const missingAriaInputContextError = new Error('Missing AriaInputContext.Provider');
@@ -44,27 +72,26 @@ const AriaInputContext = React.createContext<IAriaInputContext>({
setHasLabel() {
throw missingAriaInputContextError;
},
- setHasDescription() {
- throw missingAriaInputContextError;
- },
});
export function AriaInputGroup(props: IAriaGroupProps) {
const id = useMemo(getNewId, []);
const [hasLabel, setHasLabel] = useState(false);
- const [hasDescription, setHasDescription] = useState(false);
- const contextValue = {
- inputId: `${id}-input`,
- labelId: hasLabel ? `${id}-label` : undefined,
- descriptionId: hasDescription ? `${id}-description` : undefined,
- setHasLabel,
- setHasDescription,
- };
+ const contextValue = useMemo(
+ () => ({
+ inputId: `${id}-input`,
+ labelId: hasLabel ? `${id}-label` : undefined,
+ setHasLabel,
+ }),
+ [hasLabel],
+ );
return (
- <AriaInputContext.Provider value={contextValue}>{props.children}</AriaInputContext.Provider>
+ <AriaDescriptionGroup>
+ <AriaInputContext.Provider value={contextValue}>{props.children}</AriaInputContext.Provider>
+ </AriaDescriptionGroup>
);
}
@@ -83,13 +110,16 @@ export function AriaControls(props: IAriaElementProps) {
}
export function AriaInput(props: IAriaElementProps) {
- const { inputId, labelId, descriptionId } = useContext(AriaInputContext);
+ const { inputId, labelId } = useContext(AriaInputContext);
- return React.cloneElement(props.children, {
- id: inputId,
- 'aria-labelledby': labelId,
- 'aria-describedby': descriptionId,
- });
+ return (
+ <AriaDescribed>
+ {React.cloneElement(props.children, {
+ id: inputId,
+ 'aria-labelledby': labelId,
+ })}
+ </AriaDescribed>
+ );
}
export function AriaLabel(props: IAriaElementProps) {
@@ -106,8 +136,16 @@ export function AriaLabel(props: IAriaElementProps) {
});
}
+export function AriaDescribed(props: IAriaElementProps) {
+ const { descriptionId } = useContext(AriaDescriptionContext);
+
+ return React.cloneElement(props.children, {
+ 'aria-describedby': descriptionId,
+ });
+}
+
export function AriaDescription(props: IAriaElementProps) {
- const { descriptionId, setHasDescription } = useContext(AriaInputContext);
+ const { descriptionId, setHasDescription } = useContext(AriaDescriptionContext);
useEffect(() => {
setHasDescription(true);
diff --git a/gui/src/renderer/components/ErrorBoundary.tsx b/gui/src/renderer/components/ErrorBoundary.tsx
index 6470a2052d..c54fe97721 100644
--- a/gui/src/renderer/components/ErrorBoundary.tsx
+++ b/gui/src/renderer/components/ErrorBoundary.tsx
@@ -79,7 +79,7 @@ export default class ErrorBoundary extends React.Component<IProps, IState> {
<StyledContainer>
<Logo height={106} width={106} source="logo-icon" />
<Title>{messages.pgettext('generic', 'MULLVAD VPN')}</Title>
- <Subtitle>{reachBackMessage}</Subtitle>
+ <Subtitle role="alert">{reachBackMessage}</Subtitle>
</StyledContainer>
</Layout>
</PlatformWindowContainer>
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index d64c780808..2a2af26e54 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -6,6 +6,7 @@ import { AccountToken } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { LoginState } from '../redux/account/reducers';
import * as AppButton from './AppButton';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './Cell';
import {
StyledAccountTokenContainer,
@@ -168,10 +169,21 @@ export default class ExpiredAccountErrorView extends React.Component<
<AppButton.BlockingButton
disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
onClick={this.onOpenExternalPayment}>
- <StyledBuyCreditButton>
- <AppButton.Label>{buttonText}</AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </StyledBuyCreditButton>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <StyledBuyCreditButton>
+ <AppButton.Label>{buttonText}</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>
);
}
diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx
index 20045086be..a18e3f09b3 100644
--- a/gui/src/renderer/components/HeaderBar.tsx
+++ b/gui/src/renderer/components/HeaderBar.tsx
@@ -53,7 +53,7 @@ const BrandContainer = styled.div({
alignItems: 'center',
});
-const Title = styled.h1({
+const Title = styled.span({
fontFamily: 'DINPro',
fontSize: '24px',
fontWeight: 900,
diff --git a/gui/src/renderer/components/Launch.tsx b/gui/src/renderer/components/Launch.tsx
index fec3ab68ba..ab75dd5cd3 100644
--- a/gui/src/renderer/components/Launch.tsx
+++ b/gui/src/renderer/components/Launch.tsx
@@ -45,7 +45,7 @@ export default function Launch() {
<StyledContainer>
<Logo height={106} width={106} source="logo-icon" />
<Title>{messages.pgettext('generic', 'MULLVAD VPN')}</Title>
- <Subtitle>
+ <Subtitle role="alert">
{messages.pgettext('launch-view', 'Connecting to Mullvad system service...')}
</Subtitle>
</StyledContainer>
diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx
index 28b6178397..01d42bb8ba 100644
--- a/gui/src/renderer/components/NavigationBar.tsx
+++ b/gui/src/renderer/components/NavigationBar.tsx
@@ -136,7 +136,7 @@ const TitleBarItemContext = React.createContext({
get titleContainerRef(): React.RefObject<HTMLDivElement> {
throw Error('Missing TitleBarItemContext provider');
},
- get measuringTitleRef(): React.RefObject<HTMLSpanElement> {
+ get measuringTitleRef(): React.RefObject<HTMLHeadingElement> {
throw Error('Missing TitleBarItemContext provider');
},
});
@@ -151,7 +151,7 @@ export const NavigationBar = function NavigationBarT(props: INavigationBarProps)
const [titleAdjustment, setTitleAdjustment] = useState(0);
const titleContainerRef = useRef() as React.RefObject<HTMLDivElement>;
- const measuringTitleRef = useRef() as React.RefObject<HTMLSpanElement>;
+ const measuringTitleRef = useRef() as React.RefObject<HTMLHeadingElement>;
const navigationBarRef = useRef() as React.RefObject<HTMLDivElement>;
useLayoutEffect(() => {
@@ -215,10 +215,7 @@ export const TitleBarItem = React.memo(function TitleBarItemT(props: ITitleBarIt
return (
<StyledTitleBarItemContainer ref={titleContainerRef}>
- <StyledTitleBarItemLabel
- titleAdjustment={titleAdjustment}
- visible={visible}
- aria-hidden={!visible}>
+ <StyledTitleBarItemLabel titleAdjustment={titleAdjustment} visible={visible}>
{props.children}
</StyledTitleBarItemLabel>
diff --git a/gui/src/renderer/components/NavigationBarStyles.tsx b/gui/src/renderer/components/NavigationBarStyles.tsx
index c141f4cfed..3f964c1f93 100644
--- a/gui/src/renderer/components/NavigationBarStyles.tsx
+++ b/gui/src/renderer/components/NavigationBarStyles.tsx
@@ -44,7 +44,7 @@ interface ITitleBarItemLabelProps {
visible?: boolean;
}
-export const StyledTitleBarItemLabel = styled.span({}, (props: ITitleBarItemLabelProps) => ({
+export const StyledTitleBarItemLabel = styled.h1({}, (props: ITitleBarItemLabelProps) => ({
fontFamily: 'Open Sans',
fontSize: '16px',
fontWeight: 600,
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index a852d084ea..3cfdefebd5 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { colors, links } from '../../config.json';
import { hasExpired, formatRemainingTime } from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './Cell';
import { Layout } from './Layout';
import {
@@ -176,15 +177,24 @@ export default class Settings extends React.Component<IProps> {
}
return (
- <>
- <Cell.CellButton disabled={this.props.isOffline} onClick={this.openDownloadLink}>
- {icon}
- <Cell.Label>{messages.pgettext('settings-view', 'App version')}</Cell.Label>
- <Cell.SubText>{this.props.appVersion}</Cell.SubText>
- <Cell.Icon height={16} width={16} source="icon-extLink" />
- </Cell.CellButton>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <Cell.CellButton disabled={this.props.isOffline} onClick={this.openDownloadLink}>
+ {icon}
+ <Cell.Label>{messages.pgettext('settings-view', 'App version')}</Cell.Label>
+ <Cell.SubText>{this.props.appVersion}</Cell.SubText>
+ <AriaDescription>
+ <Cell.Icon
+ height={16}
+ width={16}
+ source="icon-extLink"
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </Cell.CellButton>
+ </AriaDescribed>
{footer}
- </>
+ </AriaDescriptionGroup>
);
}
@@ -201,15 +211,26 @@ export default class Settings extends React.Component<IProps> {
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
- <Cell.CellButton disabled={this.props.isOffline} onClick={this.openFaqLink}>
- <Cell.Label>
- {
- // TRANSLATORS: Link to the webpage
- messages.pgettext('settings-view', 'FAQs & Guides')
- }
- </Cell.Label>
- <Cell.Icon height={16} width={16} source="icon-extLink" />
- </Cell.CellButton>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <Cell.CellButton disabled={this.props.isOffline} onClick={this.openFaqLink}>
+ <Cell.Label>
+ {
+ // TRANSLATORS: Link to the webpage
+ messages.pgettext('settings-view', 'FAQs & Guides')
+ }
+ </Cell.Label>
+ <AriaDescription>
+ <Cell.Icon
+ height={16}
+ width={16}
+ source="icon-extLink"
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </Cell.CellButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
<Cell.CellButton onClick={this.props.onViewSelectLanguage}>
<StyledCellIcon width={24} height={24} source="icon-language" />
diff --git a/gui/src/renderer/components/SettingsHeader.tsx b/gui/src/renderer/components/SettingsHeader.tsx
index c0223461e0..808b6f80c7 100644
--- a/gui/src/renderer/components/SettingsHeader.tsx
+++ b/gui/src/renderer/components/SettingsHeader.tsx
@@ -13,7 +13,7 @@ export const ContentWrapper = styled.div({
},
});
-export const HeaderTitle = styled.h1(bigText);
+export const HeaderTitle = styled.span(bigText);
export const HeaderSubTitle = styled.span(smallText);
interface ISettingsHeaderProps {
diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx
index 9436636516..5cb9adcba6 100644
--- a/gui/src/renderer/components/Support.tsx
+++ b/gui/src/renderer/components/Support.tsx
@@ -2,10 +2,11 @@ import * as React from 'react';
import { links } from '../../config.json';
import { messages } from '../../shared/gettext';
import * as AppButton from './AppButton';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ImageView from './ImageView';
import { Layout } from './Layout';
import { ModalAlert, ModalAlertType, ModalContainer } from './Modal';
-import { BackBarItem, NavigationBar, NavigationItems } from './NavigationBar';
+import { BackBarItem, NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import {
StyledBlueButton,
@@ -159,6 +160,12 @@ export default class Support extends React.Component<ISupportProps, ISupportStat
messages.pgettext('navigation-bar', 'Settings')
}
</BackBarItem>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('support-view', 'Report a problem')
+ }
+ </TitleBarItem>
</NavigationItems>
</NavigationBar>
<StyledContentContainer>
@@ -277,13 +284,25 @@ export default class Support extends React.Component<ISupportProps, ISupportStat
type={ModalAlertType.Warning}
message={message}
buttons={[
- <AppButton.GreenButton
- key="upgrade"
- disabled={this.props.isOffline}
- onClick={this.openDownloadLink}>
- <AppButton.Label>{messages.pgettext('support-view', 'Upgrade app')}</AppButton.Label>
- <AppButton.Icon height={16} width={16} source="icon-extLink" />
- </AppButton.GreenButton>,
+ <AriaDescriptionGroup key="upgrade">
+ <AriaDescribed>
+ <AppButton.GreenButton
+ disabled={this.props.isOffline}
+ onClick={this.openDownloadLink}>
+ <AppButton.Label>
+ {messages.pgettext('support-view', 'Upgrade app')}
+ </AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ height={16}
+ width={16}
+ source="icon-extLink"
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </AppButton.GreenButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>,
<AppButton.RedButton key="proceed" onClick={this.acknowledgeOutdateVersion}>
{messages.pgettext('support-view', 'Continue anyway')}
</AppButton.RedButton>,
@@ -316,10 +335,23 @@ export default class Support extends React.Component<ISupportProps, ISupportStat
</StyledFormMessageRow>
</StyledForm>
<StyledFooter>
- <StyledBlueButton onClick={this.onViewLog} disabled={this.state.disableActions}>
- <AppButton.Label>{messages.pgettext('support-view', 'View app logs')}</AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </StyledBlueButton>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <StyledBlueButton onClick={this.onViewLog} disabled={this.state.disableActions}>
+ <AppButton.Label>
+ {messages.pgettext('support-view', 'View app logs')}
+ </AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ source="icon-extLink"
+ height={16}
+ width={16}
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </StyledBlueButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
<AppButton.GreenButton
disabled={!this.validate() || this.state.disableActions}
onClick={this.onSend}>
diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx
index e5f674d7c4..e0345c0fbb 100644
--- a/gui/src/renderer/components/TunnelControl.tsx
+++ b/gui/src/renderer/components/TunnelControl.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
@@ -83,7 +84,12 @@ export default class TunnelControl extends React.Component<ITunnelControlProps>
};
const SelectedLocation = () => (
- <SwitchLocationButton onClick={this.props.onSelectLocation}>
+ <SwitchLocationButton
+ onClick={this.props.onSelectLocation}
+ aria-label={sprintf(
+ messages.pgettext('accessibility', 'Select location. Current location is %(location)s'),
+ { location: this.props.selectedRelayName },
+ )}>
<AppButton.Label>{this.props.selectedRelayName}</AppButton.Label>
<SelectedLocationChevron height={12} width={7} source="icon-chevron" />
</SwitchLocationButton>
diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx
index a8cc7f517e..3f3e8521d7 100644
--- a/gui/src/renderer/components/WireguardKeys.tsx
+++ b/gui/src/renderer/components/WireguardKeys.tsx
@@ -6,6 +6,7 @@ import { TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { IWgKey, WgKeyState } from '../redux/settings/reducers';
import * as AppButton from './AppButton';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ClipboardLabel from './ClipboardLabel';
import ImageView from './ImageView';
import { Layout } from './Layout';
@@ -145,12 +146,23 @@ export default class WireguardKeys extends React.Component<IProps, IState> {
<AppButton.BlockingButton
disabled={this.props.isOffline}
onClick={this.props.onVisitWebsiteKey}>
- <AppButton.BlueButton>
- <AppButton.Label>
- {messages.pgettext('wireguard-key-view', 'Manage keys')}
- </AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </AppButton.BlueButton>
+ <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>
diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts
new file mode 100644
index 0000000000..cbb5575f9b
--- /dev/null
+++ b/gui/src/renderer/lib/utilityHooks.ts
@@ -0,0 +1,15 @@
+import { useCallback, useEffect, useRef } from 'react';
+
+export function useMounted() {
+ const mountedRef = useRef(false);
+ const isMounted = useCallback(() => mountedRef.current, []);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ return () => {
+ mountedRef.current = false;
+ };
+ }, []);
+
+ return isMounted;
+}