diff options
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 370 | ||||
| -rw-r--r-- | gui/src/renderer/components/AriaInputGroup.tsx | 91 | ||||
| -rw-r--r-- | gui/src/renderer/components/Cell.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 3 | ||||
| -rw-r--r-- | gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx | 1 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 66 | ||||
| -rw-r--r-- | gui/src/renderer/components/Preferences.tsx | 258 | ||||
| -rw-r--r-- | gui/src/renderer/components/RedeemVoucher.tsx | 3 | ||||
| -rw-r--r-- | gui/src/renderer/components/Support.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Switch.tsx | 19 |
10 files changed, 534 insertions, 286 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 40131d58ff..5baeaced3a 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -15,6 +15,7 @@ import { StyledTunnelProtocolContainer, } from './AdvancedSettingsStyles'; import * as AppButton from './AppButton'; +import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaInputGroup'; import * as Cell from './Cell'; import { Layout } from './Layout'; import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal'; @@ -169,182 +170,230 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { </HeaderTitle> </SettingsHeader> - <Cell.Container> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'Enable IPv6')} - </Cell.Label> - <Cell.Switch isOn={this.props.enableIpv6} onChange={this.props.setEnableIpv6} /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'advanced-settings-view', - 'Enable IPv6 communication through the tunnel.', - )} - </Cell.FooterText> - </Cell.Footer> - - <Cell.Container> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'Always require VPN')} - </Cell.Label> - <Cell.Switch - isOn={this.props.blockWhenDisconnected} - onChange={this.setBlockWhenDisconnected} - /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'advanced-settings-view', - 'If you disconnect or quit the app, this setting will block your internet.', - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Enable IPv6')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.enableIpv6} + onChange={this.props.setEnableIpv6} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'advanced-settings-view', + 'Enable IPv6 communication through the tunnel.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <StyledTunnelProtocolContainer> - <StyledTunnelProtocolSelector - title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')} - values={this.tunnelProtocolItems(hasWireguardKey)} - value={this.props.tunnelProtocol} - onSelect={this.onSelectTunnelProtocol} - /> - {!hasWireguardKey && ( - <StyledNoWireguardKeyErrorContainer> - <StyledNoWireguardKeyError> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Always require VPN')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.blockWhenDisconnected} + onChange={this.setBlockWhenDisconnected} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> {messages.pgettext( 'advanced-settings-view', - 'To enable WireGuard, generate a key under the "WireGuard key" setting below.', + 'If you disconnect or quit the app, this setting will block your internet.', )} - </StyledNoWireguardKeyError> - </StyledNoWireguardKeyErrorContainer> - )} - </StyledTunnelProtocolContainer> + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - {this.props.tunnelProtocol !== 'wireguard' ? ( - <StyledSelectorContainer> - <Selector - title={messages.pgettext( - 'advanced-settings-view', - 'OpenVPN transport protocol', - )} - values={this.protocolItems} - value={this.props.openvpn.protocol} - onSelect={this.onSelectOpenvpnProtocol} + <AriaInputGroup> + <StyledTunnelProtocolContainer> + <StyledTunnelProtocolSelector + title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')} + values={this.tunnelProtocolItems(hasWireguardKey)} + value={this.props.tunnelProtocol} + onSelect={this.onSelectTunnelProtocol} /> + {!hasWireguardKey && ( + <StyledNoWireguardKeyErrorContainer> + <AriaDescription> + <StyledNoWireguardKeyError> + {messages.pgettext( + 'advanced-settings-view', + 'To enable WireGuard, generate a key under the "WireGuard key" setting below.', + )} + </StyledNoWireguardKeyError> + </AriaDescription> + </StyledNoWireguardKeyErrorContainer> + )} + </StyledTunnelProtocolContainer> + </AriaInputGroup> - {this.props.openvpn.protocol ? ( + {this.props.tunnelProtocol !== 'wireguard' ? ( + <AriaInputGroup> + <StyledSelectorContainer> <Selector - title={sprintf( - // TRANSLATORS: The title for the port selector section. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - messages.pgettext('advanced-settings-view', 'OpenVPN %(portType)s port'), - { - portType: this.props.openvpn.protocol.toUpperCase(), - }, + title={messages.pgettext( + 'advanced-settings-view', + 'OpenVPN transport protocol', )} - values={this.portItems[this.props.openvpn.protocol]} - value={this.props.openvpn.port} - onSelect={this.onSelectOpenVpnPort} + values={this.protocolItems} + value={this.props.openvpn.protocol} + onSelect={this.onSelectOpenvpnProtocol} /> - ) : undefined} - </StyledSelectorContainer> + + {this.props.openvpn.protocol ? ( + <Selector + title={sprintf( + // TRANSLATORS: The title for the port selector section. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) + messages.pgettext( + 'advanced-settings-view', + 'OpenVPN %(portType)s port', + ), + { + portType: this.props.openvpn.protocol.toUpperCase(), + }, + )} + values={this.portItems[this.props.openvpn.protocol]} + value={this.props.openvpn.port} + onSelect={this.onSelectOpenVpnPort} + /> + ) : undefined} + </StyledSelectorContainer> + </AriaInputGroup> ) : undefined} {this.props.tunnelProtocol === 'wireguard' ? ( - <StyledSelectorContainer> - <Selector - // TRANSLATORS: The title for the shadowsocks bridge selector section. - title={messages.pgettext('advanced-settings-view', 'WireGuard port')} - values={this.wireguardPortItems} - value={this.props.wireguard.port} - onSelect={this.onSelectWireguardPort} - /> - </StyledSelectorContainer> + <AriaInputGroup> + <StyledSelectorContainer> + <Selector + // TRANSLATORS: The title for the shadowsocks bridge selector section. + title={messages.pgettext('advanced-settings-view', 'WireGuard port')} + values={this.wireguardPortItems} + value={this.props.wireguard.port} + onSelect={this.onSelectWireguardPort} + /> + </StyledSelectorContainer> + </AriaInputGroup> ) : undefined} - <Selector - title={ - // TRANSLATORS: The title for the shadowsocks bridge selector section. - messages.pgettext('advanced-settings-view', 'Bridge mode') - } - values={this.bridgeStateItems} - value={this.props.bridgeState} - onSelect={this.onSelectBridgeState} - /> + <AriaInputGroup> + <Selector + title={ + // TRANSLATORS: The title for the shadowsocks bridge selector section. + messages.pgettext('advanced-settings-view', 'Bridge mode') + } + values={this.bridgeStateItems} + value={this.props.bridgeState} + onSelect={this.onSelectBridgeState} + /> + </AriaInputGroup> - <Cell.Container> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'OpenVPN Mssfix')} - </Cell.Label> - <StyledInputFrame> - <Cell.AutoSizingTextInput - value={this.props.mssfix ? this.props.mssfix.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.pgettext('advanced-settings-view', 'Default')} - onSubmitValue={this.onMssfixSubmit} - validateValue={AdvancedSettings.mssfixIsValid} - submitOnBlur={true} - modifyValue={AdvancedSettings.removeNonNumericCharacters} - /> - </StyledInputFrame> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the Mssfix input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible mssfix value - // TRANSLATORS: %(min)d - the minimum possible mssfix value - messages.pgettext( - 'advanced-settings-view', - 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', - ), - { - min: MIN_MSSFIX_VALUE, - max: MAX_MSSFIX_VALUE, - }, - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'OpenVPN Mssfix')} + </Cell.InputLabel> + </AriaLabel> + <StyledInputFrame> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.mssfix ? this.props.mssfix.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.pgettext('advanced-settings-view', 'Default')} + onSubmitValue={this.onMssfixSubmit} + validateValue={AdvancedSettings.mssfixIsValid} + submitOnBlur={true} + modifyValue={AdvancedSettings.removeNonNumericCharacters} + /> + </AriaInput> + </StyledInputFrame> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the Mssfix input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible mssfix value + // TRANSLATORS: %(min)d - the minimum possible mssfix value + messages.pgettext( + 'advanced-settings-view', + 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_MSSFIX_VALUE, + max: MAX_MSSFIX_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <Cell.Container> - <Cell.Label> - {messages.pgettext('advanced-settings-view', 'WireGuard MTU')} - </Cell.Label> - <StyledInputFrame> - <Cell.AutoSizingTextInput - value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.pgettext('advanced-settings-view', 'Default')} - onSubmitValue={this.onWireguardMtuSubmit} - validateValue={AdvancedSettings.wireguarMtuIsValid} - submitOnBlur={true} - modifyValue={AdvancedSettings.removeNonNumericCharacters} - /> - </StyledInputFrame> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard MTU input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value - // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value - messages.pgettext( - 'advanced-settings-view', - 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', - ), - { - min: MIN_WIREGUARD_MTU_VALUE, - max: MAX_WIREGUARD_MTU_VALUE, - }, - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'WireGuard MTU')} + </Cell.InputLabel> + </AriaLabel> + <StyledInputFrame> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.pgettext('advanced-settings-view', 'Default')} + onSubmitValue={this.onWireguardMtuSubmit} + validateValue={AdvancedSettings.wireguarMtuIsValid} + submitOnBlur={true} + modifyValue={AdvancedSettings.removeNonNumericCharacters} + /> + </AriaInput> + </StyledInputFrame> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard MTU input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value + // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value + messages.pgettext( + 'advanced-settings-view', + 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_WIREGUARD_MTU_VALUE, + max: MAX_WIREGUARD_MTU_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> <StyledBottomCellGroup> <Cell.CellButton onClick={this.props.onViewWireguardKeys}> @@ -410,7 +459,8 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { <AppButton.BlueButton key="back" onClick={this.hideConfirmBlockWhenDisconnectedAlert}> {messages.gettext('Back')} </AppButton.BlueButton>, - ]}> + ]} + close={this.hideConfirmBlockWhenDisconnectedAlert}> <ModalMessage> {messages.pgettext( 'advanced-settings-view', diff --git a/gui/src/renderer/components/AriaInputGroup.tsx b/gui/src/renderer/components/AriaInputGroup.tsx new file mode 100644 index 0000000000..ae668e1314 --- /dev/null +++ b/gui/src/renderer/components/AriaInputGroup.tsx @@ -0,0 +1,91 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; + +let groupCounter = 0; +function getNewId() { + return groupCounter++; +} + +interface IAriaInputContext { + inputId: string; + labelId?: string; + descriptionId?: string; + setHasLabel: (value: boolean) => void; + setHasDescription: (value: boolean) => void; +} + +const missingAriaInputContextError = new Error('Missing AriaInputContext.Provider'); +const AriaInputContext = React.createContext<IAriaInputContext>({ + get inputId(): string { + throw missingAriaInputContextError; + }, + setHasLabel() { + throw missingAriaInputContextError; + }, + setHasDescription() { + throw missingAriaInputContextError; + }, +}); + +interface IAriaInputGroupProps { + children: React.ReactNode; +} + +export function AriaInputGroup(props: IAriaInputGroupProps) { + 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, + }; + + return ( + <AriaInputContext.Provider value={contextValue}>{props.children}</AriaInputContext.Provider> + ); +} + +interface IAriaElementProps { + children: React.ReactElement; +} + +export function AriaInput(props: IAriaElementProps) { + const { inputId, labelId, descriptionId } = useContext(AriaInputContext); + + return React.cloneElement(props.children, { + id: inputId, + 'aria-labelledby': labelId, + 'aria-describedby': descriptionId, + }); +} + +export function AriaLabel(props: IAriaElementProps) { + const { inputId, labelId, setHasLabel } = useContext(AriaInputContext); + + useEffect(() => { + setHasLabel(true); + return () => setHasLabel(false); + }, []); + + return React.cloneElement(props.children, { + id: labelId, + htmlFor: inputId, + }); +} + +export function AriaDescription(props: IAriaElementProps) { + const { descriptionId, setHasDescription } = useContext(AriaInputContext); + + useEffect(() => { + setHasDescription(true); + return () => setHasDescription(false); + }, []); + + return React.cloneElement(props.children, { + id: descriptionId, + }); +} diff --git a/gui/src/renderer/components/Cell.tsx b/gui/src/renderer/components/Cell.tsx index 8aa4ddde78..80c48fbd0f 100644 --- a/gui/src/renderer/components/Cell.tsx +++ b/gui/src/renderer/components/Cell.tsx @@ -72,6 +72,11 @@ export function Label(props: React.HTMLAttributes<HTMLDivElement>) { return <StyledLabel disabled={disabled} {...props} />; } +export function InputLabel(props: React.LabelHTMLAttributes<HTMLLabelElement>) { + const disabled = useContext(CellDisabledContext); + return <StyledLabel as="label" disabled={disabled} {...props} />; +} + export function SubText(props: React.HTMLAttributes<HTMLDivElement>) { const disabled = useContext(CellDisabledContext); return <StyledSubText disabled={disabled} {...props} />; @@ -216,7 +221,7 @@ export function AutoSizingTextInput({ onChangeValue, ...otherProps }: IInputProp <StyledAutoSizingTextInputWrapper> <Input onChangeValue={onChangeValueWrapper} {...otherProps} /> </StyledAutoSizingTextInputWrapper> - <StyledAutoSizingTextInputFiller className={otherProps.className}> + <StyledAutoSizingTextInputFiller className={otherProps.className} aria-hidden={true}> {value === '' ? otherProps.placeholder : value} </StyledAutoSizingTextInputFiller> </StyledAutoSizingTextInputContainer> diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index 95177765a5..d64c780808 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -194,7 +194,8 @@ export default class ExpiredAccountErrorView extends React.Component< onClick={this.onCloseBlockWhenDisconnectedInstructions}> {messages.gettext('Close')} </AppButton.BlueButton>, - ]}> + ]} + close={this.onCloseBlockWhenDisconnectedInstructions}> <ModalMessage> {messages.pgettext( 'connect-view', diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx index 914f5d818f..c5b5521f05 100644 --- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx @@ -274,6 +274,7 @@ function ApplicationRow(props: IApplicationRowProps) { iconColor={warningColor} message={warningMessage} buttons={warningDialogButtons} + close={hideWarningDialog} /> )} </> diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index 3db4dcd626..b045803626 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -1,12 +1,10 @@ -import * as React from 'react'; +import React, { useContext, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { Scheduler } from '../../shared/scheduler'; import ImageView from './ImageView'; -const MODAL_CONTAINER_ID = 'modalContainer'; - const ModalContent = styled.div({ position: 'absolute', display: 'flex', @@ -39,11 +37,35 @@ interface IModalContainerProps { children?: React.ReactNode; } +interface IModalContext { + activeModal: boolean; + setActiveModal: (value: boolean) => void; + modalContainerRef: React.RefObject<HTMLDivElement>; +} + +const noActiveModalContextError = new Error('ActiveModalContext.Provider missing'); +const ActiveModalContext = React.createContext<IModalContext>({ + get activeModal(): boolean { + throw noActiveModalContextError; + }, + setActiveModal(_value) { + throw noActiveModalContextError; + }, + get modalContainerRef(): React.RefObject<HTMLDivElement> { + throw noActiveModalContextError; + }, +}); + export function ModalContainer(props: IModalContainerProps) { + const [activeModal, setActiveModal] = useState(false); + const modalContainerRef = useRef() as React.RefObject<HTMLDivElement>; + return ( - <StyledModalContainer id={MODAL_CONTAINER_ID}> - <ModalContent>{props.children}</ModalContent> - </StyledModalContainer> + <ActiveModalContext.Provider value={{ activeModal, setActiveModal, modalContainerRef }}> + <StyledModalContainer ref={modalContainerRef}> + <ModalContent aria-hidden={activeModal}>{props.children}</ModalContent> + </StyledModalContainer> + </ActiveModalContext.Provider> ); } @@ -86,18 +108,24 @@ interface IModalAlertProps { message?: string; buttons: React.ReactNode[]; children?: React.ReactNode; + close?: () => void; +} + +export function ModalAlert(props: IModalAlertProps) { + const activeModalContext = useContext(ActiveModalContext); + return <ModalAlertWithContext {...activeModalContext} {...props} />; } -export class ModalAlert extends React.Component<IModalAlertProps> { +class ModalAlertWithContext extends React.Component<IModalAlertProps & IModalContext> { private element = document.createElement('div'); - private modalContainer?: Element; private appendScheduler = new Scheduler(); public componentDidMount() { - const modalContainer = document.getElementById(MODAL_CONTAINER_ID); - if (modalContainer) { - this.modalContainer = modalContainer; + this.props.setActiveModal(true); + document.addEventListener('keydown', this.handleKeyPress); + const modalContainer = this.props.modalContainerRef.current; + if (modalContainer) { // Mounting the container element immediately results in a graphical issue with the dialog // first rendering with the wrong proportions and then changing to the correct proportions. // Postponing it to the next event cycle solves this issue. @@ -110,11 +138,11 @@ export class ModalAlert extends React.Component<IModalAlertProps> { } public componentWillUnmount() { - this.appendScheduler.cancel(); + this.props.setActiveModal(false); + document.removeEventListener('keydown', this.handleKeyPress); - if (this.modalContainer) { - this.modalContainer.removeChild(this.element); - } + this.appendScheduler.cancel(); + this.props.modalContainerRef.current?.removeChild(this.element); } public render() { @@ -125,7 +153,7 @@ export class ModalAlert extends React.Component<IModalAlertProps> { return ( <ModalBackground> <ModalAlertContainer> - <StyledModalAlert> + <StyledModalAlert role="alertdialog"> {this.props.type && ( <ModalAlertIcon>{this.renderTypeIcon(this.props.type)}</ModalAlertIcon> )} @@ -157,6 +185,12 @@ export class ModalAlert extends React.Component<IModalAlertProps> { <ImageView height={44} width={44} source={source} tintColor={this.props.iconColor ?? color} /> ); } + + private handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.props.close?.(); + } + }; } export const ModalMessage = styled.span({ diff --git a/gui/src/renderer/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx index 0b56c53221..79bd94706c 100644 --- a/gui/src/renderer/components/Preferences.tsx +++ b/gui/src/renderer/components/Preferences.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { messages } from '../../shared/gettext'; +import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaInputGroup'; import * as Cell from './Cell'; import { Layout } from './Layout'; import { @@ -62,118 +63,181 @@ export default class Preferences extends React.Component<IProps> { </SettingsHeader> <StyledContent> - <Cell.Container> - <Cell.Label> - {messages.pgettext('preferences-view', 'Launch app on start-up')} - </Cell.Label> - <Cell.Switch isOn={this.props.autoStart} onChange={this.props.setAutoStart} /> - </Cell.Container> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Launch app on start-up')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch isOn={this.props.autoStart} onChange={this.props.setAutoStart} /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> <StyledSeparator /> - <Cell.Container> - <Cell.Label>{messages.pgettext('preferences-view', 'Auto-connect')}</Cell.Label> - <Cell.Switch isOn={this.props.autoConnect} onChange={this.props.setAutoConnect} /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Automatically connect to a server when the app launches.', - )} - </Cell.FooterText> - </Cell.Footer> - - <Cell.Container> - <Cell.Label> - {messages.pgettext('preferences-view', 'Local network sharing')} - </Cell.Label> - <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Allows access to other devices on the same network for sharing, printing etc.', - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Auto-connect')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.autoConnect} + onChange={this.props.setAutoConnect} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Automatically connect to a server when the app launches.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <Cell.Container> - <Cell.Label>{messages.pgettext('preferences-view', 'Notifications')}</Cell.Label> - <Cell.Switch - isOn={this.props.enableSystemNotifications} - onChange={this.props.setEnableSystemNotifications} - /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Enable or disable system notifications. The critical notifications will always be displayed.', - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Local network sharing')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Allows access to other devices on the same network for sharing, printing etc.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - <Cell.Container> - <Cell.Label> - {messages.pgettext('preferences-view', 'Monochromatic tray icon')} - </Cell.Label> - <Cell.Switch - isOn={this.props.monochromaticIcon} - onChange={this.props.setMonochromaticIcon} - /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {messages.pgettext( - 'preferences-view', - 'Use a monochromatic tray icon instead of a colored one.', - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Notifications')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.enableSystemNotifications} + onChange={this.props.setEnableSystemNotifications} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Enable or disable system notifications. The critical notifications will always be displayed.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - {this.props.enableStartMinimizedToggle ? ( - <React.Fragment> - <Cell.Container> - <Cell.Label> - {messages.pgettext('preferences-view', 'Start minimized')} - </Cell.Label> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Monochromatic tray icon')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> <Cell.Switch - isOn={this.props.startMinimized} - onChange={this.props.setStartMinimized} + isOn={this.props.monochromaticIcon} + onChange={this.props.setMonochromaticIcon} /> - </Cell.Container> - <Cell.Footer> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> <Cell.FooterText> {messages.pgettext( 'preferences-view', - 'Show only the tray icon when the app starts.', + 'Use a monochromatic tray icon instead of a colored one.', )} </Cell.FooterText> - </Cell.Footer> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + + {this.props.enableStartMinimizedToggle ? ( + <React.Fragment> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Start minimized')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.startMinimized} + onChange={this.props.setStartMinimized} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {messages.pgettext( + 'preferences-view', + 'Show only the tray icon when the app starts.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> </React.Fragment> ) : undefined} - <Cell.Container disabled={this.props.isBeta}> - <Cell.Label>{messages.pgettext('preferences-view', 'Beta program')}</Cell.Label> - <Cell.Switch - isOn={this.props.showBetaReleases} - onChange={this.props.setShowBetaReleases} - /> - </Cell.Container> - <Cell.Footer> - <Cell.FooterText> - {this.props.isBeta - ? messages.pgettext( - 'preferences-view', - 'This option is unavailable while using a beta version.', - ) - : messages.pgettext( - 'preferences-view', - 'Enable to get notified when new beta versions of the app are released.', - )} - </Cell.FooterText> - </Cell.Footer> + <AriaInputGroup> + <Cell.Container disabled={this.props.isBeta}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Beta program')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={this.props.showBetaReleases} + onChange={this.props.setShowBetaReleases} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {this.props.isBeta + ? messages.pgettext( + 'preferences-view', + 'This option is unavailable while using a beta version.', + ) + : messages.pgettext( + 'preferences-view', + 'Enable to get notified when new beta versions of the app are released.', + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> </StyledContent> </NavigationScrollbars> </NavigationContainer> diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx index 7ca83c43d1..29a84f868c 100644 --- a/gui/src/renderer/components/RedeemVoucher.tsx +++ b/gui/src/renderer/components/RedeemVoucher.tsx @@ -198,7 +198,8 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) { <AppButton.BlueButton key="cancel" disabled={cancelDisabled} onClick={props.onClose}> {messages.pgettext('redeem-voucher-alert', 'Cancel')} </AppButton.BlueButton>, - ]}> + ]} + close={props.onClose}> <StyledLabel>{messages.pgettext('redeem-voucher-alert', 'Enter voucher code')}</StyledLabel> <RedeemVoucherInput /> <RedeemVoucherResponse /> diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx index 295855a9a6..c685ebe946 100644 --- a/gui/src/renderer/components/Support.tsx +++ b/gui/src/renderer/components/Support.tsx @@ -252,6 +252,7 @@ export default class Support extends React.Component<ISupportProps, ISupportStat {messages.gettext('Back')} </AppButton.BlueButton>, ]} + close={this.onCancelNoEmailDialog} /> ); } @@ -286,6 +287,7 @@ export default class Support extends React.Component<ISupportProps, ISupportStat {messages.gettext('Cancel')} </AppButton.BlueButton>, ]} + close={this.props.onClose} /> ); } diff --git a/gui/src/renderer/components/Switch.tsx b/gui/src/renderer/components/Switch.tsx index 0578064e71..f04ac91d39 100644 --- a/gui/src/renderer/components/Switch.tsx +++ b/gui/src/renderer/components/Switch.tsx @@ -3,6 +3,9 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; interface IProps { + id?: string; + 'aria-labelledby'?: string; + 'aria-describedby'?: string; isOn: boolean; onChange?: (isOn: boolean) => void; className?: string; @@ -47,7 +50,7 @@ const Knob = styled.div({}, (props: { isOn: boolean; isPressed: boolean; disable }; }); -export default class Switch extends React.Component<IProps, IState> { +export default class Switch extends React.PureComponent<IProps, IState> { public state: IState = { isOn: this.props.isOn, isPressed: false, @@ -59,14 +62,6 @@ export default class Switch extends React.Component<IProps, IState> { private startPos = 0; private changedDuringPan = false; - public shouldComponentUpdate(nextProps: IProps, nextState: IState) { - return ( - nextState.isOn !== this.state.isOn || - nextState.isPressed !== this.state.isPressed || - nextProps.isOn !== this.props.isOn - ); - } - public componentDidUpdate(prevProps: IProps, _prevState: IState) { if ( this.props.isOn !== prevProps.isOn && @@ -80,11 +75,15 @@ export default class Switch extends React.Component<IProps, IState> { public render() { return ( <SwitchContainer + id={this.props.id} role="checkbox" - aria-checked={this.state.isOn} + aria-labelledby={this.props['aria-labelledby']} + aria-describedby={this.props['aria-describedby']} + aria-checked={this.props.isOn} ref={this.containerRef} onClick={this.handleClick} disabled={this.props.disabled ?? false} + aria-disabled={this.props.disabled ?? false} className={this.props.className}> <Knob disabled={this.props.disabled ?? false} |
