summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-09-22 10:43:14 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-09-27 10:30:53 +0200
commit83ee97d103f44f32f3f821055184ef5aba4b4ee4 (patch)
tree34285e624577546b09b07cc7837b1f30fc03d02f /gui
parent56365c73773c52fc4711fb096e6b6e6d7b47db6c (diff)
downloadmullvadvpn-83ee97d103f44f32f3f821055184ef5aba4b4ee4.tar.xz
mullvadvpn-83ee97d103f44f32f3f821055184ef5aba4b4ee4.zip
Make Input a functional component and improve ref handling
Diffstat (limited to 'gui')
-rw-r--r--gui/src/renderer/components/cell/Input.tsx183
1 files changed, 85 insertions, 98 deletions
diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx
index 81ba648338..e68fac424c 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, useRef, useState } from 'rea
import styled from 'styled-components';
import { colors } from '../../../config.json';
-import { useBoolean } from '../../lib/utilityHooks';
+import { useBoolean, useCombinedRefs } from '../../lib/utilityHooks';
import { normalText } from '../common-styles';
import ImageView from '../ImageView';
import { BackAction } from '../KeyboardNavigation';
@@ -45,112 +45,96 @@ interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
onChangeValue?: (value: string) => void;
}
-interface IInputState {
- value?: string;
- focused: boolean;
-}
-
-export class Input extends React.Component<IInputProps, IInputState> {
- public state = {
- value: this.props.value ?? '',
- focused: false,
- };
-
- public inputRef = React.createRef<HTMLInputElement>();
-
- public componentDidUpdate(prevProps: IInputProps, _prevState: IInputState) {
- if (
- !this.state.focused &&
- prevProps.value !== this.props.value &&
- this.props.value !== this.state.value
- ) {
- this.setState(
- (_state, props) => ({
- value: props.value,
- }),
- () => {
- this.props.onChangeValue?.(this.state.value);
- },
- );
- }
- }
+function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) {
+ const {
+ validateValue,
+ modifyValue,
+ submitOnBlur,
+ onSubmitValue,
+ onChangeValue,
+ ...otherProps
+ } = props;
- public render() {
- const {
- type: _type,
- onChange: _onChange,
- onFocus: _onFocus,
- onBlur: _onBlur,
- onKeyPress: _onKeyPress,
- value: _value,
- modifyValue: _modifyValue,
- submitOnBlur: _submitOnBlur,
- onChangeValue: _onChangeValue,
- onSubmitValue: _onSubmitValue,
- validateValue,
- ...otherProps
- } = this.props;
+ const [value, setValue] = useState(props.value ?? '');
+ const [isFocused, setFocused, setBlurred] = useBoolean(false);
- const valid = validateValue?.(this.state.value);
+ const inputRef = useRef() as React.RefObject<HTMLInputElement>;
+ const combinedRef = useCombinedRefs(inputRef, forwardedRef);
- return (
- <CellDisabledContext.Consumer>
- {(disabled) => (
- <StyledInput
- ref={this.inputRef}
- type="text"
- valid={valid}
- focused={this.state.focused}
- aria-invalid={!valid}
- onChange={this.onChange}
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- onKeyPress={this.onKeyPress}
- value={this.state.value}
- disabled={disabled}
- {...otherProps}
- />
- )}
- </CellDisabledContext.Consumer>
- );
- }
+ const onFocus = useCallback(
+ (event: React.FocusEvent<HTMLInputElement>) => {
+ setFocused();
+ props.onFocus?.(event);
+ },
+ [props.onFocus],
+ );
- public blur = () => {
- this.inputRef.current?.blur();
- };
+ const onBlur = useCallback(
+ (event: React.FocusEvent<HTMLInputElement>) => {
+ setBlurred();
+ props.onBlur?.(event);
- private onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const value = this.props.modifyValue?.(event.target.value) ?? event.target.value;
- this.setState({ value });
- this.props.onChange?.(event);
- this.props.onChangeValue?.(value);
- };
+ if (validateValue?.(value) !== false && submitOnBlur) {
+ onSubmitValue?.(value);
+ }
+ },
+ [value, props.onBlur, validateValue, onSubmitValue, submitOnBlur],
+ );
- private onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
- this.setState({ focused: true });
- this.props.onFocus?.(event);
- };
+ const onChange = useCallback(
+ (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = modifyValue?.(event.target.value) ?? event.target.value;
+ setValue(value);
+ props.onChange?.(event);
+ onChangeValue?.(value);
+ },
+ [value, modifyValue, props.onSubmit, onSubmitValue],
+ );
- private onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
- this.setState({ focused: false });
+ const onKeyPress = useCallback(
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
+ if (event.key === 'Enter') {
+ onSubmitValue?.(value);
+ inputRef.current?.blur();
+ }
+ props.onKeyPress?.(event);
+ },
+ [value, onSubmitValue, inputRef, props.onKeyPress],
+ );
- this.props.onBlur?.(event);
- if (this.props.validateValue?.(this.state.value) !== false && this.props.submitOnBlur) {
- this.props.onSubmitValue?.(this.state.value);
- } else {
- this.setState({ value: this.props.value });
+ useEffect(() => {
+ if (!isFocused && props.value !== undefined && value !== props.value) {
+ setValue(props.value);
+ onChangeValue?.(props.value);
}
- };
+ }, [props.value]);
- private onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
- if (event.key === 'Enter') {
- this.props.onSubmitValue?.(this.state.value);
- this.inputRef.current?.blur();
- }
- this.props.onKeyPress?.(event);
- };
+ const valid = validateValue?.(value);
+
+ return (
+ <CellDisabledContext.Consumer>
+ {(disabled) => (
+ <StyledInput
+ {...otherProps}
+ ref={combinedRef}
+ type="text"
+ valid={valid}
+ focused={isFocused}
+ aria-invalid={!valid}
+ onChange={onChange}
+ onFocus={onFocus}
+ onBlur={onBlur}
+ onKeyPress={onKeyPress}
+ value={value}
+ disabled={disabled}
+ />
+ )}
+ </CellDisabledContext.Consumer>
+ );
}
+export const Input = React.memo(React.forwardRef(InputWithRef));
+
const InputFrame = styled.div((props: { focused: boolean }) => ({
display: 'flex',
flexGrow: 0,
@@ -177,12 +161,13 @@ const StyledAutoSizingTextInputWrapper = styled.div({
height: '100%',
});
-export function AutoSizingTextInput(props: IInputProps) {
+function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) {
const { onChangeValue, onFocus, onBlur, ...otherProps } = props;
const [value, setValue] = useState(otherProps.value ?? '');
const [focused, setFocused, setBlurred] = useBoolean(false);
- const inputRef = useRef() as React.RefObject<Input>;
+ const inputRef = useRef() as React.RefObject<HTMLInputElement>;
+ const combinedRef = useCombinedRefs(inputRef, forwardedRef);
const onChangeValueWrapper = useCallback(
(value: string) => {
@@ -216,7 +201,7 @@ export function AutoSizingTextInput(props: IInputProps) {
<StyledAutoSizingTextInputContainer>
<StyledAutoSizingTextInputWrapper>
<Input
- ref={inputRef}
+ ref={combinedRef}
onChangeValue={onChangeValueWrapper}
onBlur={onBlurWrapper}
onFocus={onFocusWrapper}
@@ -232,6 +217,8 @@ export function AutoSizingTextInput(props: IInputProps) {
);
}
+export const AutoSizingTextInput = React.memo(React.forwardRef(AutoSizingTextInputWithRef));
+
const StyledCellInputRowContainer = styled(Container)({
backgroundColor: 'white',
marginBottom: '1px',