summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-09-02 07:13:31 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:43 +0200
commit0c2254cfcde677dcf67063f21daa31d765b73d87 (patch)
tree12582cb291b19e12198e1d01fbaf260fd10ab810
parenta59f1975dda0afeccc289b2e271c6577fa7a5dc9 (diff)
downloadmullvadvpn-0c2254cfcde677dcf67063f21daa31d765b73d87.tar.xz
mullvadvpn-0c2254cfcde677dcf67063f21daa31d765b73d87.zip
Link feature indicators to settings in wireguard settings
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts60
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/ip-version-setting/IpVersionSetting.tsx84
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/mtu-setting/MtuSetting.tsx130
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx139
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx111
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/quantum-resistant-setting/QuantumResistantSetting.tsx72
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>
);
}