diff options
| author | Oskar <oskar@mullvad.net> | 2024-10-22 15:32:24 +0200 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-10-22 15:32:24 +0200 |
| commit | 1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129 (patch) | |
| tree | 43caf1e9884496f64c2333648c16381eaa1cf18a /gui/src/renderer/components/cell | |
| parent | 6533665d1fdf9a97e6ea1e36f01ee2f293b4338c (diff) | |
| parent | cf62ffdd17eac7958977895f098742c704aa3047 (diff) | |
| download | mullvadvpn-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.tsx | 73 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Section.tsx | 10 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Selector.tsx | 40 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsForm.tsx | 14 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsGroup.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsRadioGroup.tsx | 19 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsRow.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsSelect.tsx | 27 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/SettingsTextInput.tsx | 15 |
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 ( |
