summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-09-18 07:08:15 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:44 +0200
commit0de561bf0da51b0eac142b339c088ae99cb92354 (patch)
tree6a33963d1deff31e15d5082c3d30c1bd43c3e19b
parentae54d3c40329bafec26465b691e865e84feff5b3 (diff)
downloadmullvadvpn-0de561bf0da51b0eac142b339c088ae99cb92354.tar.xz
mullvadvpn-0de561bf0da51b0eac142b339c088ae99cb92354.zip
Update InputOption component to persist value when not selected
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOption.tsx24
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOptionContext.tsx3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-input/InputOptionInput.tsx43
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-trigger/InputOptionTrigger.tsx41
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/shadowsocks-settings/components/shadowsocks-port-setting/ShadowSocksPortSetting.tsx13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/port-setting/PortSetting.tsx13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/use-text-field.ts (renamed from desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx)6
8 files changed, 84 insertions, 61 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOption.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOption.tsx
index 090c6b8aaf..ec813b07ca 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOption.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOption.tsx
@@ -2,16 +2,34 @@ import React from 'react';
import { Listbox } from '../../../../lib/components/listbox';
import { ListboxOptionProps } from '../../../../lib/components/listbox/components';
+import { useTextField } from '../../../../lib/components/text-field';
import { InputOptionInput, InputOptionLabel, InputOptionTrigger } from './components';
import { InputOptionProvider } from './InputOptionContext';
-export type InputOptionProps<T> = ListboxOptionProps<T>;
+export type InputOptionProps<T> = ListboxOptionProps<T> & {
+ defaultValue?: string;
+ validate?: (value: string) => boolean;
+ format?: (value: string) => string;
+};
-function InputOption<T>({ children, ...props }: InputOptionProps<T>) {
+function InputOption<T>({
+ defaultValue,
+ validate,
+ format,
+ children,
+ ...props
+}: InputOptionProps<T>) {
const inputRef = React.useRef<HTMLInputElement>(null);
const labelId = React.useId();
+ const inputState = useTextField({
+ inputRef,
+ defaultValue,
+ validate,
+ format,
+ });
+
return (
- <InputOptionProvider inputRef={inputRef} labelId={labelId}>
+ <InputOptionProvider inputRef={inputRef} labelId={labelId} inputState={inputState}>
<Listbox.Option level={1} {...props}>
<InputOptionTrigger>
<Listbox.Option.Item>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOptionContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOptionContext.tsx
index 083456c356..a549abf409 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOptionContext.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/InputOptionContext.tsx
@@ -1,8 +1,11 @@
import React, { createContext, ReactNode, useContext } from 'react';
+import { UseTextFieldState } from '../../../../lib/components/text-field';
+
type InputOptionContextType = {
inputRef: React.RefObject<HTMLInputElement | null>;
labelId: string | undefined;
+ inputState: UseTextFieldState;
};
const InputOptionContextContext = createContext<InputOptionContextType | undefined>(undefined);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-input/InputOptionInput.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-input/InputOptionInput.tsx
index ec3044ef2d..3d7244c258 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-input/InputOptionInput.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-input/InputOptionInput.tsx
@@ -3,48 +3,21 @@ import React from 'react';
import { ListItem } from '../../../../../../lib/components/list-item';
import { ListItemTextFieldInputProps } from '../../../../../../lib/components/list-item/components/list-item-text-field/components';
import { useListboxContext } from '../../../../../../lib/components/listbox';
-import { useTextField } from '../../../../../../lib/components/text-field';
import { useInputOptionContext } from '../../InputOptionContext';
-type InputOptionInputProps = {
- initialValue?: string;
- validate?: (value: string) => boolean;
- format?: (value: string) => string;
-} & ListItemTextFieldInputProps;
+type InputOptionInputProps = ListItemTextFieldInputProps;
-export function InputOptionInput({
- initialValue,
- validate,
- format,
- ...props
-}: InputOptionInputProps) {
- const { onValueChange: listBoxOnValueChange, value: listBoxValue } = useListboxContext<
- string | undefined
- >();
+export function InputOptionInput(props: InputOptionInputProps) {
+ const { onValueChange: listBoxOnValueChange } = useListboxContext<string | undefined>();
- const { inputRef, labelId } = useInputOptionContext();
+ const { inputRef, labelId, inputState } = useInputOptionContext();
+ const { value, invalid, dirty, blur, handleChange, reset } = inputState;
- const { value, invalid, dirty, blur, handleChange, reset } = useTextField({
- inputRef,
- defaultValue: initialValue,
- validate,
- format,
- });
-
- React.useEffect(() => {
- if (listBoxValue !== 'custom') {
- reset();
- }
- }, [listBoxValue, reset]);
-
- const handleBlur = React.useCallback(async () => {
- if (listBoxOnValueChange && !invalid && dirty) {
- await listBoxOnValueChange(value);
- }
+ const handleBlur = React.useCallback(() => {
if (invalid) {
reset();
}
- }, [dirty, invalid, listBoxOnValueChange, reset, value]);
+ }, [invalid, reset]);
const handleSubmit = React.useCallback(
async (event: React.FormEvent) => {
@@ -58,7 +31,7 @@ export function InputOptionInput({
);
return (
- <ListItem.TextField invalid={invalid} onSubmit={handleSubmit}>
+ <ListItem.TextField invalid={invalid && dirty} onSubmit={handleSubmit}>
<ListItem.TextField.Input
ref={inputRef}
value={value}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-trigger/InputOptionTrigger.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-trigger/InputOptionTrigger.tsx
index ae8f4d6c4e..25f71a03e0 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-trigger/InputOptionTrigger.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/settings-listbox/components/input-option/components/input-option-trigger/InputOptionTrigger.tsx
@@ -13,6 +13,18 @@ import { useInputOptionContext } from '../../InputOptionContext';
export type InputOptionTriggerProps = ListboxOptionTriggerProps;
export const StyledInputOptionTrigger = styled.li`
+ &&:hover {
+ ${StyledListItemOptionItem} {
+ background-color: ${colors.whiteOnBlue10};
+ }
+ }
+
+ &&:active {
+ ${StyledListItemOptionItem} {
+ background-color: ${colors.whiteOnBlue20};
+ }
+ }
+
&&[aria-selected='true'] {
&:hover {
${StyledListItemOptionItem} {
@@ -29,9 +41,17 @@ export const StyledInputOptionTrigger = styled.li`
export const InputOptionTrigger = ({ children, ...props }: InputOptionTriggerProps) => {
const { value } = useListboxOptionContext();
- const { inputRef } = useInputOptionContext();
+ const {
+ inputRef,
+ inputState: { value: inputValue },
+ } = useInputOptionContext();
- const { value: selectedValue, focusedValue, setFocusedValue } = useListboxContext();
+ const {
+ value: selectedValue,
+ focusedValue,
+ setFocusedValue,
+ onValueChange,
+ } = useListboxContext();
const selected = value === selectedValue;
const focused = value === focusedValue;
@@ -43,12 +63,18 @@ export const InputOptionTrigger = ({ children, ...props }: InputOptionTriggerPro
}
}, [value, focused, inputRef]);
- const handleFocus = React.useCallback(() => {
- if (!focused) {
- setFocusedValue(value);
- inputRef.current?.focus();
+ const handleClick = React.useCallback(async () => {
+ setFocusedValue(value);
+ inputRef.current?.focus();
+ if (!selected) {
+ await onValueChange?.(inputValue);
}
- }, [focused, inputRef, setFocusedValue, value]);
+ }, [inputRef, inputValue, onValueChange, selected, setFocusedValue, value]);
+
+ const handleFocus = React.useCallback(() => {
+ setFocusedValue(value);
+ inputRef.current?.focus();
+ }, [inputRef, setFocusedValue, value]);
return (
<StyledInputOptionTrigger
@@ -56,6 +82,7 @@ export const InputOptionTrigger = ({ children, ...props }: InputOptionTriggerPro
aria-selected={selected}
tabIndex={tabIndex}
onFocus={handleFocus}
+ onClick={handleClick}
{...props}>
{children}
</StyledInputOptionTrigger>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/shadowsocks-settings/components/shadowsocks-port-setting/ShadowSocksPortSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/shadowsocks-settings/components/shadowsocks-port-setting/ShadowSocksPortSetting.tsx
index 5dcb0a951a..91876a61f6 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/shadowsocks-settings/components/shadowsocks-port-setting/ShadowSocksPortSetting.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/shadowsocks-settings/components/shadowsocks-port-setting/ShadowSocksPortSetting.tsx
@@ -62,7 +62,13 @@ export function ShadowsocksPortSetting() {
<SettingsListbox.BaseOption value={null}>
{messages.gettext('Automatic')}
</SettingsListbox.BaseOption>
- <SettingsListbox.InputOption value="custom">
+ <SettingsListbox.InputOption
+ value="custom"
+ defaultValue={
+ selectedOption.value === 'custom' ? selectedOption.port?.toString() : undefined
+ }
+ validate={validateValue}
+ format={removeNonNumericCharacters}>
<SettingsListbox.InputOption.Label>
{messages.gettext('Custom')}
</SettingsListbox.InputOption.Label>
@@ -70,11 +76,6 @@ export function ShadowsocksPortSetting() {
aria-describedby={descriptionId}
type="text"
placeholder={messages.pgettext('wireguard-settings-view', 'Port')}
- initialValue={
- selectedOption.value === 'custom' ? selectedOption.port?.toString() : undefined
- }
- validate={validateValue}
- format={removeNonNumericCharacters}
maxLength={`${ALLOWED_RANGE[1]}`.length}
/>
</SettingsListbox.InputOption>
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 04fa29c9b9..f808b0175b 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
@@ -124,20 +124,21 @@ export function PortSetting() {
{item.label}
</SettingsListbox.BaseOption>
))}
- <SettingsListbox.InputOption value="custom">
+ <SettingsListbox.InputOption
+ defaultValue={
+ selectedOption.value === 'custom' ? selectedOption.port?.toString() : undefined
+ }
+ value="custom"
+ validate={validateStringValue}
+ format={removeNonNumericCharacters}>
<SettingsListbox.InputOption.Label>
{messages.gettext('Custom')}
</SettingsListbox.InputOption.Label>
<SettingsListbox.InputOption.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}
/>
</SettingsListbox.InputOption>
</SettingsListbox.Options>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts
index d59cdb842f..106172db8b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts
@@ -1 +1 @@
-export * from './useTextField';
+export * from './use-text-field';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/use-text-field.ts
index b58fa08599..f6a99682cd 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/use-text-field.ts
@@ -1,13 +1,13 @@
import React from 'react';
-export type UseTextFieldOptions = {
+export type UseTextFieldProps = {
inputRef: React.RefObject<HTMLInputElement | null>;
defaultValue?: string;
validate?: (value: string) => boolean;
format?: (value: string) => string;
};
-export type UseTextFieldReturn = {
+export type UseTextFieldState = {
value: string;
invalid: boolean;
dirty: boolean;
@@ -23,7 +23,7 @@ export function useTextField({
defaultValue,
format,
validate,
-}: UseTextFieldOptions): UseTextFieldReturn {
+}: UseTextFieldProps): UseTextFieldState {
const [value, setValue] = React.useState(defaultValue ?? '');
const [invalid, setInvalid] = React.useState(validate ? !validate(value) : false);
const [dirty, setDirty] = React.useState(false);