mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
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>
This commit is contained in:
parent
45ccd50f8c
commit
e3823a3263
@ -36,8 +36,24 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
||||
id="someid"
|
||||
inputComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"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]}
|
||||
@ -127,8 +143,24 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
||||
id="someid"
|
||||
inputComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"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]}
|
||||
@ -218,8 +250,24 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
||||
id="someid"
|
||||
inputComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"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]}
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {AutosizeTextarea} from 'components/autosize_textarea';
|
||||
import AutosizeTextarea from 'components/autosize_textarea';
|
||||
|
||||
describe('components/AutosizeTextarea', () => {
|
||||
test('should match snapshot, init', () => {
|
||||
|
@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {ChangeEvent, FormEvent, CSSProperties} from 'react';
|
||||
import type {ChangeEvent, FormEvent, HTMLProps} from 'react';
|
||||
import React, {useRef, useEffect, useCallback} from 'react';
|
||||
|
||||
import type {Intersection} from '@mattermost/types/utilities';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
@ -15,191 +17,173 @@ type Props = {
|
||||
onWidthChange?: (width: number) => void;
|
||||
onInput?: (e: FormEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder?: string;
|
||||
forwardedRef?: ((instance: HTMLTextAreaElement | null) => void) | React.MutableRefObject<HTMLTextAreaElement | null> | null;
|
||||
}
|
||||
} & Intersection<HTMLProps<HTMLTextAreaElement>, HTMLProps<HTMLDivElement>>;
|
||||
|
||||
export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
private height: number;
|
||||
const styles = {
|
||||
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;
|
||||
private referenceRef: React.RefObject<HTMLDivElement>;
|
||||
const AutosizeTextarea = React.forwardRef<HTMLTextAreaElement, Props>(({
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
// 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',
|
||||
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;
|
||||
|
||||
this.referenceRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.recalculateHeight();
|
||||
this.recalculateWidth();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.recalculateHeight();
|
||||
this.recalculateWidth();
|
||||
}
|
||||
|
||||
private recalculateHeight = () => {
|
||||
if (!this.referenceRef.current || !this.textarea) {
|
||||
const recalculateHeight = () => {
|
||||
if (!referenceRef.current || !textarea.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const height = (this.referenceRef.current).scrollHeight;
|
||||
const textarea = this.textarea;
|
||||
const scrollHeight = referenceRef.current.scrollHeight;
|
||||
const currentTextarea = textarea.current;
|
||||
|
||||
if (height > 0 && height !== this.height) {
|
||||
const style = getComputedStyle(textarea);
|
||||
if (scrollHeight > 0 && scrollHeight !== height.current) {
|
||||
const style = getComputedStyle(currentTextarea);
|
||||
|
||||
// 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 = () => {
|
||||
if (!this.referenceRef) {
|
||||
const recalculateWidth = () => {
|
||||
if (!referenceRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.referenceRef.current?.offsetWidth || -1;
|
||||
const width = referenceRef.current?.offsetWidth || -1;
|
||||
if (width >= 0) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.props.onWidthChange?.(width);
|
||||
onWidthChange?.(width);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private setTextareaRef = (textarea: HTMLTextAreaElement) => {
|
||||
if (this.props.forwardedRef) {
|
||||
if (typeof this.props.forwardedRef === 'function') {
|
||||
this.props.forwardedRef(textarea);
|
||||
const setTextareaRef = useCallback((textareaRef: HTMLTextAreaElement) => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(textareaRef);
|
||||
} 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() {
|
||||
const props = {...this.props};
|
||||
if (height.current <= 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 = height.current;
|
||||
}
|
||||
|
||||
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
|
||||
// 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>
|
||||
let textareaPlaceholder = null;
|
||||
const placeholderAriaLabel = placeholder ? placeholder.toLowerCase() : '';
|
||||
if (!value && !defaultValue) {
|
||||
textareaPlaceholder = (
|
||||
<div
|
||||
{...otherProps}
|
||||
id={`${id}_placeholder`}
|
||||
data-testid={`${id}_placeholder`}
|
||||
style={styles.placeholder}
|
||||
>
|
||||
{placeholder}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles: { [Key: string]: CSSProperties} = {
|
||||
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'},
|
||||
};
|
||||
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';
|
||||
}
|
||||
|
||||
const forwarded = React.forwardRef<HTMLTextAreaElement>((props, ref) => (
|
||||
<AutosizeTextarea
|
||||
forwardedRef={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
{textareaPlaceholder}
|
||||
<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 forwarded;
|
||||
export default React.memo(AutosizeTextarea);
|
||||
|
20
webapp/channels/src/components/common/hooks/useDidUpdate.ts
Normal file
20
webapp/channels/src/components/common/hooks/useDidUpdate.ts
Normal file
@ -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> =
|
||||
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)>)>;
|
Loading…
Reference in New Issue
Block a user