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,133 +83,117 @@ export type Props = {
role?: string; 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 // 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 // environments (ie. IE11) where typing quickly would sometimes miss inputs
export class QuickInput extends React.PureComponent<Props> { export const QuickInput = React.memo(({
private input?: HTMLInputElement | HTMLTextAreaElement; delayInputUpdate = false,
value = '',
clearable = false,
autoFocus,
forwardedRef,
inputComponent,
clearClassName,
clearableWithoutValue,
clearableTooltipText,
onClear: onClearFromProps,
...restProps
}: Props) => {
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
static defaultProps = { useEffect(() => {
delayInputUpdate: false, if (autoFocus) {
value: '',
clearable: false,
};
componentDidMount() {
if (this.props.autoFocus) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.input?.focus(); inputRef.current?.focus();
}); });
} }
}
componentDidUpdate(prevProps: Props) { /* eslint-disable-next-line react-hooks/exhaustive-deps --
if (prevProps.value !== this.props.value) { * This 'useEffect' should only run once during mount.
if (this.props.delayInputUpdate) { **/
requestAnimationFrame(this.updateInputFromProps); }, []);
} else {
this.updateInputFromProps(); useEffect(() => {
const updateInputFromProps = () => {
if (!inputRef.current || inputRef.current.value === value) {
return;
} }
}
}
private updateInputFromProps = () => { inputRef.current.value = value;
if (!this.input || this.input.value === this.props.value) { };
return;
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) => { const setInputRef = useCallback((input: HTMLInputElement) => {
if (this.props.forwardedRef) { if (forwardedRef) {
if (typeof this.props.forwardedRef === 'function') { if (typeof forwardedRef === 'function') {
this.props.forwardedRef(input); forwardedRef(input);
} else { } 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (this.props.onClear) { if (onClearFromProps) {
this.props.onClear(); onClearFromProps();
} }
this.input?.focus(); inputRef.current?.focus();
}; }, [onClearFromProps]);
render() { const showClearButton = onClearFromProps && (clearableWithoutValue || (clearable && value));
let clearableTooltipText = this.props.clearableTooltipText || '';
if (!clearableTooltipText) {
clearableTooltipText = (
<FormattedMessage
id={'input.clear'}
defaultMessage='Clear'
/>
);
}
const { const inputElement = React.createElement(
value, inputComponent || 'input',
inputComponent, {
clearable, ...restProps,
clearClassName, ref: setInputRef,
clearableWithoutValue, defaultValue: value, // Only set the defaultValue since the real one will be updated using the 'useEffect' above
...props },
} = this.props; );
Reflect.deleteProperty(props, 'delayInputUpdate'); return (
Reflect.deleteProperty(props, 'onClear'); <div className='input-wrapper'>
Reflect.deleteProperty(props, 'clearableTooltipText'); {inputElement}
Reflect.deleteProperty(props, 'channelId'); {showClearButton && (
Reflect.deleteProperty(props, 'clearClassName'); <WithTooltip title={clearableTooltipText || defaultClearableTooltipText}>
Reflect.deleteProperty(props, 'tooltipPosition'); <button
Reflect.deleteProperty(props, 'forwardedRef'); data-testid='input-clear'
className={classNames(clearClassName, 'input-clear visible')}
if (inputComponent !== AutosizeTextarea) { onClick={onClear}
Reflect.deleteProperty(props, 'onHeightChange'); >
Reflect.deleteProperty(props, 'onWidthChange'); <span
} className='input-clear-x'
aria-hidden='true'
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}
> >
<span <i className='icon icon-close-circle'/>
className='input-clear-x' </span>
aria-hidden='true' </button>
> </WithTooltip>
<i className='icon icon-close-circle'/> )}
</span> </div>
</button> );
</WithTooltip> });
)}
</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)