diff options
6 files changed, 363 insertions, 233 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts index 61c3b17b9d..0bc0d9d336 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts @@ -88,6 +88,42 @@ export const useGetFeatureIndicator = () => { }); }, [history]); + const gotoMtuFeature = React.useCallback(() => { + history.push(RoutePath.wireguardSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'mtu-setting', + }, + ], + }); + }, [history]); + + const gotoQuantumResistantFeature = React.useCallback(() => { + history.push(RoutePath.wireguardSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'quantum-resistant-setting', + }, + ], + }); + }, [history]); + + const gotoObfuscation = React.useCallback(() => { + history.push(RoutePath.wireguardSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'obfuscation-setting', + }, + ], + }); + }, [history]); + const featureMap: Record<FeatureIndicator, { label: string; onClick?: () => void }> = { [FeatureIndicator.daita]: { label: strings.daita, onClick: gotoDaitaFeature }, [FeatureIndicator.daitaMultihop]: { @@ -101,14 +137,20 @@ export const useGetFeatureIndicator = () => { DAITA: strings.daita, }, ), + onClick: gotoDaitaFeature, }, [FeatureIndicator.udp2tcp]: { label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + onClick: gotoObfuscation, }, [FeatureIndicator.shadowsocks]: { label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + onClick: gotoObfuscation, + }, + [FeatureIndicator.quic]: { + label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + onClick: gotoObfuscation, }, - [FeatureIndicator.quic]: { label: messages.pgettext('wireguard-settings-view', 'Obfuscation') }, [FeatureIndicator.multihop]: { label: // TRANSLATORS: This refers to the multihop setting in the VPN settings view. This is @@ -123,7 +165,10 @@ export const useGetFeatureIndicator = () => { messages.gettext('Custom DNS'), onClick: gotoCustomDnsFeature, }, - [FeatureIndicator.customMtu]: { label: messages.pgettext('wireguard-settings-view', 'MTU') }, + [FeatureIndicator.customMtu]: { + label: messages.pgettext('wireguard-settings-view', 'MTU'), + onClick: gotoMtuFeature, + }, [FeatureIndicator.bridgeMode]: { label: messages.pgettext('openvpn-settings-view', 'Bridge mode'), }, @@ -145,10 +190,13 @@ export const useGetFeatureIndicator = () => { [FeatureIndicator.serverIpOverride]: { label: messages.pgettext('settings-import', 'Server IP override'), }, - [FeatureIndicator.quantumResistance]: - // TRANSLATORS: This refers to the quantum resistance setting in the WireGuard settings view. - // TRANSLATORS: This is displayed when the feature is on. - { label: messages.gettext('Quantum resistance') }, + [FeatureIndicator.quantumResistance]: { + label: + // TRANSLATORS: This refers to the quantum resistance setting in the WireGuard settings view. + // TRANSLATORS: This is displayed when the feature is on. + messages.gettext('Quantum resistance'), + onClick: gotoQuantumResistantFeature, + }, [FeatureIndicator.dnsContentBlockers]: { label: messages.pgettext('vpn-settings-view', 'DNS content blockers'), onClick: gotoDnsContentBlockersFeature, diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/ip-version-setting/IpVersionSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/ip-version-setting/IpVersionSetting.tsx index 4d72450bd3..4ff48fdc88 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/ip-version-setting/IpVersionSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/ip-version-setting/IpVersionSetting.tsx @@ -1,20 +1,15 @@ import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; import { strings } from '../../../../../../shared/constants'; import { IpVersion, wrapConstraint } from '../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useRelaySettingsUpdater } from '../../../../../lib/constraint-updater'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInputGroup } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; - -const StyledSelectorContainer = styled.div({ - flex: 0, -}); +import { DefaultListboxOption } from '../../../../default-listbox-option'; export function IpVersionSetting() { const relaySettingsUpdater = useRelaySettingsUpdater(); @@ -24,19 +19,7 @@ export function IpVersionSetting() { return ipVersion === 'any' ? null : ipVersion; }, [relaySettings]); - const ipVersionItems: SelectorItem<IpVersion>[] = useMemo( - () => [ - { - label: messages.gettext('IPv4'), - value: 'ipv4', - }, - { - label: messages.gettext('IPv6'), - value: 'ipv6', - }, - ], - [], - ); + const scrollToAnchor = useScrollToListItem(); const setIpVersion = useCallback( async (ipVersion: IpVersion | null) => { @@ -54,33 +37,36 @@ export function IpVersionSetting() { ); return ( - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - // TRANSLATORS: The title for the WireGuard IP version selector. - title={messages.pgettext('wireguard-settings-view', 'IP version')} - items={ipVersionItems} - value={ipVersion} - onSelect={setIpVersion} - automaticValue={null} - /> - </StyledSelectorContainer> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard IP version selector. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" - messages.pgettext( - 'wireguard-settings-view', - 'This allows access to %(wireguard)s for devices that only support IPv6.', - ), - { wireguard: strings.wireguard }, - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> + <Listbox value={ipVersion} onValueChange={setIpVersion} animation={scrollToAnchor?.animation}> + <Listbox.Item> + <Listbox.Content> + <Listbox.Label> + { + // TRANSLATORS: The title for the WireGuard IP version selector. + messages.pgettext('wireguard-settings-view', 'IP version') + } + </Listbox.Label> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={null}>{messages.gettext('Automatic')}</DefaultListboxOption> + <DefaultListboxOption value={'ipv4'}>{messages.gettext('IPv4')}</DefaultListboxOption> + <DefaultListboxOption value={'ipv6'}>{messages.gettext('IPv6')}</DefaultListboxOption> + </Listbox.Options> + <Listbox.Footer> + <Listbox.Text> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard IP version selector. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" + messages.pgettext( + 'wireguard-settings-view', + 'This allows access to %(wireguard)s for devices that only support IPv6.', + ), + { wireguard: strings.wireguard }, + )} + </Listbox.Text> + </Listbox.Footer> + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/mtu-setting/MtuSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/mtu-setting/MtuSetting.tsx index 3b8ed56c81..4b4acbb929 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/mtu-setting/MtuSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/mtu-setting/MtuSetting.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import React, { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { strings } from '../../../../../../shared/constants'; @@ -6,9 +6,10 @@ import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { removeNonNumericCharacters } from '../../../../../../shared/string-helpers'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; +import { ListItem } from '../../../../../lib/components/list-item'; +import { useTextField } from '../../../../../lib/components/text-field'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; const MIN_WIREGUARD_MTU_VALUE = 1280; const MAX_WIREGUARD_MTU_VALUE = 1420; @@ -25,6 +26,14 @@ export function MtuSetting() { const { setWireguardMtu: setWireguardMtuImpl } = useAppContext(); const mtu = useSelector((state) => state.settings.wireguard.mtu); + const id = 'mtu-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + + const inputRef = React.useRef<HTMLInputElement>(null); + const labelId = React.useId(); + const descriptionId = React.useId(); + const setMtu = useCallback( async (mtu?: number) => { try { @@ -47,47 +56,80 @@ export function MtuSetting() { [setMtu], ); + const { value, handleChange, invalid, dirty, blur, reset } = useTextField({ + inputRef, + defaultValue: mtu ? mtu.toString() : '', + format: removeNonNumericCharacters, + validate: mtuIsValid, + }); + + const handleBlur = React.useCallback(async () => { + if (!invalid && dirty) { + await onSubmit(value); + } + if (invalid) { + reset(); + } + }, [dirty, invalid, onSubmit, reset, value]); + + const handleSubmit = React.useCallback( + async (event: React.FormEvent) => { + event.preventDefault(); + if (!invalid) { + await onSubmit(value); + blur(); + } + }, + [blur, invalid, onSubmit, value], + ); + return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel>{messages.pgettext('wireguard-settings-view', 'MTU')}</Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.AutoSizingTextInput - initialValue={mtu ? mtu.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.gettext('Default')} - onSubmitValue={onSubmit} - validateValue={mtuIsValid} - submitOnBlur={true} - modifyValue={removeNonNumericCharacters} - /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard MTU input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" - // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value - // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value - messages.pgettext( - 'wireguard-settings-view', - 'Set %(wireguard)s MTU value. Valid range: %(min)d - %(max)d.', - ), - { - wireguard: strings.wireguard, - min: MIN_WIREGUARD_MTU_VALUE, - max: MAX_WIREGUARD_MTU_VALUE, - }, - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> + <ListItem animation={scrollToAnchor?.animation}> + <ListItem.Item ref={ref}> + <ListItem.Content> + <ListItem.Label id={labelId}> + { + // TRANSLATORS: The title for the WireGuard MTU setting. MTU stands for Maximum + // TRANSLATORS: Transmission Unit and controls the maximum size of packets sent over + // TRANSLATORS: the VPN tunnel. + messages.pgettext('wireguard-settings-view', 'MTU') + } + </ListItem.Label> + <ListItem.TextField invalid={invalid} onSubmit={handleSubmit}> + <ListItem.TextField.Input + ref={inputRef} + value={value} + placeholder={messages.gettext('Default')} + inputMode="numeric" + maxLength={4} + aria-labelledby={labelId} + aria-describedby={descriptionId} + onBlur={handleBlur} + onChange={handleChange} + /> + </ListItem.TextField> + </ListItem.Content> + </ListItem.Item> + <ListItem.Footer> + <ListItem.Text id={descriptionId}> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard MTU input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" + // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value + // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value + messages.pgettext( + 'wireguard-settings-view', + 'Set %(wireguard)s MTU value. Valid range: %(min)d - %(max)d.', + ), + { + wireguard: strings.wireguard, + min: MIN_WIREGUARD_MTU_VALUE, + max: MAX_WIREGUARD_MTU_VALUE, + }, + )} + </ListItem.Text> + </ListItem.Footer> + </ListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx index 8ad91f387e..7a9f70590c 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx @@ -1,19 +1,20 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; +import React from 'react'; import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; import { Constraint, ObfuscationType } from '../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../shared/gettext'; import { RoutePath } from '../../../../../../shared/routes'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Text } from '../../../../../lib/components'; +import { FlexColumn } from '../../../../../lib/components/flex-column'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useSelector } from '../../../../../redux/store'; -import { AriaInputGroup } from '../../../../AriaGroup'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; +import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; - -const StyledSelectorContainer = styled.div({ - flex: 0, -}); +import { SplitListboxOption } from '../../../../split-listbox-option'; export function formatPortForSubLabel(port: Constraint<number>): string { return port === 'any' ? messages.gettext('Automatic') : `${port.only}`; @@ -23,51 +24,16 @@ export function ObfuscationSettings() { const { setObfuscationSettings } = useAppContext(); const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings); + const id = 'obfuscation-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + // TRANSLATORS: Text showing currently selected port. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(port)s - Can be either a number between 1 and 65535 or the text "Automatic". const subLabelTemplate = messages.pgettext('wireguard-settings-view', 'Port: %(port)s'); const obfuscationType = obfuscationSettings.selectedObfuscation; - const obfuscationTypeItems: SelectorItem<ObfuscationType>[] = useMemo( - () => [ - { - label: messages.pgettext('wireguard-settings-view', 'Shadowsocks'), - subLabel: sprintf(subLabelTemplate, { - port: formatPortForSubLabel(obfuscationSettings.shadowsocksSettings.port), - }), - value: ObfuscationType.shadowsocks, - details: { - path: RoutePath.shadowsocks, - ariaLabel: messages.pgettext('accessibility', 'Shadowsocks settings'), - }, - }, - { - label: messages.pgettext('wireguard-settings-view', 'UDP-over-TCP'), - subLabel: sprintf(subLabelTemplate, { - port: formatPortForSubLabel(obfuscationSettings.udp2tcpSettings.port), - }), - value: ObfuscationType.udp2tcp, - details: { - path: RoutePath.udpOverTcp, - ariaLabel: messages.pgettext('accessibility', 'UDP-over-TCP settings'), - }, - }, - { - label: messages.pgettext('wireguard-settings-view', 'QUIC'), - value: ObfuscationType.quic, - }, - { - label: messages.gettext('Off'), - value: ObfuscationType.off, - }, - ], - [ - obfuscationSettings.shadowsocksSettings.port, - obfuscationSettings.udp2tcpSettings.port, - subLabelTemplate, - ], - ); const selectObfuscationType = useCallback( async (value: ObfuscationType) => { @@ -80,12 +46,19 @@ export function ObfuscationSettings() { ); return ( - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - // TRANSLATORS: The title for the WireGuard obfuscation selector. - title={messages.pgettext('wireguard-settings-view', 'Obfuscation')} - details={ + <Listbox + onValueChange={selectObfuscationType} + value={obfuscationType} + animation={scrollToAnchor?.animation}> + <Listbox.Item ref={ref}> + <Listbox.Content> + <Listbox.Label> + { + // TRANSLATORS: The title for the WireGuard obfuscation selector. + messages.pgettext('wireguard-settings-view', 'Obfuscation') + } + </Listbox.Label> + <InfoButton> <ModalMessage> { // TRANSLATORS: Describes what WireGuard obfuscation does, how it works and when @@ -96,14 +69,56 @@ export function ObfuscationSettings() { ) } </ModalMessage> - } - items={obfuscationTypeItems} - value={obfuscationType} - onSelect={selectObfuscationType} - automaticValue={ObfuscationType.auto} - automaticTestId="automatic-obfuscation" - /> - </StyledSelectorContainer> - </AriaInputGroup> + </InfoButton> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={ObfuscationType.auto} data-testid="automatic-obfuscation"> + {messages.gettext('Automatic')} + </DefaultListboxOption> + <SplitListboxOption value={ObfuscationType.shadowsocks}> + <SplitListboxOption.Item> + <FlexColumn> + <Listbox.Option.Label> + {messages.pgettext('wireguard-settings-view', 'Shadowsocks')} + </Listbox.Option.Label> + <Text variant="labelTiny" color="whiteAlpha60"> + {sprintf(subLabelTemplate, { + port: formatPortForSubLabel(obfuscationSettings.shadowsocksSettings.port), + })} + </Text> + </FlexColumn> + </SplitListboxOption.Item> + <SplitListboxOption.NavigateButton + to={RoutePath.shadowsocks} + aria-description={messages.pgettext('accessibility', 'Shadowsocks settings')} + /> + </SplitListboxOption> + <SplitListboxOption value={ObfuscationType.udp2tcp}> + <SplitListboxOption.Item> + <FlexColumn> + <Listbox.Option.Label> + {messages.pgettext('wireguard-settings-view', 'UDP-over-TCP')} + </Listbox.Option.Label> + <Text variant="labelTiny" color="whiteAlpha60"> + {sprintf(subLabelTemplate, { + port: formatPortForSubLabel(obfuscationSettings.udp2tcpSettings.port), + })} + </Text> + </FlexColumn> + </SplitListboxOption.Item> + <SplitListboxOption.NavigateButton + to={RoutePath.udpOverTcp} + aria-description={messages.pgettext('accessibility', 'UDP-over-TCP settings')} + /> + </SplitListboxOption> + <DefaultListboxOption value={ObfuscationType.quic}> + {messages.pgettext('wireguard-settings-view', 'QUIC')} + </DefaultListboxOption> + <DefaultListboxOption value={ObfuscationType.off}> + {messages.gettext('Off')} + </DefaultListboxOption> + </Listbox.Options> + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx index c0fc48945c..79f3294c73 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx @@ -1,16 +1,20 @@ import { useCallback, useMemo } from 'react'; +import React from 'react'; import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; import { wrapConstraint } from '../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { removeNonNumericCharacters } from '../../../../../../shared/string-helpers'; import { isInRanges } from '../../../../../../shared/utils'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useRelaySettingsUpdater } from '../../../../../lib/constraint-updater'; import { useSelector } from '../../../../../redux/store'; -import { AriaInputGroup } from '../../../../AriaGroup'; -import { SelectorItem, SelectorWithCustomItem } from '../../../../cell/Selector'; +import { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; +import InfoButton from '../../../../InfoButton'; +import { InputListboxOption } from '../../../../input-listbox-option'; import { ModalMessage } from '../../../../Modal'; const WIREUGARD_UDP_PORTS = [51820, 53]; @@ -18,31 +22,45 @@ const WIREUGARD_UDP_PORTS = [51820, 53]; function mapPortToSelectorItem(value: number): SelectorItem<number> { return { label: value.toString(), value }; } - -const StyledSelectorContainer = styled.div({ - flex: 0, -}); - export function PortSetting() { const relaySettings = useSelector((state) => state.settings.relaySettings); const relaySettingsUpdater = useRelaySettingsUpdater(); const allowedPortRanges = useSelector((state) => state.settings.wireguardEndpointData.portRanges); + const id = 'port-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + const wireguardPortItems = useMemo<Array<SelectorItem<number>>>( () => WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), [], ); - const port = useMemo(() => { + const selectedOption = useMemo(() => { const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : 'any'; - return port === 'any' ? null : port; + if (port === 'any') + return { + port: 'any', + value: null, + }; + if (port && !WIREUGARD_UDP_PORTS.includes(port)) + return { + port, + value: 'custom', + }; + return { + port, + value: port, + }; }, [relaySettings]); const setWireguardPort = useCallback( - async (port: number | null) => { + async (port: number | string | null) => { try { await relaySettingsUpdater((settings) => { - settings.wireguardConstraints.port = wrapConstraint(port); + settings.wireguardConstraints.port = wrapConstraint( + typeof port === 'string' ? parseInt(port) : port, + ); return settings; }); } catch (e) { @@ -53,33 +71,38 @@ export function PortSetting() { [relaySettingsUpdater], ); - const parseValue = useCallback((port: string) => parseInt(port), []); - const validateValue = useCallback( (value: number) => isInRanges(value, allowedPortRanges), [allowedPortRanges], ); + const validateStringValue = useCallback( + (value: string) => { + const numericValue = parseInt(value, 10); + if (Number.isNaN(numericValue)) return false; + return validateValue(numericValue); + }, + [validateValue], + ); + const portRangesText = allowedPortRanges .map(([start, end]) => (start === end ? start : `${start}-${end}`)) .join(', '); return ( - <AriaInputGroup> - <StyledSelectorContainer> - <SelectorWithCustomItem - // TRANSLATORS: The title for the WireGuard port selector. - title={messages.pgettext('wireguard-settings-view', 'Port')} - items={wireguardPortItems} - value={port} - onSelect={setWireguardPort} - inputPlaceholder={messages.pgettext('wireguard-settings-view', 'Port')} - automaticValue={null} - parseValue={parseValue} - modifyValue={removeNonNumericCharacters} - validateValue={validateValue} - maxLength={5} - details={ + <Listbox + value={selectedOption.value} + onValueChange={setWireguardPort} + animation={scrollToAnchor?.animation}> + <Listbox.Item ref={ref}> + <Listbox.Content> + <Listbox.Label> + { + // TRANSLATORS: The title for the WireGuard port selector. + messages.pgettext('wireguard-settings-view', 'Port') + } + </Listbox.Label> + <InfoButton> <> <ModalMessage> {messages.pgettext( @@ -97,9 +120,31 @@ export function PortSetting() { )} </ModalMessage> </> - } - /> - </StyledSelectorContainer> - </AriaInputGroup> + </InfoButton> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={null}>{messages.gettext('Automatic')}</DefaultListboxOption> + {wireguardPortItems.map((item) => ( + <DefaultListboxOption key={item.value} value={item.value}> + {item.label} + </DefaultListboxOption> + ))} + <InputListboxOption value="custom"> + <InputListboxOption.Label>{messages.gettext('Custom')}</InputListboxOption.Label> + <InputListboxOption.Input + initialValue={ + selectedOption.value === 'custom' ? selectedOption.port?.toString() : undefined + } + placeholder={messages.pgettext('wireguard-settings-view', 'Port')} + maxLength={5} + type="text" + inputMode="numeric" + validate={validateStringValue} + format={removeNonNumericCharacters} + /> + </InputListboxOption> + </Listbox.Options> + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/quantum-resistant-setting/QuantumResistantSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/quantum-resistant-setting/QuantumResistantSetting.tsx index f853d913a4..8b5450b66e 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/quantum-resistant-setting/QuantumResistantSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/quantum-resistant-setting/QuantumResistantSetting.tsx @@ -1,34 +1,22 @@ -import { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; +import { useCallback } from 'react'; +import React from 'react'; import { messages } from '../../../../../../shared/gettext'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useSelector } from '../../../../../redux/store'; -import { AriaInputGroup } from '../../../../AriaGroup'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; +import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; -const StyledSelectorContainer = styled.div({ - flex: 0, -}); - export function QuantumResistantSetting() { const { setWireguardQuantumResistant } = useAppContext(); const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); - const items: SelectorItem<boolean>[] = useMemo( - () => [ - { - label: messages.gettext('On'), - value: true, - }, - { - label: messages.gettext('Off'), - value: false, - }, - ], - [], - ); + const id = 'quantum-resistant-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToListItem = useScrollToListItem(ref, id); const selectQuantumResistant = useCallback( async (quantumResistant: boolean | null) => { @@ -38,16 +26,21 @@ export function QuantumResistantSetting() { ); return ( - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - title={ - // TRANSLATORS: The title for the WireGuard quantum resistance selector. This setting - // TRANSLATORS: makes the cryptography resistant to the future abilities of quantum - // TRANSLATORS: computers. - messages.pgettext('wireguard-settings-view', 'Quantum-resistant tunnel') - } - details={ + <Listbox + animation={scrollToListItem?.animation} + value={quantumResistant ?? null} + onValueChange={selectQuantumResistant}> + <Listbox.Item ref={ref}> + <Listbox.Content> + <Listbox.Label> + { + // TRANSLATORS: The title for the WireGuard quantum resistance selector. This setting + // TRANSLATORS: makes the cryptography resistant to the future abilities of quantum + // TRANSLATORS: computers. + messages.pgettext('wireguard-settings-view', 'Quantum-resistant tunnel') + } + </Listbox.Label> + <InfoButton> <> <ModalMessage> {messages.pgettext( @@ -62,13 +55,14 @@ export function QuantumResistantSetting() { )} </ModalMessage> </> - } - items={items} - value={quantumResistant ?? null} - onSelect={selectQuantumResistant} - automaticValue={null} - /> - </StyledSelectorContainer> - </AriaInputGroup> + </InfoButton> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={null}>{messages.gettext('Automatic')}</DefaultListboxOption> + <DefaultListboxOption value={true}>{messages.gettext('On')}</DefaultListboxOption> + <DefaultListboxOption value={false}>{messages.gettext('Off')}</DefaultListboxOption> + </Listbox.Options> + </Listbox> ); } |
