summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components/cell
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-10-22 15:32:24 +0200
committerOskar <oskar@mullvad.net>2024-10-22 15:32:24 +0200
commit1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129 (patch)
tree43caf1e9884496f64c2333648c16381eaa1cf18a /gui/src/renderer/components/cell
parent6533665d1fdf9a97e6ea1e36f01ee2f293b4338c (diff)
parentcf62ffdd17eac7958977895f098742c704aa3047 (diff)
downloadmullvadvpn-1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129.tar.xz
mullvadvpn-1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129.zip
Merge branch 'add-react-linter-rules'
Diffstat (limited to 'gui/src/renderer/components/cell')
-rw-r--r--gui/src/renderer/components/cell/Input.tsx73
-rw-r--r--gui/src/renderer/components/cell/Section.tsx10
-rw-r--r--gui/src/renderer/components/cell/Selector.tsx40
-rw-r--r--gui/src/renderer/components/cell/SettingsForm.tsx14
-rw-r--r--gui/src/renderer/components/cell/SettingsGroup.tsx4
-rw-r--r--gui/src/renderer/components/cell/SettingsRadioGroup.tsx19
-rw-r--r--gui/src/renderer/components/cell/SettingsRow.tsx7
-rw-r--r--gui/src/renderer/components/cell/SettingsSelect.tsx27
-rw-r--r--gui/src/renderer/components/cell/SettingsTextInput.tsx15
9 files changed, 134 insertions, 75 deletions
diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx
index 97fe01a185..de2649e3c6 100644
--- a/gui/src/renderer/components/cell/Input.tsx
+++ b/gui/src/renderer/components/cell/Input.tsx
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
-import { useBoolean, useCombinedRefs, useStyledRef } from '../../lib/utilityHooks';
+import { useBoolean, useCombinedRefs, useEffectEvent, useStyledRef } from '../../lib/utility-hooks';
import { normalText } from '../common-styles';
import ImageView from '../ImageView';
import { BackAction } from '../KeyboardNavigation';
@@ -59,6 +59,10 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
onSubmitValue,
onInvalidValue,
onChangeValue,
+ onFocus: propsOnFocus,
+ onBlur: propsOnBlur,
+ onChange: propsOnChange,
+ onKeyPress: propsOnKeyPress,
...otherProps
} = props;
@@ -79,26 +83,26 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
onInvalidValue?.(value);
}
},
- [onSubmitValue, onInvalidValue],
+ [validateValue, onSubmitValue, onInvalidValue],
);
const onFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setFocused();
- props.onFocus?.(event);
+ propsOnFocus?.(event);
},
- [props.onFocus],
+ [propsOnFocus, setFocused],
);
const onBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setBlurred();
- props.onBlur?.(event);
+ propsOnBlur?.(event);
if (submitOnBlur) {
onSubmit(value);
}
},
- [value, props.onBlur, validateValue, onSubmit, submitOnBlur],
+ [setBlurred, propsOnBlur, submitOnBlur, onSubmit, value],
);
const onChange = useCallback(
@@ -110,10 +114,10 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
setInternalValue(value);
}
- props.onChange?.(event);
+ propsOnChange?.(event);
onChangeValue?.(value);
},
- [modifyValue, props.onSubmit],
+ [modifyValue, onChangeValue, props.value, propsOnChange],
);
const onKeyPress = useCallback(
@@ -122,23 +126,27 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
onSubmit(value);
inputRef.current?.blur();
}
- props.onKeyPress?.(event);
+ propsOnKeyPress?.(event);
},
- [value, onSubmit, inputRef, props.onKeyPress],
+ [value, onSubmit, inputRef, propsOnKeyPress],
);
- // If the the initialValue changes in the uncontrolled mode when the user isn't currently writing,
- // then we want to update the value.
- useEffect(() => {
+ const handleInitialValueChange = useEffectEvent((initialValue?: string) => {
if (
!isFocused &&
props.value === undefined &&
- props.initialValue !== undefined &&
- internalValue !== props.initialValue
+ initialValue !== undefined &&
+ internalValue !== initialValue
) {
- setInternalValue(props.initialValue);
- onChangeValue?.(props.initialValue);
+ setInternalValue(initialValue);
+ onChangeValue?.(initialValue);
}
+ });
+
+ // If the the initialValue changes in the uncontrolled mode when the user isn't currently writing,
+ // then we want to update the value.
+ useEffect(() => {
+ handleInitialValueChange(props.initialValue);
}, [props.initialValue]);
const valid = validateValue?.(value);
@@ -205,7 +213,7 @@ function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref<
setBlurred();
onBlur?.(event);
},
- [onBlur],
+ [onBlur, setBlurred],
);
const onFocusWrapper = useCallback(
@@ -213,10 +221,10 @@ function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref<
setFocused();
onFocus?.(event);
},
- [onFocus],
+ [onFocus, setFocused],
);
- const blur = useCallback(() => inputRef.current?.blur(), []);
+ const blur = useCallback(() => inputRef.current?.blur(), [inputRef]);
const value = inputRef.current?.value;
@@ -303,18 +311,20 @@ interface IRowInputProps {
}
export function RowInput(props: IRowInputProps) {
+ const { onSubmit, onChange: propsOnChange, onFocus: propsOnFocus, onBlur: propsOnBlur } = props;
+
const [value, setValue] = useState(props.initialValue ?? '');
const textAreaRef = useStyledRef<HTMLTextAreaElement>();
const [focused, setFocused, setBlurred] = useBoolean(false);
- const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]);
+ const submit = useCallback(() => onSubmit(value), [onSubmit, value]);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value;
setValue(value);
- props.onChange?.(value);
+ propsOnChange?.(value);
},
- [props.onChange],
+ [propsOnChange],
);
const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -329,32 +339,37 @@ export function RowInput(props: IRowInputProps) {
const onFocus = useCallback(
(event: React.FocusEvent<HTMLTextAreaElement>) => {
setFocused();
- props.onFocus?.(event);
+ propsOnFocus?.(event);
},
- [props.onFocus],
+ [propsOnFocus, setFocused],
);
const onBlur = useCallback(
(event: React.FocusEvent<HTMLTextAreaElement>) => {
setBlurred();
- props.onBlur?.(event);
+ propsOnBlur?.(event);
},
- [props.onBlur],
+ [propsOnBlur, setBlurred],
);
const focus = useCallback(() => {
const input = textAreaRef.current;
if (input) {
input.focus();
+ // eslint-disable-next-line react-compiler/react-compiler
input.selectionStart = input.selectionEnd = value.length;
}
}, [textAreaRef, value.length]);
- const blur = useCallback(() => textAreaRef.current?.blur(), []);
+ const blur = useCallback(() => textAreaRef.current?.blur(), [textAreaRef]);
- useEffect(() => {
+ const focusOnMount = useEffectEvent(() => {
if (props.autofocus) {
focus();
}
+ });
+
+ useEffect(() => {
+ focusOnMount();
}, []);
useEffect(() => {
diff --git a/gui/src/renderer/components/cell/Section.tsx b/gui/src/renderer/components/cell/Section.tsx
index 056aa8ceac..b7465ecb93 100644
--- a/gui/src/renderer/components/cell/Section.tsx
+++ b/gui/src/renderer/components/cell/Section.tsx
@@ -4,7 +4,7 @@ import styled from 'styled-components';
import { colors } from '../../../config.json';
import { useAppContext } from '../../context';
import { useHistory } from '../../lib/history';
-import { useBoolean } from '../../lib/utilityHooks';
+import { useBoolean, useEffectEvent } from '../../lib/utility-hooks';
import Accordion from '../Accordion';
import ChevronButton from '../ChevronButton';
import { buttonText, openSans, sourceSansPro } from '../common-styles';
@@ -71,9 +71,13 @@ export function ExpandableSection(props: ExpandableSectionProps) {
history.location.state.expandedSections[props.expandableId] ?? !!expandedInitially;
const [expanded, , , toggleExpanded] = useBoolean(expandedValue);
- useEffect(() => {
- history.location.state.expandedSections[props.expandableId] = expanded;
+ const updateHistory = useEffectEvent((expanded: boolean) => {
+ history.recordSectionExpandedState(props.expandableId, expanded);
setNavigationHistory(history.asObject);
+ });
+
+ useEffect(() => {
+ updateHistory(expanded);
}, [expanded]);
const title = (
diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx
index 3c1988a502..b1e20a0c41 100644
--- a/gui/src/renderer/components/cell/Selector.tsx
+++ b/gui/src/renderer/components/cell/Selector.tsx
@@ -5,7 +5,7 @@ import { colors } from '../../../config.json';
import { messages } from '../../../shared/gettext';
import { useHistory } from '../../lib/history';
import { RoutePath } from '../../lib/routes';
-import { useStyledRef } from '../../lib/utilityHooks';
+import { useStyledRef } from '../../lib/utility-hooks';
import { AriaDetails, AriaInput, AriaLabel } from '../AriaGroup';
import ImageView from '../ImageView';
import InfoButton from '../InfoButton';
@@ -162,19 +162,21 @@ const StyledSideButton = styled(Cell.SideButton)({
});
function SelectorCell<T>(props: SelectorCellProps<T>) {
- const history = useHistory();
+ const { onSelect } = props;
+
+ const { push } = useHistory();
const handleClick = useCallback(() => {
if (!props.isSelected) {
- props.onSelect(props.value);
+ onSelect(props.value);
}
- }, [props.isSelected, props.onSelect, props.value]);
+ }, [props.isSelected, onSelect, props.value]);
const navigate = useCallback(() => {
if (props.details) {
- history.push(props.details.path);
+ push(props.details.path);
}
- }, [history.push, props.details?.path]);
+ }, [props.details, push]);
return (
<StyledSelectorCell>
@@ -270,8 +272,11 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
// Disables submitting of custom input when another item has been pressed.
const allowSubmitCustom = useRef(false);
- const isNonCustomItem = (value: T | U | undefined) =>
- props.items.some((item) => item.value === value) || props.automaticValue === value;
+ const isNonCustomItem = useCallback(
+ (value: T | U | undefined) =>
+ props.items.some((item) => item.value === value) || props.automaticValue === value,
+ [props.automaticValue, props.items],
+ );
const itemIsSelected = isNonCustomItem(value);
// Value of custom input. The value is undefined when custom isn't picked.
@@ -285,7 +290,7 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
// After focusing the input it should be allowed to submit custom values.
allowSubmitCustom.current = true;
setCustomValue((customValue) => customValue ?? '');
- }, [customValue, inputRef.current]);
+ }, [inputRef]);
const handleSelectItem = useCallback(
(newValue: T | U | undefined) => {
@@ -298,7 +303,7 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
onSelect(newValue!);
},
- [onSelect],
+ [inputRef, onSelect],
);
const validateCustomValue = useCallback(
@@ -319,7 +324,7 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
}
}
},
- [parseValue, onSelect],
+ [parseValue, isNonCustomItem, handleSelectItem, onSelect],
);
const handleInvalidCustom = useCallback(
@@ -330,11 +335,14 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
// Delay blur event until onMouseUp resulting in handleSelectItem being called before
// handleSubmitCustomValue and handleInvalidCustom. Clicking on the input should still move the
// cursor and therefore needs to be an exception to this.
- const handleMouseDown = useCallback((event: React.MouseEvent) => {
- if (event.target !== inputRef.current) {
- event.preventDefault();
- }
- }, []);
+ const handleMouseDown = useCallback(
+ (event: React.MouseEvent) => {
+ if (event.target !== inputRef.current) {
+ event.preventDefault();
+ }
+ },
+ [inputRef],
+ );
return (
<div onMouseDown={handleMouseDown}>
diff --git a/gui/src/renderer/components/cell/SettingsForm.tsx b/gui/src/renderer/components/cell/SettingsForm.tsx
index 1f2be529c1..9a901d88d8 100644
--- a/gui/src/renderer/components/cell/SettingsForm.tsx
+++ b/gui/src/renderer/components/cell/SettingsForm.tsx
@@ -1,5 +1,7 @@
import React, { useCallback, useContext, useEffect, useId, useMemo, useState } from 'react';
+import { useEffectEvent } from '../../lib/utility-hooks';
+
interface SettingsFormContext {
formSubmittable: boolean;
reportInputSubmittable: (key: string, submittable: boolean) => void;
@@ -31,11 +33,17 @@ export function useSettingsFormSubmittableReporter() {
(submittable: boolean) => {
context?.reportInputSubmittable(key, submittable);
},
- [context?.reportInputSubmittable],
+ [context, key],
);
- // Remove from required fields if unmounted.
- useEffect(() => () => context?.removeInput(key), []);
+ const clearRequiredFields = useEffectEvent(() => {
+ context?.removeInput(key);
+ });
+
+ useEffect(() => {
+ // Remove from required fields if unmounted.
+ return () => clearRequiredFields();
+ }, []);
return reportInputSubmittable;
}
diff --git a/gui/src/renderer/components/cell/SettingsGroup.tsx b/gui/src/renderer/components/cell/SettingsGroup.tsx
index f430d96a32..7ca8d0dff1 100644
--- a/gui/src/renderer/components/cell/SettingsGroup.tsx
+++ b/gui/src/renderer/components/cell/SettingsGroup.tsx
@@ -44,9 +44,9 @@ export function useSettingsGroupContext() {
[setError, key],
);
- const unsetErrorImpl = useCallback(() => unsetError?.(key), [unsetError]);
+ const unsetErrorImpl = useCallback(() => unsetError?.(key), [key, unsetError]);
- useEffect(() => () => unsetErrorImpl(), []);
+ useEffect(() => () => unsetErrorImpl(), [unsetErrorImpl]);
return { reportError, unsetError: unsetErrorImpl };
}
diff --git a/gui/src/renderer/components/cell/SettingsRadioGroup.tsx b/gui/src/renderer/components/cell/SettingsRadioGroup.tsx
index 46f3ccded7..6f4f1a0d90 100644
--- a/gui/src/renderer/components/cell/SettingsRadioGroup.tsx
+++ b/gui/src/renderer/components/cell/SettingsRadioGroup.tsx
@@ -17,13 +17,18 @@ interface SettingsSelectProps<T extends string> {
}
export function SettingsRadioGroup<T extends string>(props: SettingsSelectProps<T>) {
+ const { onUpdate } = props;
+
const [value, setValue] = useState<T>(props.defaultValue ?? props.items[0]?.value ?? '');
const key = useId();
- const onSelect = useCallback((value: T) => {
- setValue(value);
- props.onUpdate(value);
- }, []);
+ const onSelect = useCallback(
+ (value: T) => {
+ setValue(value);
+ onUpdate(value);
+ },
+ [onUpdate],
+ );
return (
<StyledRadioGroup>
@@ -92,11 +97,13 @@ interface RadioButtonProps<T extends string> {
}
function RadioButton<T extends string>(props: RadioButtonProps<T>) {
+ const { onSelect } = props;
+
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
- props.onSelect(event.target.value as T);
+ onSelect(event.target.value as T);
},
- [props.onSelect],
+ [onSelect],
);
return (
diff --git a/gui/src/renderer/components/cell/SettingsRow.tsx b/gui/src/renderer/components/cell/SettingsRow.tsx
index 1211098a70..f242106c92 100644
--- a/gui/src/renderer/components/cell/SettingsRow.tsx
+++ b/gui/src/renderer/components/cell/SettingsRow.tsx
@@ -104,10 +104,13 @@ export function SettingsRow(props: React.PropsWithChildren<IndentedRowProps>) {
unsetError?.();
}
},
- [reportError, unsetError],
+ [props.errorMessage, reportError, unsetError],
);
- const contextValue = useMemo(() => ({ invalid, setInvalid: setInvalidImpl }), [invalid]);
+ const contextValue = useMemo(
+ () => ({ invalid, setInvalid: setInvalidImpl }),
+ [invalid, setInvalidImpl],
+ );
return (
<settingsRowContext.Provider value={contextValue}>
diff --git a/gui/src/renderer/components/cell/SettingsSelect.tsx b/gui/src/renderer/components/cell/SettingsSelect.tsx
index f0d918ded6..7b5e1d7ab4 100644
--- a/gui/src/renderer/components/cell/SettingsSelect.tsx
+++ b/gui/src/renderer/components/cell/SettingsSelect.tsx
@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { colors } from '../../../config.json';
import { useScheduler } from '../../../shared/scheduler';
-import { useBoolean } from '../../lib/utilityHooks';
+import { useBoolean, useEffectEvent } from '../../lib/utility-hooks';
import { AriaInput } from '../AriaGroup';
import { smallNormalText } from '../common-styles';
import CustomScrollbars from '../CustomScrollbars';
@@ -99,10 +99,13 @@ export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>)
// Scheduler for clearing the search string after the user has stopped typing.
const searchClearScheduler = useScheduler();
- const onSelect = useCallback((value: T) => {
- setValue(value);
- closeDropdown();
- }, []);
+ const onSelect = useCallback(
+ (value: T) => {
+ setValue(value);
+ closeDropdown();
+ },
+ [closeDropdown],
+ );
// Handle keyboard shortcuts and type search
const onKeyDown = useCallback(
@@ -132,12 +135,16 @@ export function SettingsSelect<T extends string>(props: SettingsSelectProps<T>)
break;
}
},
- [props.items],
+ [props.items, searchClearScheduler],
);
+ const updateEvent = useEffectEvent((value: T) => {
+ props.onUpdate(value);
+ });
+
// Update the parent when the value changes.
useEffect(() => {
- props.onUpdate(value);
+ updateEvent(value);
}, [value]);
return (
@@ -229,9 +236,11 @@ interface ItemProps<T extends string> {
}
function Item<T extends string>(props: ItemProps<T>) {
+ const { onSelect } = props;
+
const onClick = useCallback(() => {
- props.onSelect(props.item.value);
- }, [props.onSelect, props.item.value]);
+ onSelect(props.item.value);
+ }, [onSelect, props.item.value]);
return (
<StyledItem
diff --git a/gui/src/renderer/components/cell/SettingsTextInput.tsx b/gui/src/renderer/components/cell/SettingsTextInput.tsx
index 1727997dc4..44381bc681 100644
--- a/gui/src/renderer/components/cell/SettingsTextInput.tsx
+++ b/gui/src/renderer/components/cell/SettingsTextInput.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
+import { useEffectEvent } from '../../lib/utility-hooks';
import { AriaInput } from '../AriaGroup';
import { smallNormalText } from '../common-styles';
import { useSettingsFormSubmittableReporter } from './SettingsForm';
@@ -49,7 +50,7 @@ export function SettingsNumberInput(props: SettingsNumberInputProps) {
(value: string) => {
onUpdate(parse(value));
},
- [onUpdate],
+ [onUpdate, parse],
);
const validateNumber = useCallback(
@@ -57,7 +58,7 @@ export function SettingsNumberInput(props: SettingsNumberInputProps) {
const parsedValue = parse(value);
return (parsedValue === undefined || validate?.(parsedValue)) ?? true;
},
- [validate],
+ [parse, validate],
);
return (
@@ -105,15 +106,19 @@ function Input<T extends ValueTypes>(props: InputProps<T>) {
reportSubmittable(value !== '' || optionalInForm === true);
}
},
- [onUpdate, propsOnChange, validate, optionalInForm],
+ [propsOnChange, onUpdate, validate, setInvalid, reportSubmittable, optionalInForm],
);
- // Report submittability to form context on load.
- useEffect(() => {
+ const updateReportSubmittable = useEffectEvent(() => {
const value = props.value ?? props.defaultValue ?? '';
reportSubmittable(
(value !== '' || optionalInForm === true) && validate?.(`${value}`) !== false,
);
+ });
+
+ // Report submittability to form context on load.
+ useEffect(() => {
+ updateReportSubmittable();
}, []);
return (