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.
Этот коммит содержится в:
@@ -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)
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user