fix(app): fix perf issues and bugs with textarea autosizing (#20)
This commit is contained in:
@@ -106,6 +106,13 @@ import * as PlaceholderUtils from '~/utils/PlaceholderUtils';
|
||||
import wrapperStyles from './textarea/InputWrapper.module.css';
|
||||
import styles from './textarea/TextareaInput.module.css';
|
||||
|
||||
function readBorderBoxBlockSize(entry: ResizeObserverEntry): number {
|
||||
const bbs: any = (entry as any).borderBoxSize;
|
||||
if (Array.isArray(bbs) && bbs[0] && typeof bbs[0].blockSize === 'number') return bbs[0].blockSize;
|
||||
if (bbs && typeof bbs.blockSize === 'number') return bbs.blockSize;
|
||||
return (entry.target as HTMLElement).getBoundingClientRect().height;
|
||||
}
|
||||
|
||||
const ChannelTextareaContent = observer(
|
||||
({
|
||||
channel,
|
||||
@@ -123,7 +130,6 @@ const ChannelTextareaContent = observer(
|
||||
const [isInputAreaFocused, setIsInputAreaFocused] = React.useState(false);
|
||||
const [value, setValue] = React.useState('');
|
||||
const [showAllButtons, setShowAllButtons] = React.useState(true);
|
||||
const [_textareaHeight, setTextareaHeight] = React.useState(0);
|
||||
const [containerWidth, setContainerWidth] = React.useState(0);
|
||||
const [pendingMentionConfirmation, setPendingMentionConfirmation] = React.useState<MentionConfirmationInfo | null>(
|
||||
null,
|
||||
@@ -139,6 +145,64 @@ const ChannelTextareaContent = observer(
|
||||
const scrollerRef = React.useRef<ScrollerHandle>(null);
|
||||
useMarkdownKeybinds(isFocused);
|
||||
|
||||
const textareaHeightRef = React.useRef<number>(0);
|
||||
const handleTextareaHeightChange = React.useCallback((height: number) => {
|
||||
textareaHeightRef.current = height;
|
||||
}, []);
|
||||
|
||||
const inputBoxHeightRef = React.useRef<number | null>(null);
|
||||
const pendingLayoutDeltaRef = React.useRef(0);
|
||||
const flushScheduledRef = React.useRef(false);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
inputBoxHeightRef.current = null;
|
||||
pendingLayoutDeltaRef.current = 0;
|
||||
flushScheduledRef.current = false;
|
||||
|
||||
const flush = () => {
|
||||
flushScheduledRef.current = false;
|
||||
const delta = pendingLayoutDeltaRef.current;
|
||||
pendingLayoutDeltaRef.current = 0;
|
||||
if (!delta) return;
|
||||
if (delta <= 0) return;
|
||||
|
||||
ComponentDispatch.dispatch('LAYOUT_RESIZED', {
|
||||
channelId: channel.id,
|
||||
heightDelta: delta,
|
||||
});
|
||||
};
|
||||
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry) return;
|
||||
|
||||
const nextHeight = Math.round(readBorderBoxBlockSize(entry));
|
||||
const prevHeight = inputBoxHeightRef.current;
|
||||
|
||||
if (prevHeight == null) {
|
||||
inputBoxHeightRef.current = nextHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = nextHeight - prevHeight;
|
||||
if (!delta) return;
|
||||
|
||||
inputBoxHeightRef.current = nextHeight;
|
||||
pendingLayoutDeltaRef.current += delta;
|
||||
|
||||
if (!flushScheduledRef.current) {
|
||||
flushScheduledRef.current = true;
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
});
|
||||
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [channel.id]);
|
||||
|
||||
const showGiftButton = AccessibilityStore.showGiftButton;
|
||||
const showGifButton = AccessibilityStore.showGifButton;
|
||||
const showMemesButton = AccessibilityStore.showMemesButton;
|
||||
@@ -653,9 +717,15 @@ const ChannelTextareaContent = observer(
|
||||
React.useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
let lastWidth = -1;
|
||||
|
||||
const checkButtonVisibility = () => {
|
||||
if (!containerRef.current) return;
|
||||
const containerWidthLocal = containerRef.current.offsetWidth;
|
||||
|
||||
if (containerWidthLocal === lastWidth) return;
|
||||
lastWidth = containerWidthLocal;
|
||||
|
||||
const shouldShowAll = containerWidthLocal > 500;
|
||||
setShowAllButtons(shouldShowAll);
|
||||
setContainerWidth(containerWidthLocal);
|
||||
@@ -765,12 +835,12 @@ const ChannelTextareaContent = observer(
|
||||
<Scroller ref={scrollerRef} fade={true} className={styles.scroller} key="channel-textarea-scroller">
|
||||
<div style={{display: 'flex', flexDirection: 'column'}}>
|
||||
<TextareaInputField
|
||||
channelId={channel.id}
|
||||
disabled={disabled}
|
||||
isMobile={mobileLayout.enabled}
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
textareaRef={textareaRef}
|
||||
scrollerRef={scrollerRef}
|
||||
isFocused={isFocused}
|
||||
isAutocompleteAttached={isAutocompleteAttached}
|
||||
autocompleteOptions={autocompleteOptions}
|
||||
@@ -787,7 +857,7 @@ const ChannelTextareaContent = observer(
|
||||
handleTextChange(newValue, previousValueRef.current);
|
||||
setValue(newValue);
|
||||
}}
|
||||
onHeightChange={setTextareaHeight}
|
||||
onHeightChange={handleTextareaHeightChange}
|
||||
onCursorMove={onCursorMove}
|
||||
onArrowUp={handleArrowUp}
|
||||
onEnter={handleSubmit}
|
||||
|
||||
@@ -221,6 +221,7 @@ export const EditingMessageInput = observer(
|
||||
style={{position: 'absolute', visibility: 'hidden', pointerEvents: 'none'}}
|
||||
/>
|
||||
<TextareaInputField
|
||||
channelId={channel.id}
|
||||
disabled={false}
|
||||
isMobile={mobileLayout.enabled}
|
||||
value={value}
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
--message-group-spacing: 16px;
|
||||
}
|
||||
|
||||
.nativeAnchor {
|
||||
overflow-anchor: auto !important;
|
||||
}
|
||||
|
||||
.scrollerContainer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
@@ -718,6 +718,7 @@ export const Messages = observer(function Messages({channel}: {channel: ChannelR
|
||||
{topBar}
|
||||
<div className={styles.scrollerContainer}>
|
||||
<Scroller
|
||||
className={state.isAtBottom ? styles.nativeAnchor : undefined}
|
||||
fade={false}
|
||||
scrollbar="regular"
|
||||
hideThumbWhenWindowBlurred
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
.textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: transparent;
|
||||
@@ -88,6 +88,12 @@
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@supports (field-sizing: content) {
|
||||
.textarea {
|
||||
field-sizing: content;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -21,19 +21,18 @@ import {clsx} from 'clsx';
|
||||
import React from 'react';
|
||||
import * as HighlightActionCreators from '~/actions/HighlightActionCreators';
|
||||
import {type AutocompleteOption, isChannel} from '~/components/channel/Autocomplete';
|
||||
import type {ScrollerHandle} from '~/components/uikit/Scroller';
|
||||
import {useTextareaAutofocus} from '~/hooks/useTextareaAutofocus';
|
||||
import {ComponentDispatch} from '~/lib/ComponentDispatch';
|
||||
import {TextareaAutosize} from '~/lib/TextareaAutosize';
|
||||
import styles from './TextareaInput.module.css';
|
||||
|
||||
interface TextareaInputFieldProps {
|
||||
channelId: string;
|
||||
|
||||
disabled: boolean;
|
||||
isMobile: boolean;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
scrollerRef?: React.RefObject<ScrollerHandle | null> | null;
|
||||
shouldStickToBottomRef?: React.MutableRefObject<boolean>;
|
||||
isFocused?: boolean;
|
||||
isAutocompleteAttached: boolean;
|
||||
@@ -60,7 +59,6 @@ export const TextareaInputField = React.forwardRef<HTMLTextAreaElement, Textarea
|
||||
value,
|
||||
placeholder,
|
||||
textareaRef,
|
||||
scrollerRef,
|
||||
isAutocompleteAttached,
|
||||
autocompleteOptions,
|
||||
selectedIndex,
|
||||
@@ -75,12 +73,9 @@ export const TextareaInputField = React.forwardRef<HTMLTextAreaElement, Textarea
|
||||
setSelectedIndex,
|
||||
className,
|
||||
onKeyDown,
|
||||
shouldStickToBottomRef,
|
||||
},
|
||||
_ref,
|
||||
) => {
|
||||
const lastHeightRef = React.useRef<number>(0);
|
||||
|
||||
useTextareaAutofocus(textareaRef, isMobile, !disabled);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -124,49 +119,6 @@ export const TextareaInputField = React.forwardRef<HTMLTextAreaElement, Textarea
|
||||
}
|
||||
};
|
||||
|
||||
const handleHeightChange = (height: number, meta?: {rowHeight: number}) => {
|
||||
const clampToSingleRow = () => {
|
||||
if (value.length > 0 || !textareaRef.current || !meta?.rowHeight) {
|
||||
return height;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(textareaRef.current);
|
||||
const padding = (parseFloat(style.paddingTop || '0') || 0) + (parseFloat(style.paddingBottom || '0') || 0);
|
||||
const border =
|
||||
(parseFloat(style.borderTopWidth || '0') || 0) + (parseFloat(style.borderBottomWidth || '0') || 0);
|
||||
const singleRowHeight = meta.rowHeight + padding + border;
|
||||
|
||||
if (!Number.isFinite(singleRowHeight) || singleRowHeight <= 0 || singleRowHeight >= height) {
|
||||
return height;
|
||||
}
|
||||
|
||||
textareaRef.current.style.setProperty('height', `${singleRowHeight}px`, 'important');
|
||||
return singleRowHeight;
|
||||
};
|
||||
|
||||
const adjustedHeight = clampToSingleRow();
|
||||
|
||||
if (adjustedHeight === lastHeightRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const heightDelta = adjustedHeight - lastHeightRef.current;
|
||||
lastHeightRef.current = adjustedHeight;
|
||||
|
||||
const distanceFromBottom = scrollerRef?.current?.getDistanceFromBottom?.() ?? 0;
|
||||
const shouldStickToBottom = shouldStickToBottomRef?.current ?? distanceFromBottom <= 8;
|
||||
|
||||
onHeightChange(adjustedHeight);
|
||||
|
||||
if (shouldStickToBottom) {
|
||||
scrollerRef?.current?.scrollToBottom({animate: false});
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
ComponentDispatch.dispatch('LAYOUT_RESIZED', {heightDelta});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TextareaAutosize
|
||||
data-channel-textarea
|
||||
@@ -176,7 +128,7 @@ export const TextareaInputField = React.forwardRef<HTMLTextAreaElement, Textarea
|
||||
onBlur={onBlur}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onFocus={onFocus}
|
||||
onHeightChange={handleHeightChange}
|
||||
onHeightChange={(h) => onHeightChange(h)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
ref={textareaRef}
|
||||
|
||||
Reference in New Issue
Block a user