feat(function component migration): migrate QuickInput to a function component (#31492)

* Initial commit

* feat(function component migration): migrate QuickInput to a function component

* fix: add missing export keyword

* Revert "Initial commit"

This reverts commit e40909c6d0590b67e00f62283f6e5a622fbaf9bb.

* refactor: reposition 'showClearButton' variable so tests pass

* refactor: ignore eslint warnings and rename props

Removed 'deleteProperty' calls since some props are destructured.

* refactor(quick_input): wrap functions in useCallback

Updated snapshots, tests, and removed dead code.

* fix(quick_input): add dependencies to useCallback

Restored quick_input.test.tsx to its initial state and made the value prop optional instead.
Этот коммит содержится в:
Vicktor
2025-06-30 17:05:25 +03:00
коммит произвёл GitHub
родитель b3724d1151
Коммит efa3b3b8db
3 изменённых файлов: 90 добавлений и 108 удалений

Просмотреть файл

@@ -14,7 +14,7 @@ describe('components/QuickInput', () => {
['when no onClear callback', {value: 'value', clearable: true}],
['when value undefined', {clearable: true, onClear: () => {}}],
['when value empty', {value: '', clearable: true, onClear: () => {}}],
])('should not render clear button', (description, props) => {
])('should not render clear button', (_description, props) => {
renderWithContext(
<QuickInput {...props}/>,
);

Просмотреть файл

@@ -3,11 +3,10 @@
import classNames from 'classnames';
import type {ReactComponentLike} from 'prop-types';
import React from 'react';
import React, {useCallback, useEffect, useRef} from 'react';
import type {ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import AutosizeTextarea from 'components/autosize_textarea';
import WithTooltip from 'components/with_tooltip';
export type Props = {
@@ -26,7 +25,7 @@ export type Props = {
/**
* The string value displayed in this input
*/
value: string;
value?: string;
/**
* When true, and an onClear callback is defined, show an X on the input field that clears
@@ -84,133 +83,117 @@ export type Props = {
role?: string;
}
const defaultClearableTooltipText = (
<FormattedMessage
id={'input.clear'}
defaultMessage='Clear'
/>);
// A component that can be used to make controlled inputs that function properly in certain
// environments (ie. IE11) where typing quickly would sometimes miss inputs
export class QuickInput extends React.PureComponent<Props> {
private input?: HTMLInputElement | HTMLTextAreaElement;
export const QuickInput = React.memo(({
delayInputUpdate = false,
value = '',
clearable = false,
autoFocus,
forwardedRef,
inputComponent,
clearClassName,
clearableWithoutValue,
clearableTooltipText,
onClear: onClearFromProps,
...restProps
}: Props) => {
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
static defaultProps = {
delayInputUpdate: false,
value: '',
clearable: false,
};
componentDidMount() {
if (this.props.autoFocus) {
useEffect(() => {
if (autoFocus) {
requestAnimationFrame(() => {
this.input?.focus();
inputRef.current?.focus();
});
}
}
componentDidUpdate(prevProps: Props) {
if (prevProps.value !== this.props.value) {
if (this.props.delayInputUpdate) {
requestAnimationFrame(this.updateInputFromProps);
} else {
this.updateInputFromProps();
/* eslint-disable-next-line react-hooks/exhaustive-deps --
* This 'useEffect' should only run once during mount.
**/
}, []);
useEffect(() => {
const updateInputFromProps = () => {
if (!inputRef.current || inputRef.current.value === value) {
return;
}
}
}
private updateInputFromProps = () => {
if (!this.input || this.input.value === this.props.value) {
return;
inputRef.current.value = value;
};
if (delayInputUpdate) {
requestAnimationFrame(updateInputFromProps);
} else {
updateInputFromProps();
}
this.input.value = this.props.value;
};
/* eslint-disable-next-line react-hooks/exhaustive-deps --
* This 'useEffect' should run only when 'value' prop changes.
**/
}, [value]);
private setInputRef = (input: HTMLInputElement) => {
if (this.props.forwardedRef) {
if (typeof this.props.forwardedRef === 'function') {
this.props.forwardedRef(input);
const setInputRef = useCallback((input: HTMLInputElement) => {
if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(input);
} else {
this.props.forwardedRef.current = input;
forwardedRef.current = input;
}
}
this.input = input;
};
inputRef.current = input;
}, [forwardedRef]);
private onClear = (e: React.MouseEvent<HTMLButtonElement> | React.TouchEvent) => {
const onClear = useCallback((e: React.MouseEvent<HTMLButtonElement> | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
if (this.props.onClear) {
this.props.onClear();
if (onClearFromProps) {
onClearFromProps();
}
this.input?.focus();
};
inputRef.current?.focus();
}, [onClearFromProps]);
render() {
let clearableTooltipText = this.props.clearableTooltipText || '';
if (!clearableTooltipText) {
clearableTooltipText = (
<FormattedMessage
id={'input.clear'}
defaultMessage='Clear'
/>
);
}
const showClearButton = onClearFromProps && (clearableWithoutValue || (clearable && value));
const {
value,
inputComponent,
clearable,
clearClassName,
clearableWithoutValue,
...props
} = this.props;
const inputElement = React.createElement(
inputComponent || 'input',
{
...restProps,
ref: setInputRef,
defaultValue: value, // Only set the defaultValue since the real one will be updated using the 'useEffect' above
},
);
Reflect.deleteProperty(props, 'delayInputUpdate');
Reflect.deleteProperty(props, 'onClear');
Reflect.deleteProperty(props, 'clearableTooltipText');
Reflect.deleteProperty(props, 'channelId');
Reflect.deleteProperty(props, 'clearClassName');
Reflect.deleteProperty(props, 'tooltipPosition');
Reflect.deleteProperty(props, 'forwardedRef');
if (inputComponent !== AutosizeTextarea) {
Reflect.deleteProperty(props, 'onHeightChange');
Reflect.deleteProperty(props, 'onWidthChange');
}
const inputElement = React.createElement(
inputComponent || 'input',
{
...props,
ref: this.setInputRef,
defaultValue: value, // Only set the defaultValue since the real one will be updated using componentDidUpdate
},
);
const showClearButton = this.props.onClear && (clearableWithoutValue || (clearable && value));
return (
<div className='input-wrapper'>
{inputElement}
{showClearButton && (
<WithTooltip title={clearableTooltipText}>
<button
data-testid='input-clear'
className={classNames(clearClassName, 'input-clear visible')}
onClick={this.onClear}
return (
<div className='input-wrapper'>
{inputElement}
{showClearButton && (
<WithTooltip title={clearableTooltipText || defaultClearableTooltipText}>
<button
data-testid='input-clear'
className={classNames(clearClassName, 'input-clear visible')}
onClick={onClear}
>
<span
className='input-clear-x'
aria-hidden='true'
>
<span
className='input-clear-x'
aria-hidden='true'
>
<i className='icon icon-close-circle'/>
</span>
</button>
</WithTooltip>
)}
</div>
);
}
}
<i className='icon icon-close-circle'/>
</span>
</button>
</WithTooltip>
)}
</div>
);
});
type ForwardedProps = Omit<React.ComponentPropsWithoutRef<typeof QuickInput>, 'forwardedRef'>;

Просмотреть файл

@@ -187,10 +187,9 @@ exports[`component/user_group_popover should match snapshot 1`] = `
/>
</svg>
</MagnifyIcon>
<QuickInput
<Memo()
className="user-group-popover_search-bar"
clearable={true}
delayInputUpdate={false}
onChange={[Function]}
onClear={[Function]}
placeholder="Search members"
@@ -208,7 +207,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
type="text"
/>
</div>
</QuickInput>
</Memo()>
</div>
</SearchBar>
<Connect(Component)