diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-09-24 15:11:18 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-09-24 15:11:18 +0200 |
| commit | 1051fb8f3310685e93ebd9f0b228b7126191aae4 (patch) | |
| tree | 755bfadcb90693eefb386e2efc9c94f11b46ae9d /gui/src | |
| parent | 1245208413a091de9e07744bd10ec53e2ab00d49 (diff) | |
| parent | 6a8ce1507a99b6cfaf6b722f4881350a8ebc9b05 (diff) | |
| download | mullvadvpn-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.tsx | 28 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppButton.tsx | 146 | ||||
| -rw-r--r-- | gui/src/renderer/components/AriaGroup.tsx | 80 | ||||
| -rw-r--r-- | gui/src/renderer/components/ErrorBoundary.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 20 | ||||
| -rw-r--r-- | gui/src/renderer/components/HeaderBar.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Launch.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBar.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBarStyles.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Settings.tsx | 55 | ||||
| -rw-r--r-- | gui/src/renderer/components/SettingsHeader.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Support.tsx | 56 | ||||
| -rw-r--r-- | gui/src/renderer/components/TunnelControl.tsx | 8 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 24 | ||||
| -rw-r--r-- | gui/src/renderer/lib/utilityHooks.ts | 15 |
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; +} |
