diff options
| author | Oliver <oliver@mohlin.dev> | 2025-08-28 07:45:23 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:43 +0200 |
| commit | c85e3bfb05fadf74e6d472605acbc5fecb457531 (patch) | |
| tree | 71e2a16d32bd97c1353152bc73dd47df1c4caebf | |
| parent | 49648f534772799f2e994cd939fc7f1ee94d816b (diff) | |
| download | mullvadvpn-c85e3bfb05fadf74e6d472605acbc5fecb457531.tar.xz mullvadvpn-c85e3bfb05fadf74e6d472605acbc5fecb457531.zip | |
Link feature indicators to settings in vpn settings
25 files changed, 690 insertions, 717 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/FeatureIndicators.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/FeatureIndicators.tsx index 20d5691336..b2cadb7aac 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/FeatureIndicators.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/FeatureIndicators.tsx @@ -2,14 +2,14 @@ import { useEffect, useRef } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; -import { strings } from '../../../../../../../../shared/constants'; -import { FeatureIndicator } from '../../../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../../../shared/gettext'; +import { FeatureIndicator, Text } from '../../../../../../../lib/components'; import { colors } from '../../../../../../../lib/foundations'; import { useStyledRef } from '../../../../../../../lib/utility-hooks'; import { useSelector } from '../../../../../../../redux/store'; import { tinyText } from '../../../../../../common-styles'; import { ConnectionPanelAccordion } from '../../../../styles'; +import { useGetFeatureIndicator } from './hooks'; const LINE_HEIGHT = 22; const GAP = 8; @@ -38,39 +38,18 @@ const StyledFeatureIndicators = styled.div({ const StyledFeatureIndicatorsWrapper = styled.div<{ $expanded: boolean }>((props) => ({ display: 'flex', flexWrap: 'wrap', + alignItems: 'center', gap: `${GAP}px`, - maxHeight: props.$expanded ? 'fit-content' : '52px', + maxHeight: props.$expanded ? 'fit-content' : '56px', overflow: 'hidden', })); -const StyledFeatureIndicatorLabel = styled.span(tinyText, (props) => ({ - display: 'flex', - gap: '4px', - padding: '1px 7px', - justifyContent: 'center', - alignItems: 'center', - borderRadius: '4px', - background: colors.darkBlue, - color: colors.white, - fontWeight: 400, - whiteSpace: 'nowrap', - visibility: 'hidden', - - // Style clickable feature indicators with a border and on-hover effect - boxSizing: 'border-box', // make border act as padding rather than margin - border: 'solid 1px', - borderColor: props.onClick ? colors.blue : colors.darkBlue, - transition: 'background ease-in-out 300ms', - '&&:hover': { - background: props.onClick ? colors.blue60 : undefined, - }, -})); - -const StyledBaseEllipsis = styled.span<{ $display: boolean }>(tinyText, (props) => ({ +const StyledBaseEllipsis = styled(Text)<{ $display: boolean }>((props) => ({ position: 'absolute', top: `${LINE_HEIGHT + GAP}px`, - color: colors.white, - padding: '2px 8px 2px 16px', + padding: '4px 8px', + marginLeft: '8px', + border: '1px solid transparent', display: props.$display ? 'inline' : 'none', })); @@ -103,6 +82,7 @@ export function FeatureIndicators(props: FeatureIndicatorsProps) { const ellipsisRef = useStyledRef<HTMLSpanElement>(); const ellipsisSpacerRef = useStyledRef<HTMLSpanElement>(); const featureIndicatorsContainerRef = useStyledRef<HTMLDivElement>(); + const featureMap = useGetFeatureIndicator(); const featureIndicatorsVisible = tunnelState.state === 'connected' || tunnelState.state === 'connecting'; @@ -128,7 +108,7 @@ export function FeatureIndicators(props: FeatureIndicatorsProps) { ) { // Get all feature indicator elements. const indicatorElements = Array.from( - featureIndicatorsContainerRef.current.getElementsByTagName('span'), + featureIndicatorsContainerRef.current.getElementsByTagName('button'), ); let lastVisibleIndex = 0; @@ -188,16 +168,21 @@ export function FeatureIndicators(props: FeatureIndicatorsProps) { ref={featureIndicatorsContainerRef} $expanded={props.expanded}> {sortedIndicators.map((indicator) => { + const feature = featureMap[indicator]; return ( - <StyledFeatureIndicatorLabel + <FeatureIndicator key={indicator.toString()} - data-testid="feature-indicator"> - {getFeatureIndicatorLabel(indicator)} - </StyledFeatureIndicatorLabel> + data-testid="feature-indicator" + onClick={feature.onClick}> + <FeatureIndicator.Text>{feature.label}</FeatureIndicator.Text> + </FeatureIndicator> ); })} </StyledFeatureIndicatorsWrapper> - <StyledEllipsisSpacer $display={!props.expanded} ref={ellipsisSpacerRef}> + <StyledEllipsisSpacer + variant="labelTiny" + $display={!props.expanded} + ref={ellipsisSpacerRef}> { // Mock amount for the spacer ellipsis. This needs to be wider than the real // ellipsis will ever be. @@ -205,6 +190,7 @@ export function FeatureIndicators(props: FeatureIndicatorsProps) { } </StyledEllipsisSpacer> <StyledEllipsis + variant="labelTiny" onClick={props.expandIsland} $display={!props.expanded} ref={ellipsisRef} @@ -236,53 +222,3 @@ function indicatorShouldBeVisible( // doesn't overlap with the ellipsis. return lineIndex === 0 || (lineIndex === 1 && indicatorRect.right < ellipsisSpacerRect.left); } - -function getFeatureIndicatorLabel(indicator: FeatureIndicator) { - switch (indicator) { - case FeatureIndicator.daita: - return strings.daita; - case FeatureIndicator.daitaMultihop: - return sprintf( - // TRANSLATORS: This is used as a feature indicator to show that DAITA is enabled through - // TRANSLATORS: multihop. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(DAITA)s - Is a non-translatable feature "DAITA" - messages.pgettext('connect-view', '%(DAITA)s: Multihop'), - { - DAITA: strings.daita, - }, - ); - case FeatureIndicator.udp2tcp: - case FeatureIndicator.shadowsocks: - case FeatureIndicator.quic: - return messages.pgettext('wireguard-settings-view', 'Obfuscation'); - case FeatureIndicator.multihop: - // TRANSLATORS: This refers to the multihop setting in the VPN settings view. This is - // TRANSLATORS: displayed when the feature is on. - return messages.gettext('Multihop'); - case FeatureIndicator.customDns: - // TRANSLATORS: This refers to the Custom DNS setting in the VPN settings view. This is - // TRANSLATORS: displayed when the feature is on. - return messages.gettext('Custom DNS'); - case FeatureIndicator.customMtu: - return messages.pgettext('wireguard-settings-view', 'MTU'); - case FeatureIndicator.bridgeMode: - return messages.pgettext('openvpn-settings-view', 'Bridge mode'); - case FeatureIndicator.lanSharing: - return messages.pgettext('vpn-settings-view', 'Local network sharing'); - case FeatureIndicator.customMssFix: - return messages.pgettext('openvpn-settings-view', 'Mssfix'); - case FeatureIndicator.lockdownMode: - return messages.pgettext('vpn-settings-view', 'Lockdown mode'); - case FeatureIndicator.splitTunneling: - return strings.splitTunneling; - case FeatureIndicator.serverIpOverride: - return messages.pgettext('settings-import', 'Server IP override'); - case FeatureIndicator.quantumResistance: - // TRANSLATORS: This refers to the quantum resistance setting in the WireGuard settings view. - // TRANSLATORS: This is displayed when the feature is on. - return messages.gettext('Quantum resistance'); - case FeatureIndicator.dnsContentBlockers: - return messages.pgettext('vpn-settings-view', 'DNS content blockers'); - } -} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/index.ts new file mode 100644 index 0000000000..74ae78f3d6 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-get-feature-indicator'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/index.ts new file mode 100644 index 0000000000..eac65c5fe7 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/index.ts @@ -0,0 +1 @@ +export * from './useGetFeatureIndicator'; 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 new file mode 100644 index 0000000000..91ebbfacb8 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts @@ -0,0 +1,147 @@ +import React from 'react'; +import { sprintf } from 'sprintf-js'; + +import { strings } from '../../../../../../../../../../shared/constants'; +import { FeatureIndicator } from '../../../../../../../../../../shared/daemon-rpc-types'; +import { messages } from '../../../../../../../../../../shared/gettext'; +import { RoutePath } from '../../../../../../../../../../shared/routes'; +import { TransitionType, useHistory } from '../../../../../../../../../lib/history'; + +export const useGetFeatureIndicator = () => { + const history = useHistory(); + + const gotoDaitaFeature = React.useCallback(() => { + history.push(RoutePath.daitaSettings, { + transition: TransitionType.show, + }); + }, [history]); + + const gotoMultihopFeature = React.useCallback(() => { + history.push(RoutePath.multihopSettings, { + transition: TransitionType.show, + }); + }, [history]); + + const gotoCustomDnsFeature = React.useCallback(() => { + history.push(RoutePath.vpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'custom-dns-settings', + }, + ], + }); + }, [history]); + + const gotoLanSharingFeature = React.useCallback(() => { + history.push(RoutePath.vpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'allow-lan-setting', + }, + ], + }); + }, [history]); + + const gotoLockdownModeFeature = React.useCallback(() => { + history.push(RoutePath.vpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'lockdown-mode-setting', + }, + ], + }); + }, [history]); + + const gotoSplitTunnelingFeature = React.useCallback(() => { + history.push(RoutePath.splitTunneling, { + transition: TransitionType.show, + }); + }, [history]); + + const gotoDnsContentBlockersFeature = React.useCallback(() => { + history.push(RoutePath.vpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'dns-blocker-setting', + }, + ], + }); + }, [history]); + + const featureMap: Record<FeatureIndicator, { label: string; onClick?: () => void }> = { + [FeatureIndicator.daita]: { label: strings.daita, onClick: gotoDaitaFeature }, + [FeatureIndicator.daitaMultihop]: { + label: sprintf( + // TRANSLATORS: This is used as a feature indicator to show that DAITA is enabled through + // TRANSLATORS: multihop. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(DAITA)s - Is a non-translatable feature "DAITA" + messages.pgettext('connect-view', '%(DAITA)s: Multihop'), + { + DAITA: strings.daita, + }, + ), + }, + [FeatureIndicator.udp2tcp]: { + label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + }, + [FeatureIndicator.shadowsocks]: { + label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + }, + [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 + // TRANSLATORS: displayed when the feature is on. + messages.gettext('Multihop'), + onClick: gotoMultihopFeature, + }, + [FeatureIndicator.customDns]: { + label: + // TRANSLATORS: This refers to the Custom DNS setting in the VPN settings view. This is + // TRANSLATORS: displayed when the feature is on. + messages.gettext('Custom DNS'), + onClick: gotoCustomDnsFeature, + }, + [FeatureIndicator.customMtu]: { label: messages.pgettext('wireguard-settings-view', 'MTU') }, + [FeatureIndicator.bridgeMode]: { + label: messages.pgettext('openvpn-settings-view', 'Bridge mode'), + }, + [FeatureIndicator.lanSharing]: { + label: messages.pgettext('vpn-settings-view', 'Local network sharing'), + onClick: gotoLanSharingFeature, + }, + [FeatureIndicator.customMssFix]: { + label: messages.pgettext('openvpn-settings-view', 'Mssfix'), + }, + [FeatureIndicator.lockdownMode]: { + label: messages.pgettext('vpn-settings-view', 'Lockdown mode'), + onClick: gotoLockdownModeFeature, + }, + [FeatureIndicator.splitTunneling]: { + label: strings.splitTunneling, + onClick: gotoSplitTunnelingFeature, + }, + [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.dnsContentBlockers]: { + label: messages.pgettext('vpn-settings-view', 'DNS content blockers'), + onClick: gotoDnsContentBlockersFeature, + }, + }; + + return featureMap; +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/allow-lan-setting/AllowLanSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/allow-lan-setting/AllowLanSetting.tsx index 543f9f7faa..b814d5631f 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/allow-lan-setting/AllowLanSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/allow-lan-setting/AllowLanSetting.tsx @@ -1,17 +1,14 @@ +import { useRef } from 'react'; import styled from 'styled-components'; import { messages } from '../../../../../../shared/gettext'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; import { spacings } from '../../../../../lib/foundations'; import { useSelector } from '../../../../../redux/store'; -import { AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; - -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); +import { ToggleListItem } from '../../../../toggle-list-item'; const LanIpRanges = styled.ul({ listStyle: 'disc outside', @@ -21,43 +18,44 @@ const LanIpRanges = styled.ul({ export function AllowLanSetting() { const allowLan = useSelector((state) => state.settings.allowLan); const { setAllowLan } = useAppContext(); + const id = 'allow-lan-setting'; + const ref = useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Local network sharing')} - </Cell.InputLabel> - </AriaLabel> - <AriaDetails> - <StyledInfoButton> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:', - )} - <LanIpRanges> - <li>10.0.0.0/8</li> - <li>172.16.0.0/12</li> - <li>192.168.0.0/16</li> - <li>169.254.0.0/16</li> - <li>fe80::/10</li> - <li>fc00::/7</li> - </LanIpRanges> - </ModalMessage> - </StyledInfoButton> - </AriaDetails> - <AriaInput> - <Cell.Switch isOn={allowLan} onChange={setAllowLan} /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> + <ToggleListItem + ref={ref} + animation={scrollToAnchor?.animation} + checked={allowLan} + onCheckedChange={setAllowLan}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Local network sharing')} + </ToggleListItem.Label> + <ToggleListItem.Group> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:', + )} + <LanIpRanges> + <li>10.0.0.0/8</li> + <li>172.16.0.0/12</li> + <li>192.168.0.0/16</li> + <li>169.254.0.0/16</li> + <li>fe80::/10</li> + <li>fc00::/7</li> + </LanIpRanges> + </ModalMessage> + </InfoButton> + <ToggleListItem.Switch /> + </ToggleListItem.Group> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-connect-setting/AutoConnectSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-connect-setting/AutoConnectSetting.tsx index eb7849415e..4883f41dc8 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-connect-setting/AutoConnectSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-connect-setting/AutoConnectSetting.tsx @@ -1,35 +1,29 @@ import { messages } from '../../../../../../shared/gettext'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; +import { ToggleListItem } from '../../../../toggle-list-item'; export function AutoConnectSetting() { const autoConnect = useSelector((state) => state.settings.guiSettings.autoConnect); const { setAutoConnect } = useAppContext(); + const scrollToAnchor = useScrollToListItem(); + + const footer = messages.pgettext( + 'vpn-settings-view', + 'Automatically connect to a server when the app launches.', + ); return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Auto-connect')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={autoConnect} onChange={setAutoConnect} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext( - 'vpn-settings-view', - 'Automatically connect to a server when the app launches.', - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> + <ToggleListItem + animation={scrollToAnchor?.animation} + checked={autoConnect} + onCheckedChange={setAutoConnect} + footer={footer}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Auto-connect')} + </ToggleListItem.Label> + <ToggleListItem.Switch aria-description={footer} /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-start-setting/AutoStartSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-start-setting/AutoStartSetting.tsx index b7d0967990..e18a249a77 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-start-setting/AutoStartSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/auto-start-setting/AutoStartSetting.tsx @@ -3,13 +3,14 @@ import { useCallback } from 'react'; import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; import { useSelector } from '../../../../../redux/store'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; +import { ToggleListItem } from '../../../../toggle-list-item'; export function AutoStartSetting() { const autoStart = useSelector((state) => state.settings.autoStart); const { setAutoStart: setAutoStartImpl } = useAppContext(); + const scrollToAnchor = useScrollToListItem(); const setAutoStart = useCallback( async (autoStart: boolean) => { @@ -24,17 +25,14 @@ export function AutoStartSetting() { ); return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Launch app on start-up')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={autoStart} onChange={setAutoStart} /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> + <ToggleListItem + animation={scrollToAnchor?.animation} + checked={autoStart} + onCheckedChange={setAutoStart}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Launch app on start-up')} + </ToggleListItem.Label> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/custom-dns-settings/CustomDnsSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/custom-dns-settings/CustomDnsSettings.tsx index 6ce6f313c5..84d5975199 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/custom-dns-settings/CustomDnsSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/custom-dns-settings/CustomDnsSettings.tsx @@ -2,23 +2,18 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { messages } from '../../../../../../shared/gettext'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; import { Button, IconButton } from '../../../../../lib/components'; +import { Accordion } from '../../../../../lib/components/accordion'; import { formatHtml } from '../../../../../lib/html-formatter'; import { IpAddress } from '../../../../../lib/ip'; import { useBoolean, useMounted, useStyledRef } from '../../../../../lib/utility-hooks'; import { useSelector } from '../../../../../redux/store'; -import Accordion from '../../../../Accordion'; -import { - AriaDescribed, - AriaDescription, - AriaDescriptionGroup, - AriaInput, - AriaInputGroup, - AriaLabel, -} from '../../../../AriaGroup'; +import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from '../../../../AriaGroup'; import * as Cell from '../../../../cell'; import List, { stringValueAsKey } from '../../../../List'; import { ModalAlert, ModalAlertType } from '../../../../Modal'; +import { ToggleListItem } from '../../../../toggle-list-item'; import { AddServerContainer, StyledAddCustomDnsLabel, @@ -41,6 +36,10 @@ export function CustomDnsSettings() { const [savingEdit, setSavingEdit] = useState(false); const willShowConfirmationDialog = useRef(false); + const id = 'custom-dns-settings'; + const ref = useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + const featureAvailable = useMemo( () => dns.state === 'custom' || @@ -53,7 +52,7 @@ export function CustomDnsSettings() { [dns], ); - const switchRef = useStyledRef<HTMLDivElement>(); + const switchRef = useStyledRef<HTMLButtonElement>(); const addButtonRef = useStyledRef<HTMLButtonElement>(); const inputContainerRef = useStyledRef<HTMLDivElement>(); @@ -192,68 +191,65 @@ export function CustomDnsSettings() { return ( <> - <Cell.Container disabled={!featureAvailable}> - <AriaInputGroup> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Use custom DNS server')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - innerRef={switchRef} - isOn={dns.state === 'custom' || inputVisible} - onChange={setCustomDnsEnabled} - /> - </AriaInput> - </AriaInputGroup> - </Cell.Container> - <Accordion expanded={listExpanded}> - <Cell.Section role="listbox"> - <List - items={dns.customOptions.addresses} - getKey={stringValueAsKey} - skipAddTransition={true} - skipRemoveTransition={savingEdit}> - {(item) => ( - <CellListItem - onRemove={onRemove} - onChange={onEdit} - willShowConfirmationDialog={willShowConfirmationDialog}> - {item} - </CellListItem> - )} - </List> - </Cell.Section> + <ToggleListItem + ref={ref} + checked={dns.state === 'custom' || inputVisible} + onCheckedChange={setCustomDnsEnabled} + animation={scrollToAnchor?.animation} + disabled={!featureAvailable}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Use custom DNS server')} + </ToggleListItem.Label> + <ToggleListItem.Switch ref={switchRef} /> + </ToggleListItem> + <Accordion expanded={listExpanded} disabled={!featureAvailable}> + <Accordion.Content> + <Cell.Section role="listbox"> + <List + items={dns.customOptions.addresses} + getKey={stringValueAsKey} + skipAddTransition={true} + skipRemoveTransition={savingEdit}> + {(item) => ( + <CellListItem + onRemove={onRemove} + onChange={onEdit} + willShowConfirmationDialog={willShowConfirmationDialog}> + {item} + </CellListItem> + )} + </List> + </Cell.Section> - {inputVisible && ( - <div ref={inputContainerRef}> - <Cell.RowInput - placeholder={messages.pgettext('vpn-settings-view', 'Enter IP')} - onSubmit={onAdd} - onChange={setValid} - invalid={invalid} - paddingLeft={32} - onBlur={onInputBlur} - autofocus - /> - </div> - )} + {inputVisible && ( + <div ref={inputContainerRef}> + <Cell.RowInput + placeholder={messages.pgettext('vpn-settings-view', 'Enter IP')} + onSubmit={onAdd} + onChange={setValid} + invalid={invalid} + paddingLeft={32} + onBlur={onInputBlur} + autofocus + /> + </div> + )} - <AddServerContainer> - <StyledButton - ref={addButtonRef} - onClick={showInput} - disabled={inputVisible} - tabIndex={-1}> - <StyledAddCustomDnsLabel tabIndex={-1}> - {messages.pgettext('vpn-settings-view', 'Add a server')} - </StyledAddCustomDnsLabel> - </StyledButton> - <IconButton variant="secondary" onClick={showInput}> - <IconButton.Icon icon="add-circle" /> - </IconButton> - </AddServerContainer> + <AddServerContainer> + <StyledButton + ref={addButtonRef} + onClick={showInput} + disabled={inputVisible} + tabIndex={-1}> + <StyledAddCustomDnsLabel tabIndex={-1}> + {messages.pgettext('vpn-settings-view', 'Add a server')} + </StyledAddCustomDnsLabel> + </StyledButton> + <IconButton variant="secondary" onClick={showInput}> + <IconButton.Icon icon="add-circle" /> + </IconButton> + </AddServerContainer> + </Accordion.Content> </Accordion> <StyledCustomDnsFooter> diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/DnsBlockerSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/DnsBlockerSetting.tsx index 76d49f429c..e525e4f5a6 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/DnsBlockerSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/DnsBlockerSetting.tsx @@ -1,11 +1,13 @@ +import { useRef } from 'react'; import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; import { messages } from '../../../../../../shared/gettext'; -import { spacings } from '../../../../../lib/foundations'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Accordion } from '../../../../../lib/components/accordion'; +import { FlexRow } from '../../../../../lib/components/flex-row'; import { formatHtml } from '../../../../../lib/html-formatter'; +import { useBoolean } from '../../../../../lib/utility-hooks'; import { useSelector } from '../../../../../redux/store'; -import * as Cell from '../../../../cell'; import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; import { @@ -15,61 +17,70 @@ import { BlockMalwareSetting, BlockSocialMediaSetting, BlockTrackersSetting, + CustomDnsEnabledFooter, } from './components'; -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); - -const StyledTitleLabel = styled(Cell.SectionTitle)({ - flex: 1, -}); - export function DnsBlockerSettings() { const dns = useSelector((state) => state.settings.dns); const customDnsFeatureName = messages.pgettext('vpn-settings-view', 'Use custom DNS server'); + const [expanded, , , toggleExpanded] = useBoolean(); + const id = 'dns-blocker-setting'; + const ref = useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); - const title = ( + return ( <> - <StyledTitleLabel as="label" disabled={dns.state === 'custom'}> - {messages.pgettext('vpn-settings-view', 'DNS content blockers')} - </StyledTitleLabel> - <StyledInfoButton> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'This might cause issues on certain websites, services, and apps.', - )} - </ModalMessage> - <ModalMessage> - {formatHtml( - sprintf( - messages.pgettext( - 'vpn-settings-view', - 'Attention: this setting cannot be used in combination with <b>%(customDnsFeatureName)s</b>', - ), - { customDnsFeatureName }, - ), - )} - </ModalMessage> - </StyledInfoButton> + <Accordion + ref={ref} + expanded={expanded} + onExpandedChange={toggleExpanded} + disabled={dns.state === 'custom'} + animation={scrollToAnchor?.animation}> + <Accordion.Header> + <Accordion.Title> + {messages.pgettext('vpn-settings-view', 'DNS content blockers')} + </Accordion.Title> + <FlexRow $gap="medium"> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'This might cause issues on certain websites, services, and apps.', + )} + </ModalMessage> + <ModalMessage> + {formatHtml( + sprintf( + messages.pgettext( + 'vpn-settings-view', + 'Attention: this setting cannot be used in combination with <b>%(customDnsFeatureName)s</b>', + ), + { customDnsFeatureName }, + ), + )} + </ModalMessage> + </InfoButton> + <Accordion.Trigger> + <Accordion.Icon /> + </Accordion.Trigger> + </FlexRow> + </Accordion.Header> + <Accordion.Content> + <BlockAdsSetting /> + <BlockTrackersSetting /> + <BlockMalwareSetting /> + <BlockGamblingSetting /> + <BlockAdultContentSetting /> + <BlockSocialMediaSetting /> + </Accordion.Content> + </Accordion> + {dns.state === 'custom' && <CustomDnsEnabledFooter />} </> ); - - return ( - <Cell.ExpandableSection sectionTitle={title} expandableId="dns-blockers"> - <BlockAdsSetting /> - <BlockTrackersSetting /> - <BlockMalwareSetting /> - <BlockGamblingSetting /> - <BlockAdultContentSetting /> - <BlockSocialMediaSetting /> - </Cell.ExpandableSection> - ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-ads-setting/BlockAdsSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-ads-setting/BlockAdsSetting.tsx index a6d46ae59e..fdc6e4a611 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-ads-setting/BlockAdsSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-ads-setting/BlockAdsSetting.tsx @@ -1,40 +1,26 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockAdsSetting() { const [dns, setBlockAds] = useDns('blockAds'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables ad blocking. - messages.pgettext('vpn-settings-view', 'Ads') - } - </IndentedValueLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockAds} - onChange={setBlockAds} - /> - </AriaInput> - </StyledSectionItem> - </AriaInputGroup> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockAds} + onCheckedChange={setBlockAds}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables ad blocking. + messages.pgettext('vpn-settings-view', 'Ads') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-adult-content-setting/BlockAdultContentSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-adult-content-setting/BlockAdultContentSetting.tsx index 0614eca823..c81b170e61 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-adult-content-setting/BlockAdultContentSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-adult-content-setting/BlockAdultContentSetting.tsx @@ -1,40 +1,26 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockAdultContentSetting() { const [dns, setBlockAdultContent] = useDns('blockAdultContent'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables block of adult content. - messages.pgettext('vpn-settings-view', 'Adult content') - } - </IndentedValueLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockAdultContent} - onChange={setBlockAdultContent} - /> - </AriaInput> - </StyledSectionItem> - </AriaInputGroup> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockAdultContent} + onCheckedChange={setBlockAdultContent}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables block of adult content. + messages.pgettext('vpn-settings-view', 'Adult content') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-gambling-setting/BlockGamblingSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-gambling-setting/BlockGamblingSetting.tsx index c1fb237628..264f7e7328 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-gambling-setting/BlockGamblingSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-gambling-setting/BlockGamblingSetting.tsx @@ -1,40 +1,26 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockGamblingSetting() { const [dns, setBlockGambling] = useDns('blockGambling'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables block of gamling related websites. - messages.pgettext('vpn-settings-view', 'Gambling') - } - </IndentedValueLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockGambling} - onChange={setBlockGambling} - /> - </AriaInput> - </StyledSectionItem> - </AriaInputGroup> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockGambling} + onCheckedChange={setBlockGambling}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables block of gamling related websites. + messages.pgettext('vpn-settings-view', 'Gambling') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-malware-setting/BlockMalwareSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-malware-setting/BlockMalwareSetting.tsx index c988af6774..65042df5c6 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-malware-setting/BlockMalwareSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-malware-setting/BlockMalwareSetting.tsx @@ -1,56 +1,38 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; import InfoButton from '../../../../../../InfoButton'; import { ModalMessage } from '../../../../../../Modal'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); - -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockMalwareSetting() { const [dns, setBlockMalware] = useDns('blockMalware'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables malware blocking. - messages.pgettext('vpn-settings-view', 'Malware') - } - </IndentedValueLabel> - </AriaLabel> - <AriaDetails> - <StyledInfoButton> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.', - )} - </ModalMessage> - </StyledInfoButton> - </AriaDetails> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockMalware} - onChange={setBlockMalware} - /> - </AriaInput> - </StyledSectionItem> - </AriaInputGroup> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockMalware} + onCheckedChange={setBlockMalware}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables malware blocking. + messages.pgettext('vpn-settings-view', 'Malware') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Group> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.', + )} + </ModalMessage> + </InfoButton> + <ToggleListItem.Switch /> + </ToggleListItem.Group> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-social-media-setting/BlockSocialMediaSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-social-media-setting/BlockSocialMediaSetting.tsx index 964d6c61e6..9684ba5f2f 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-social-media-setting/BlockSocialMediaSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-social-media-setting/BlockSocialMediaSetting.tsx @@ -1,66 +1,26 @@ -import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { formatHtml } from '../../../../../../../lib/html-formatter'; -import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockSocialMediaSetting() { const [dns, setBlockSocialMedia] = useDns('blockSocialMedia'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables block of social media. - messages.pgettext('vpn-settings-view', 'Social media') - } - </IndentedValueLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockSocialMedia} - onChange={setBlockSocialMedia} - /> - </AriaInput> - </StyledSectionItem> - {dns.state === 'custom' && <CustomDnsEnabledFooter />} - </AriaInputGroup> - ); -} - -function CustomDnsEnabledFooter() { - const customDnsFeatureName = messages.pgettext('vpn-settings-view', 'Use custom DNS server'); - - // TRANSLATORS: This is displayed when the custom DNS setting is turned on which makes the block - // TRANSLATORS: ads/trackers settings disabled. The text enclosed in "<b></b>" will appear bold. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle. - const blockingDisabledText = messages.pgettext( - 'vpn-settings-view', - 'Disable <b>%(customDnsFeatureName)s</b> below to activate these settings.', - ); - - return ( - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {formatHtml(sprintf(blockingDisabledText, { customDnsFeatureName }))} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockSocialMedia} + onCheckedChange={setBlockSocialMedia}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables block of social media. + messages.pgettext('vpn-settings-view', 'Social media') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-trackers-setting/BlockTrackersSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-trackers-setting/BlockTrackersSetting.tsx index 16a5cc7173..055c4df78a 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-trackers-setting/BlockTrackersSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/block-trackers-setting/BlockTrackersSetting.tsx @@ -1,40 +1,26 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../../../shared/gettext'; -import { colors, spacings } from '../../../../../../../lib/foundations'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../../../AriaGroup'; -import * as Cell from '../../../../../../cell'; +import { FlexRow } from '../../../../../../../lib/components/flex-row'; +import { ToggleListItem } from '../../../../../../toggle-list-item'; import { useDns } from '../../hooks'; -const StyledSectionItem = styled(Cell.Container)({ - backgroundColor: colors.blue40, -}); - -const IndentedValueLabel = styled(Cell.ValueLabel)({ - marginLeft: spacings.medium, -}); - export function BlockTrackersSetting() { const [dns, setBlockTrackers] = useDns('blockTrackers'); return ( - <AriaInputGroup> - <StyledSectionItem disabled={dns.state === 'custom'}> - <AriaLabel> - <IndentedValueLabel> - { - // TRANSLATORS: Label for settings that enables tracker blocking. - messages.pgettext('vpn-settings-view', 'Trackers') - } - </IndentedValueLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - isOn={dns.state === 'default' && dns.defaultOptions.blockTrackers} - onChange={setBlockTrackers} - /> - </AriaInput> - </StyledSectionItem> - </AriaInputGroup> + <ToggleListItem + level={1} + disabled={dns.state === 'custom'} + checked={dns.state === 'default' && dns.defaultOptions.blockTrackers} + onCheckedChange={setBlockTrackers}> + <FlexRow $padding={{ left: 'medium' }}> + <ToggleListItem.Label variant="bodySmall"> + { + // TRANSLATORS: Label for settings that enables tracker blocking. + messages.pgettext('vpn-settings-view', 'Trackers') + } + </ToggleListItem.Label> + </FlexRow> + <ToggleListItem.Switch /> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/CustomDnsEnabledFooter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/CustomDnsEnabledFooter.tsx new file mode 100644 index 0000000000..931471d231 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/CustomDnsEnabledFooter.tsx @@ -0,0 +1,25 @@ +import { sprintf } from 'sprintf-js'; + +import { messages } from '../../../../../../../../shared/gettext'; +import { Flex, Text } from '../../../../../../../lib/components'; + +export function CustomDnsEnabledFooter() { + const customDnsFeatureName = messages.pgettext('vpn-settings-view', 'Use custom DNS server'); + + // TRANSLATORS: This is displayed when the custom DNS setting is turned on which makes the block + // TRANSLATORS: ads/trackers settings disabled. The text enclosed in "<b></b>" will appear bold. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle. + const blockingDisabledText = messages.pgettext( + 'vpn-settings-view', + 'Disable "%(customDnsFeatureName)s" below to activate these settings.', + ); + + return ( + <Flex $padding={{ top: 'tiny', horizontal: 'medium' }}> + <Text variant="labelTiny" color="whiteAlpha60"> + {sprintf(blockingDisabledText, { customDnsFeatureName })} + </Text> + </Flex> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/index.ts new file mode 100644 index 0000000000..1192d2aa5a --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/custom-dns-enabled-footer/index.ts @@ -0,0 +1 @@ +export * from './CustomDnsEnabledFooter'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/index.ts index 913b2dca77..fd79e5c5cc 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/dns-blocker-settings/components/index.ts @@ -4,3 +4,4 @@ export * from './block-gambling-setting'; export * from './block-malware-setting'; export * from './block-social-media-setting'; export * from './block-trackers-setting'; +export * from './custom-dns-enabled-footer'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/enable-ipv6-setting/EnableIpv6Setting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/enable-ipv6-setting/EnableIpv6Setting.tsx index 3d66cc3dea..c441d58fb3 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/enable-ipv6-setting/EnableIpv6Setting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/enable-ipv6-setting/EnableIpv6Setting.tsx @@ -1,23 +1,20 @@ -import { useCallback } from 'react'; -import styled from 'styled-components'; +import { useCallback, useRef } from 'react'; import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { useAppContext } from '../../../../../context'; -import { spacings } from '../../../../../lib/foundations'; +import { useScrollToListItem } from '../../../../../hooks'; import { useSelector } from '../../../../../redux/store'; -import { AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; - -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); +import { ToggleListItem } from '../../../../toggle-list-item'; export function EnableIpv6Setting() { const enableIpv6 = useSelector((state) => state.settings.enableIpv6); const { setEnableIpv6: setEnableIpv6Impl } = useAppContext(); + const id = 'enable-ipv6-setting'; + const ref = useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); const setEnableIpv6 = useCallback( async (enableIpv6: boolean) => { @@ -32,31 +29,31 @@ export function EnableIpv6Setting() { ); return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel>{messages.pgettext('vpn-settings-view', 'Enable IPv6')}</Cell.InputLabel> - </AriaLabel> - <AriaDetails> - <StyledInfoButton> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'When this feature is enabled, IPv6 can be used alongside IPv4 in the VPN tunnel to communicate with internet services.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it.', - )} - </ModalMessage> - </StyledInfoButton> - </AriaDetails> - <AriaInput> - <Cell.Switch isOn={enableIpv6} onChange={setEnableIpv6} /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> + <ToggleListItem + ref={ref} + animation={scrollToAnchor?.animation} + checked={enableIpv6} + onCheckedChange={setEnableIpv6}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Enable IPv6')} + </ToggleListItem.Label> + <ToggleListItem.Group> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'When this feature is enabled, IPv6 can be used alongside IPv4 in the VPN tunnel to communicate with internet services.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it.', + )} + </ModalMessage> + </InfoButton> + <ToggleListItem.Switch /> + </ToggleListItem.Group> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/index.ts index 3cd40a607b..a475e36484 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/index.ts @@ -1,4 +1,5 @@ export * from './allow-lan-setting'; +export * from './custom-dns-settings'; export * from './auto-connect-setting'; export * from './auto-start-setting'; export * from './dns-blocker-settings'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/kill-switch-setting/KillSwitchSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/kill-switch-setting/KillSwitchSetting.tsx index ae0fc3a845..bd650c84bd 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/kill-switch-setting/KillSwitchSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/kill-switch-setting/KillSwitchSetting.tsx @@ -1,58 +1,39 @@ -import styled from 'styled-components'; - import { messages } from '../../../../../../shared/gettext'; -import { Button } from '../../../../../lib/components'; -import { spacings } from '../../../../../lib/foundations'; -import { useBoolean } from '../../../../../lib/utility-hooks'; -import { AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; +import { useScrollToListItem } from '../../../../../hooks'; +import { ListItem } from '../../../../../lib/components/list-item'; +import { Switch } from '../../../../../lib/components/switch'; import InfoButton from '../../../../InfoButton'; -import { ModalAlert, ModalAlertType, ModalMessage } from '../../../../Modal'; - -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); +import { ModalMessage } from '../../../../Modal'; export function KillSwitchSetting() { - const [killSwitchInfoVisible, showKillSwitchInfo, hideKillSwitchInfo] = useBoolean(false); + const scrollToAnchor = useScrollToListItem(); return ( - <> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Kill switch')} - </Cell.InputLabel> - </AriaLabel> - <StyledInfoButton onClick={showKillSwitchInfo} /> - <AriaInput> - <Cell.Switch isOn disabled /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> - <ModalAlert - isOpen={killSwitchInfoVisible} - type={ModalAlertType.info} - buttons={[ - <Button key="back" onClick={hideKillSwitchInfo}> - <Button.Text>{messages.gettext('Got it!')}</Button.Text> - </Button>, - ]} - close={hideKillSwitchInfo}> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'This built-in feature prevents your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails, it does this by blocking your traffic until your connection is reestablished.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents. With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', - )} - </ModalMessage> - </ModalAlert> - </> + <ListItem animation={scrollToAnchor?.animation}> + <ListItem.Item> + <ListItem.Content> + <ListItem.Label>{messages.pgettext('vpn-settings-view', 'Kill switch')}</ListItem.Label> + <ListItem.Group $gap="medium"> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'This built-in feature prevents your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails, it does this by blocking your traffic until your connection is reestablished.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents. With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', + )} + </ModalMessage> + </InfoButton> + <Switch checked disabled> + <Switch.Thumb /> + </Switch> + </ListItem.Group> + </ListItem.Content> + </ListItem.Item> + </ListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/lockdown-mode-setting/LockdownModeSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/lockdown-mode-setting/LockdownModeSetting.tsx index da15a9dc4c..6fbef43645 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/lockdown-mode-setting/LockdownModeSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/lockdown-mode-setting/LockdownModeSetting.tsx @@ -1,25 +1,22 @@ -import { useCallback } from 'react'; -import styled from 'styled-components'; +import { useCallback, useRef } from 'react'; import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; import { Button } from '../../../../../lib/components'; -import { spacings } from '../../../../../lib/foundations'; import { useBoolean } from '../../../../../lib/utility-hooks'; import { useSelector } from '../../../../../redux/store'; -import { AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; import InfoButton from '../../../../InfoButton'; import { ModalAlert, ModalAlertType, ModalMessage } from '../../../../Modal'; - -const StyledInfoButton = styled(InfoButton)({ - marginRight: spacings.medium, -}); +import { ToggleListItem } from '../../../../toggle-list-item'; export function LockdownModeSetting() { const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected); const { setBlockWhenDisconnected: setBlockWhenDisconnectedImpl } = useAppContext(); + const id = 'lockdown-mode-setting'; + const ref = useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(false); @@ -53,60 +50,57 @@ export function LockdownModeSetting() { }, [hideConfirmationDialog, setBlockWhenDisconnected]); return ( - <> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('vpn-settings-view', 'Lockdown mode')} - </Cell.InputLabel> - </AriaLabel> - <AriaDetails> - <StyledInfoButton> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', - )} - </ModalMessage> - </StyledInfoButton> - </AriaDetails> - <AriaInput> - <Cell.Switch isOn={blockWhenDisconnected} onChange={setLockDownMode} /> - </AriaInput> - </Cell.Container> - </AriaInputGroup> - <ModalAlert - isOpen={confirmationDialogVisible} - type={ModalAlertType.caution} - buttons={[ - <Button variant="destructive" key="confirm" onClick={confirmLockdownMode}> - <Button.Text>{messages.gettext('Enable anyway')}</Button.Text> - </Button>, - <Button key="back" onClick={hideConfirmationDialog}> - <Button.Text>{messages.gettext('Back')}</Button.Text> - </Button>, - ]} - close={hideConfirmationDialog}> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet.', - )} - </ModalMessage> - <ModalMessage> - {messages.pgettext( - 'vpn-settings-view', - 'The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit.', - )} - </ModalMessage> - </ModalAlert> - </> + <ToggleListItem + ref={ref} + animation={scrollToAnchor?.animation} + checked={blockWhenDisconnected} + onCheckedChange={setLockDownMode}> + <ToggleListItem.Label> + {messages.pgettext('vpn-settings-view', 'Lockdown mode')} + </ToggleListItem.Label> + <ToggleListItem.Group> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', + )} + </ModalMessage> + </InfoButton> + + <ModalAlert + isOpen={confirmationDialogVisible} + type={ModalAlertType.caution} + buttons={[ + <Button variant="destructive" key="confirm" onClick={confirmLockdownMode}> + <Button.Text>{messages.gettext('Enable anyway')}</Button.Text> + </Button>, + <Button key="back" onClick={hideConfirmationDialog}> + <Button.Text>{messages.gettext('Back')}</Button.Text> + </Button>, + ]} + close={hideConfirmationDialog}> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'vpn-settings-view', + 'The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit.', + )} + </ModalMessage> + </ModalAlert> + <ToggleListItem.Switch /> + </ToggleListItem.Group> + </ToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/open-vpn-settings/OpenVpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/open-vpn-settings/OpenVpnSettings.tsx index 3b5a35af72..149bca6d98 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/open-vpn-settings/OpenVpnSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/open-vpn-settings/OpenVpnSettings.tsx @@ -3,14 +3,19 @@ import { sprintf } from 'sprintf-js'; import { strings } from '../../../../../../shared/constants'; import { messages } from '../../../../../../shared/gettext'; import { RoutePath } from '../../../../../../shared/routes'; +import { useScrollToListItem } from '../../../../../hooks'; import { useTunnelProtocol } from '../../../../../lib/relay-settings-hooks'; import { NavigationListItem } from '../../../../NavigationListItem'; export function OpenVpnSettings() { const tunnelProtocol = useTunnelProtocol(); + const scrollToAnchor = useScrollToListItem(); return ( - <NavigationListItem to={RoutePath.openVpnSettings} disabled={tunnelProtocol === 'wireguard'}> + <NavigationListItem + to={RoutePath.openVpnSettings} + disabled={tunnelProtocol === 'wireguard'} + animation={scrollToAnchor?.animation}> <NavigationListItem.Label> {sprintf( // TRANSLATORS: %(openvpn)s will be replaced with the string "OpenVPN" diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/tunnel-protocol-setting/TunnelProtocol.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/tunnel-protocol-setting/TunnelProtocol.tsx index b1e29b8a37..8e23e70021 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/tunnel-protocol-setting/TunnelProtocol.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/tunnel-protocol-setting/TunnelProtocol.tsx @@ -1,16 +1,16 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { strings, urls } from '../../../../../../shared/constants'; import { TunnelProtocol } 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 { useTunnelProtocol } from '../../../../../lib/relay-settings-hooks'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInputGroup } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; import { ExternalLink } from '../../../../ExternalLink'; export function TunnelProtocolSetting() { @@ -24,6 +24,9 @@ export function TunnelProtocolSetting() { const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); const openVpnDisabled = daita || multihop || quantumResistant; + const id = 'tunnel-protocol-setting'; + const scrollToAnchor = useScrollToListItem(undefined, id); + const featuresToDisableForOpenVpn = []; if (daita) { featuresToDisableForOpenVpn.push(strings.daita); @@ -52,48 +55,40 @@ export function TunnelProtocolSetting() { [relaySettingsUpdater], ); - const tunnelProtocolItems: Array<SelectorItem<TunnelProtocol>> = useMemo( - () => [ - { - label: strings.wireguard, - value: 'wireguard', - }, - { - label: strings.openvpn, - value: 'openvpn', - disabled: openVpnDisabled, - }, - ], - [openVpnDisabled], + const openVpnDisabledFooter = sprintf( + messages.pgettext( + 'vpn-settings-view', + 'To select %(openvpn)s, please disable these settings: %(featureList)s.', + ), + { openvpn: strings.openvpn, featureList: featuresToDisableForOpenVpn.join(', ') }, ); return ( - <AriaInputGroup> - <Selector - title={messages.pgettext('vpn-settings-view', 'Tunnel protocol')} - items={tunnelProtocolItems} - value={tunnelProtocol} - onSelect={setTunnelProtocol} - /> + <Listbox + onValueChange={setTunnelProtocol} + value={tunnelProtocol} + animation={scrollToAnchor?.animation} + aria-description={openVpnDisabled ? openVpnDisabledFooter : undefined}> + <Listbox.Item> + <Listbox.Content> + <Listbox.Label>{messages.pgettext('vpn-settings-view', 'Tunnel protocol')}</Listbox.Label> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={'wireguard'}>{strings.wireguard}</DefaultListboxOption> + <DefaultListboxOption value={'openvpn'} disabled={openVpnDisabled}> + {strings.openvpn} + </DefaultListboxOption> + </Listbox.Options> {openVpnDisabled && ( - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {sprintf( - messages.pgettext( - 'vpn-settings-view', - 'To select %(openvpn)s, please disable these settings: %(featureList)s.', - ), - { openvpn: strings.openvpn, featureList: featuresToDisableForOpenVpn.join(', ') }, - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> + <Listbox.Footer> + <Listbox.Text>{openVpnDisabledFooter}</Listbox.Text> + </Listbox.Footer> )} {tunnelProtocol === 'openvpn' && ( - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> + <Listbox.Footer> + <div> + <Listbox.Text> {sprintf( // TRANSLATORS: Footer text for tunnel protocol selector when OpenVPN is selected. // TRANSLATORS: Available placeholders: @@ -104,20 +99,20 @@ export function TunnelProtocolSetting() { ), { openVpn: strings.openvpn }, )}{' '} - </Cell.CellFooterText> - </AriaDescription> - <ExternalLink variant="labelTiny" to={urls.removingOpenVpnBlog}> - <ExternalLink.Text> - {sprintf( - // TRANSLATORS: Link in tunnel protocol selector footer to blog post - // TRANSLATORS: about OpenVPN support ending. - messages.pgettext('vpn-settings-view', 'Read more'), - )} - </ExternalLink.Text> - <ExternalLink.Icon icon="external" size="small" /> - </ExternalLink> - </Cell.CellFooter> + </Listbox.Text> + <ExternalLink variant="labelTiny" to={urls.removingOpenVpnBlog}> + <ExternalLink.Text> + {sprintf( + // TRANSLATORS: Link in tunnel protocol selector footer to blog post + // TRANSLATORS: about OpenVPN support ending. + messages.pgettext('vpn-settings-view', 'Read more'), + )} + </ExternalLink.Text> + <ExternalLink.Icon icon="external" size="small" /> + </ExternalLink> + </div> + </Listbox.Footer> )} - </AriaInputGroup> + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/wireguard-settings/WireguardSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/wireguard-settings/WireguardSettings.tsx index 9c8f853e6a..37013cf132 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/wireguard-settings/WireguardSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/vpn-settings/components/wireguard-settings/WireguardSettings.tsx @@ -3,6 +3,7 @@ import { sprintf } from 'sprintf-js'; import { strings } from '../../../../../../shared/constants'; import { messages } from '../../../../../../shared/gettext'; import { RoutePath } from '../../../../../../shared/routes'; +import { useScrollToListItem } from '../../../../../hooks'; import { RelaySettingsRedux } from '../../../../../redux/settings/reducers'; import { useSelector } from '../../../../../redux/store'; import { NavigationListItem } from '../../../../NavigationListItem'; @@ -21,12 +22,16 @@ function mapRelaySettingsToProtocol(relaySettings: RelaySettingsRedux) { } export function WireguardSettings() { + const scrollToAnchor = useScrollToListItem(); const tunnelProtocol = useSelector((state) => mapRelaySettingsToProtocol(state.settings.relaySettings), ); return ( - <NavigationListItem to={RoutePath.wireguardSettings} disabled={tunnelProtocol === 'openvpn'}> + <NavigationListItem + to={RoutePath.wireguardSettings} + disabled={tunnelProtocol === 'openvpn'} + animation={scrollToAnchor?.animation}> <NavigationListItem.Label> {sprintf( // TRANSLATORS: %(wireguard)s will be replaced with the string "WireGuard" |
