diff options
| author | Oliver <oliver@mohlin.dev> | 2025-09-18 07:08:15 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:44 +0200 |
| commit | 0de561bf0da51b0eac142b339c088ae99cb92354 (patch) | |
| tree | 6a33963d1deff31e15d5082c3d30c1bd43c3e19b | |
| parent | ae54d3c40329bafec26465b691e865e84feff5b3 (diff) | |
| download | mullvadvpn-0de561bf0da51b0eac142b339c088ae99cb92354.tar.xz mullvadvpn-0de561bf0da51b0eac142b339c088ae99cb92354.zip | |
Update InputOption component to persist value when not selected
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); |
