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>
Этот коммит содержится в:
Daniel Espino García
2023-10-05 09:39:54 +02:00
коммит произвёл GitHub
родитель 45ccd50f8c
Коммит e3823a3263
5 изменённых файлов: 215 добавлений и 160 удалений

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

@@ -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.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref), "$$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], "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.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref), "$$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], "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.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.forward_ref), "$$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], "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,121 +17,119 @@ 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) {
super(props);
this.height = 0;
this.referenceRef = React.createRef();
}
componentDidMount() {
this.recalculateHeight();
this.recalculateWidth();
}
componentDidUpdate() {
this.recalculateHeight();
this.recalculateWidth();
}
private recalculateHeight = () => {
if (!this.referenceRef.current || !this.textarea) {
return;
}
const height = (this.referenceRef.current).scrollHeight;
const textarea = this.textarea;
if (height > 0 && height !== this.height) {
const style = getComputedStyle(textarea);
// Directly change the height to avoid circular rerenders
textarea.style.height = `${height}px`;
this.height = height;
this.props.onHeightChange?.(height, parseInt(style.maxHeight || '0', 10));
}
};
private recalculateWidth = () => {
if (!this.referenceRef) {
return;
}
const width = this.referenceRef.current?.offsetWidth || -1;
if (width >= 0) {
window.requestAnimationFrame(() => {
this.props.onWidthChange?.(width);
});
}
};
private setTextareaRef = (textarea: HTMLTextAreaElement) => {
if (this.props.forwardedRef) {
if (typeof this.props.forwardedRef === 'function') {
this.props.forwardedRef(textarea);
} else {
this.props.forwardedRef.current = textarea;
}
}
this.textarea = textarea;
};
render() {
const props = {...this.props};
Reflect.deleteProperty(props, 'onHeightChange');
Reflect.deleteProperty(props, 'providers');
Reflect.deleteProperty(props, 'channelId');
Reflect.deleteProperty(props, 'forwardedRef');
const {
value,
defaultValue,
placeholder,
disabled,
onInput,
// TODO: The provided `id` is sometimes hard-coded and used to interface with the // 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 // 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 // abstracted to avoid passing in an `id` prop at all, but we intentionally maintain
// the old behaviour to address ABC-213. // the old behaviour to address ABC-213.
id = 'autosize_textarea', id = 'autosize_textarea',
disabled,
value,
defaultValue,
onChange,
onHeightChange,
onWidthChange,
onInput,
placeholder,
...otherProps ...otherProps
} = props; }: Props, ref) => {
const height = useRef(0);
const textarea = useRef<HTMLTextAreaElement>();
const referenceRef = useRef<HTMLDivElement>(null);
const recalculateHeight = () => {
if (!referenceRef.current || !textarea.current) {
return;
}
const scrollHeight = referenceRef.current.scrollHeight;
const currentTextarea = textarea.current;
if (scrollHeight > 0 && scrollHeight !== height.current) {
const style = getComputedStyle(currentTextarea);
// Directly change the height to avoid circular rerenders
currentTextarea.style.height = `${scrollHeight}px`;
height.current = scrollHeight;
onHeightChange?.(scrollHeight, parseInt(style.maxHeight || '0', 10));
}
};
const recalculateWidth = () => {
if (!referenceRef.current) {
return;
}
const width = referenceRef.current?.offsetWidth || -1;
if (width >= 0) {
window.requestAnimationFrame(() => {
onWidthChange?.(width);
});
}
};
const setTextareaRef = useCallback((textareaRef: HTMLTextAreaElement) => {
if (ref) {
if (typeof ref === 'function') {
ref(textareaRef);
} else {
ref.current = textareaRef;
}
}
textarea.current = textareaRef;
}, [ref]);
useEffect(() => {
recalculateHeight();
recalculateWidth();
});
const heightProps = { const heightProps = {
rows: 0, rows: 0,
height: 0, height: 0,
}; };
Reflect.deleteProperty(otherProps, 'onWidthChange'); if (height.current <= 0) {
if (this.height <= 0) {
// Set an initial number of rows so that the textarea doesn't appear too large when its first rendered // Set an initial number of rows so that the textarea doesn't appear too large when its first rendered
heightProps.rows = 1; heightProps.rows = 1;
} else { } else {
heightProps.height = this.height; heightProps.height = height.current;
} }
let textareaPlaceholder = null; let textareaPlaceholder = null;
const placeholderAriaLabel = placeholder ? placeholder.toLowerCase() : ''; const placeholderAriaLabel = placeholder ? placeholder.toLowerCase() : '';
if (!this.props.value && !this.props.defaultValue) { if (!value && !defaultValue) {
textareaPlaceholder = ( textareaPlaceholder = (
<div <div
{...otherProps as any} {...otherProps}
id={`${id}_placeholder`} id={`${id}_placeholder`}
data-testid={`${id}_placeholder`} data-testid={`${id}_placeholder`}
style={styles.placeholder} style={styles.placeholder}
@@ -155,7 +155,7 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
<div> <div>
{textareaPlaceholder} {textareaPlaceholder}
<textarea <textarea
ref={this.setTextareaRef} ref={setTextareaRef}
data-testid={id} data-testid={id}
id={id} id={id}
{...heightProps} {...heightProps}
@@ -164,14 +164,14 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
aria-label={placeholderAriaLabel} aria-label={placeholderAriaLabel}
dir='auto' dir='auto'
disabled={disabled} disabled={disabled}
onChange={this.props.onChange} onChange={onChange}
onInput={onInput} onInput={onInput}
value={value} value={value}
defaultValue={defaultValue} defaultValue={defaultValue}
/> />
<div style={styles.container}> <div style={styles.container}>
<div <div
ref={this.referenceRef} ref={referenceRef}
id={id + '-reference'} id={id + '-reference'}
className={otherProps.className} className={otherProps.className}
style={styles.reference} style={styles.reference}
@@ -184,22 +184,6 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
</div> </div>
</div> </div>
); );
} });
}
const styles: { [Key: string]: CSSProperties} = { export default React.memo(AutosizeTextarea);
container: {height: 0, overflow: 'hidden'},
reference: {display: 'inline-block', height: 'auto', width: 'auto'},
placeholder: {overflow: 'hidden', textOverflow: 'ellipsis', opacity: 0.5, pointerEvents: 'none', position: 'absolute', whiteSpace: 'nowrap', background: 'none', borderColor: 'transparent'},
};
const forwarded = React.forwardRef<HTMLTextAreaElement>((props, ref) => (
<AutosizeTextarea
forwardedRef={ref}
{...props}
/>
));
forwarded.displayName = 'AutosizeTextarea';
export default forwarded;

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

@@ -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)>)>;