Class to functional component example 1 (#24181)
* Class to functional component example 1 * Fix lint and tests * Fix --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
45ccd50f8c
Коммит
e3823a3263
@@ -36,8 +36,24 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
|||||||
id="someid"
|
id="someid"
|
||||||
inputComponent={
|
inputComponent={
|
||||||
Object {
|
Object {
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.memo),
|
||||||
"render": [Function],
|
"compare": null,
|
||||||
|
"type": Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"propTypes": Object {
|
||||||
|
"className": [Function],
|
||||||
|
"defaultValue": [Function],
|
||||||
|
"disabled": [Function],
|
||||||
|
"id": [Function],
|
||||||
|
"onChange": [Function],
|
||||||
|
"onHeightChange": [Function],
|
||||||
|
"onInput": [Function],
|
||||||
|
"onWidthChange": [Function],
|
||||||
|
"placeholder": [Function],
|
||||||
|
"value": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listComponent={[Function]}
|
listComponent={[Function]}
|
||||||
@@ -127,8 +143,24 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
|||||||
id="someid"
|
id="someid"
|
||||||
inputComponent={
|
inputComponent={
|
||||||
Object {
|
Object {
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.memo),
|
||||||
"render": [Function],
|
"compare": null,
|
||||||
|
"type": Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"propTypes": Object {
|
||||||
|
"className": [Function],
|
||||||
|
"defaultValue": [Function],
|
||||||
|
"disabled": [Function],
|
||||||
|
"id": [Function],
|
||||||
|
"onChange": [Function],
|
||||||
|
"onHeightChange": [Function],
|
||||||
|
"onInput": [Function],
|
||||||
|
"onWidthChange": [Function],
|
||||||
|
"placeholder": [Function],
|
||||||
|
"value": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listComponent={[Function]}
|
listComponent={[Function]}
|
||||||
@@ -218,8 +250,24 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
|||||||
id="someid"
|
id="someid"
|
||||||
inputComponent={
|
inputComponent={
|
||||||
Object {
|
Object {
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.memo),
|
||||||
"render": [Function],
|
"compare": null,
|
||||||
|
"type": Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"propTypes": Object {
|
||||||
|
"className": [Function],
|
||||||
|
"defaultValue": [Function],
|
||||||
|
"disabled": [Function],
|
||||||
|
"id": [Function],
|
||||||
|
"onChange": [Function],
|
||||||
|
"onHeightChange": [Function],
|
||||||
|
"onInput": [Function],
|
||||||
|
"onWidthChange": [Function],
|
||||||
|
"placeholder": [Function],
|
||||||
|
"value": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listComponent={[Function]}
|
listComponent={[Function]}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {AutosizeTextarea} from 'components/autosize_textarea';
|
import AutosizeTextarea from 'components/autosize_textarea';
|
||||||
|
|
||||||
describe('components/AutosizeTextarea', () => {
|
describe('components/AutosizeTextarea', () => {
|
||||||
test('should match snapshot, init', () => {
|
test('should match snapshot, init', () => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import type {ChangeEvent, FormEvent, HTMLProps} from 'react';
|
||||||
import type {ChangeEvent, FormEvent, CSSProperties} from 'react';
|
import React, {useRef, useEffect, useCallback} from 'react';
|
||||||
|
|
||||||
|
import type {Intersection} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -15,191 +17,173 @@ type Props = {
|
|||||||
onWidthChange?: (width: number) => void;
|
onWidthChange?: (width: number) => void;
|
||||||
onInput?: (e: FormEvent<HTMLTextAreaElement>) => void;
|
onInput?: (e: FormEvent<HTMLTextAreaElement>) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
forwardedRef?: ((instance: HTMLTextAreaElement | null) => void) | React.MutableRefObject<HTMLTextAreaElement | null> | null;
|
} & Intersection<HTMLProps<HTMLTextAreaElement>, HTMLProps<HTMLDivElement>>;
|
||||||
}
|
|
||||||
|
|
||||||
export class AutosizeTextarea extends React.PureComponent<Props> {
|
const styles = {
|
||||||
private height: number;
|
container: {
|
||||||
|
height: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
display: 'inline-block',
|
||||||
|
height: 'auto',
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
opacity: 0.5,
|
||||||
|
pointerEvents: 'none' as const,
|
||||||
|
position: 'absolute' as const,
|
||||||
|
whiteSpace: 'nowrap' as const,
|
||||||
|
background: 'none',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
private textarea?: HTMLTextAreaElement;
|
const AutosizeTextarea = React.forwardRef<HTMLTextAreaElement, Props>(({
|
||||||
private referenceRef: React.RefObject<HTMLDivElement>;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
// TODO: The provided `id` is sometimes hard-coded and used to interface with the
|
||||||
super(props);
|
// component, e.g. `post_textbox`, so it can't be changed. This would ideally be
|
||||||
|
// abstracted to avoid passing in an `id` prop at all, but we intentionally maintain
|
||||||
|
// the old behaviour to address ABC-213.
|
||||||
|
id = 'autosize_textarea',
|
||||||
|
disabled,
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
onHeightChange,
|
||||||
|
onWidthChange,
|
||||||
|
onInput,
|
||||||
|
placeholder,
|
||||||
|
...otherProps
|
||||||
|
}: Props, ref) => {
|
||||||
|
const height = useRef(0);
|
||||||
|
const textarea = useRef<HTMLTextAreaElement>();
|
||||||
|
const referenceRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
this.height = 0;
|
const recalculateHeight = () => {
|
||||||
|
if (!referenceRef.current || !textarea.current) {
|
||||||
this.referenceRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.recalculateHeight();
|
|
||||||
this.recalculateWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.recalculateHeight();
|
|
||||||
this.recalculateWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
private recalculateHeight = () => {
|
|
||||||
if (!this.referenceRef.current || !this.textarea) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = (this.referenceRef.current).scrollHeight;
|
const scrollHeight = referenceRef.current.scrollHeight;
|
||||||
const textarea = this.textarea;
|
const currentTextarea = textarea.current;
|
||||||
|
|
||||||
if (height > 0 && height !== this.height) {
|
if (scrollHeight > 0 && scrollHeight !== height.current) {
|
||||||
const style = getComputedStyle(textarea);
|
const style = getComputedStyle(currentTextarea);
|
||||||
|
|
||||||
// Directly change the height to avoid circular rerenders
|
// Directly change the height to avoid circular rerenders
|
||||||
textarea.style.height = `${height}px`;
|
currentTextarea.style.height = `${scrollHeight}px`;
|
||||||
|
|
||||||
this.height = height;
|
height.current = scrollHeight;
|
||||||
|
|
||||||
this.props.onHeightChange?.(height, parseInt(style.maxHeight || '0', 10));
|
onHeightChange?.(scrollHeight, parseInt(style.maxHeight || '0', 10));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private recalculateWidth = () => {
|
const recalculateWidth = () => {
|
||||||
if (!this.referenceRef) {
|
if (!referenceRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = this.referenceRef.current?.offsetWidth || -1;
|
const width = referenceRef.current?.offsetWidth || -1;
|
||||||
if (width >= 0) {
|
if (width >= 0) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.props.onWidthChange?.(width);
|
onWidthChange?.(width);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private setTextareaRef = (textarea: HTMLTextAreaElement) => {
|
const setTextareaRef = useCallback((textareaRef: HTMLTextAreaElement) => {
|
||||||
if (this.props.forwardedRef) {
|
if (ref) {
|
||||||
if (typeof this.props.forwardedRef === 'function') {
|
if (typeof ref === 'function') {
|
||||||
this.props.forwardedRef(textarea);
|
ref(textareaRef);
|
||||||
} else {
|
} else {
|
||||||
this.props.forwardedRef.current = textarea;
|
ref.current = textareaRef;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textarea = textarea;
|
textarea.current = textareaRef;
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
recalculateHeight();
|
||||||
|
recalculateWidth();
|
||||||
|
});
|
||||||
|
|
||||||
|
const heightProps = {
|
||||||
|
rows: 0,
|
||||||
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (height.current <= 0) {
|
||||||
const props = {...this.props};
|
// Set an initial number of rows so that the textarea doesn't appear too large when its first rendered
|
||||||
|
heightProps.rows = 1;
|
||||||
|
} else {
|
||||||
|
heightProps.height = height.current;
|
||||||
|
}
|
||||||
|
|
||||||
Reflect.deleteProperty(props, 'onHeightChange');
|
let textareaPlaceholder = null;
|
||||||
Reflect.deleteProperty(props, 'providers');
|
const placeholderAriaLabel = placeholder ? placeholder.toLowerCase() : '';
|
||||||
Reflect.deleteProperty(props, 'channelId');
|
if (!value && !defaultValue) {
|
||||||
Reflect.deleteProperty(props, 'forwardedRef');
|
textareaPlaceholder = (
|
||||||
|
<div
|
||||||
const {
|
{...otherProps}
|
||||||
value,
|
id={`${id}_placeholder`}
|
||||||
defaultValue,
|
data-testid={`${id}_placeholder`}
|
||||||
placeholder,
|
style={styles.placeholder}
|
||||||
disabled,
|
>
|
||||||
onInput,
|
{placeholder}
|
||||||
|
|
||||||
// TODO: The provided `id` is sometimes hard-coded and used to interface with the
|
|
||||||
// component, e.g. `post_textbox`, so it can't be changed. This would ideally be
|
|
||||||
// abstracted to avoid passing in an `id` prop at all, but we intentionally maintain
|
|
||||||
// the old behaviour to address ABC-213.
|
|
||||||
id = 'autosize_textarea',
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const heightProps = {
|
|
||||||
rows: 0,
|
|
||||||
height: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
Reflect.deleteProperty(otherProps, 'onWidthChange');
|
|
||||||
|
|
||||||
if (this.height <= 0) {
|
|
||||||
// Set an initial number of rows so that the textarea doesn't appear too large when its first rendered
|
|
||||||
heightProps.rows = 1;
|
|
||||||
} else {
|
|
||||||
heightProps.height = this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let textareaPlaceholder = null;
|
|
||||||
const placeholderAriaLabel = placeholder ? placeholder.toLowerCase() : '';
|
|
||||||
if (!this.props.value && !this.props.defaultValue) {
|
|
||||||
textareaPlaceholder = (
|
|
||||||
<div
|
|
||||||
{...otherProps as any}
|
|
||||||
id={`${id}_placeholder`}
|
|
||||||
data-testid={`${id}_placeholder`}
|
|
||||||
style={styles.placeholder}
|
|
||||||
>
|
|
||||||
{placeholder}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let referenceValue = value || defaultValue;
|
|
||||||
if (referenceValue?.endsWith('\n')) {
|
|
||||||
// In a div, the browser doesn't always count characters at the end of a line when measuring the dimensions
|
|
||||||
// of text. In the spec, they refer to those characters as "hanging". No matter what value we set for the
|
|
||||||
// `white-space` of a div, a single newline at the end of the div will always hang.
|
|
||||||
//
|
|
||||||
// The textarea doesn't have that behaviour, so we need to trick the reference div into measuring that
|
|
||||||
// newline, and it seems like the best way to do that is by adding a second newline because only the final
|
|
||||||
// one hangs.
|
|
||||||
referenceValue += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{textareaPlaceholder}
|
|
||||||
<textarea
|
|
||||||
ref={this.setTextareaRef}
|
|
||||||
data-testid={id}
|
|
||||||
id={id}
|
|
||||||
{...heightProps}
|
|
||||||
{...otherProps}
|
|
||||||
role='textbox'
|
|
||||||
aria-label={placeholderAriaLabel}
|
|
||||||
dir='auto'
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
onInput={onInput}
|
|
||||||
value={value}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
/>
|
|
||||||
<div style={styles.container}>
|
|
||||||
<div
|
|
||||||
ref={this.referenceRef}
|
|
||||||
id={id + '-reference'}
|
|
||||||
className={otherProps.className}
|
|
||||||
style={styles.reference}
|
|
||||||
dir='auto'
|
|
||||||
disabled={true}
|
|
||||||
aria-hidden={true}
|
|
||||||
>
|
|
||||||
{referenceValue}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const styles: { [Key: string]: CSSProperties} = {
|
let referenceValue = value || defaultValue;
|
||||||
container: {height: 0, overflow: 'hidden'},
|
if (referenceValue?.endsWith('\n')) {
|
||||||
reference: {display: 'inline-block', height: 'auto', width: 'auto'},
|
// In a div, the browser doesn't always count characters at the end of a line when measuring the dimensions
|
||||||
placeholder: {overflow: 'hidden', textOverflow: 'ellipsis', opacity: 0.5, pointerEvents: 'none', position: 'absolute', whiteSpace: 'nowrap', background: 'none', borderColor: 'transparent'},
|
// of text. In the spec, they refer to those characters as "hanging". No matter what value we set for the
|
||||||
};
|
// `white-space` of a div, a single newline at the end of the div will always hang.
|
||||||
|
//
|
||||||
|
// The textarea doesn't have that behaviour, so we need to trick the reference div into measuring that
|
||||||
|
// newline, and it seems like the best way to do that is by adding a second newline because only the final
|
||||||
|
// one hangs.
|
||||||
|
referenceValue += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
const forwarded = React.forwardRef<HTMLTextAreaElement>((props, ref) => (
|
return (
|
||||||
<AutosizeTextarea
|
<div>
|
||||||
forwardedRef={ref}
|
{textareaPlaceholder}
|
||||||
{...props}
|
<textarea
|
||||||
/>
|
ref={setTextareaRef}
|
||||||
));
|
data-testid={id}
|
||||||
|
id={id}
|
||||||
|
{...heightProps}
|
||||||
|
{...otherProps}
|
||||||
|
role='textbox'
|
||||||
|
aria-label={placeholderAriaLabel}
|
||||||
|
dir='auto'
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
onInput={onInput}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div
|
||||||
|
ref={referenceRef}
|
||||||
|
id={id + '-reference'}
|
||||||
|
className={otherProps.className}
|
||||||
|
style={styles.reference}
|
||||||
|
dir='auto'
|
||||||
|
disabled={true}
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
|
{referenceValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
forwarded.displayName = 'AutosizeTextarea';
|
export default React.memo(AutosizeTextarea);
|
||||||
|
|
||||||
export default forwarded;
|
|
||||||
|
|||||||
20
webapp/channels/src/components/common/hooks/useDidUpdate.ts
Обычный файл
20
webapp/channels/src/components/common/hooks/useDidUpdate.ts
Обычный файл
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
// Disable consistent return since the effectCallback allows for non consistent returns.
|
||||||
|
/* eslint-disable consistent-return */
|
||||||
|
|
||||||
|
import {useEffect, useRef} from 'react';
|
||||||
|
|
||||||
|
const useDidUpdate: typeof useEffect = (effect, deps) => {
|
||||||
|
const mounted = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted.current) {
|
||||||
|
return effect();
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted.current = true;
|
||||||
|
}, deps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDidUpdate;
|
||||||
@@ -24,3 +24,6 @@ export type ValueOf<T> = T[keyof T];
|
|||||||
*/
|
*/
|
||||||
export type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
|
export type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
|
||||||
Pick<T, Exclude<keyof T, Keys>> & {[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>}[Keys];
|
Pick<T, Exclude<keyof T, Keys>> & {[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>}[Keys];
|
||||||
|
|
||||||
|
export type Intersection<T1, T2> =
|
||||||
|
Omit<Omit<T1&T2, keyof(Omit<T1, keyof(T2)>)>, keyof(Omit<T2, keyof(T1)>)>;
|
||||||
Ссылка в новой задаче
Block a user