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 no onClear callback', {value: 'value', clearable: true}],
['when value undefined', {clearable: true, onClear: () => {}}], ['when value undefined', {clearable: true, onClear: () => {}}],
['when value empty', {value: '', 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( renderWithContext(
<QuickInput {...props}/>, <QuickInput {...props}/>,
); );

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

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

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

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