mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-53524/MM-53525 Clean up post textbox measurement (#23973)
* Don't always render post textbox preview in background * Remove most usage of custom-textarea--emoji-picker This class is only needed by the pre-AdvancedTextEditor usage of the Textbox. It otherwise conflicted with the code that controls the AdvancedTextEditor's right padding * Change right padding for post textbox to use CSS class * Remove AutosizeTextarea.recalculatePadding This was only needed because we started setting the padding of the textarea programatically at some point. Originally, we could rely on the reference textarea having the same styling because it had the same CSS class as the actual textarea. * Change reference textarea to a div Using an actual textarea to measure the size of the text doesn't allow us to measure the width of the text because textareas don't stretch to fit text. Using a div with the same styling as the textarea works though * Remove redundant measuring div from AutosizeTextarea * Update snapshots for AutosizeTextarea * Fix padding being counted twice when measuring width * Change AutosizeTextarea to use a ResizeObserver to be more responsive * Fix right padding on post preview * Disable transitions when AutosizeTextarea first measures * Fix measurement of trailing newlines * Revert "Disable transitions when AutosizeTextarea first measures" This reverts commit a036fdbfd64e49af9a2eaa3d3e06a5af453b1a5c. * Revert "Change AutosizeTextarea to use a ResizeObserver to be more responsive" This reverts commit c8fa4a19bde087a0fe80e3639aed2f3ae2eae04a. --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
b4a47803e6
commit
4d37aad678
@ -35,24 +35,15 @@ exports[`components/AutosizeTextarea should match snapshot, init 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
<div
|
||||
aria-hidden={true}
|
||||
dir="auto"
|
||||
disabled={true}
|
||||
id="autosize_textarea-reference"
|
||||
rows={1}
|
||||
style={
|
||||
Object {
|
||||
"height": "auto",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
id="autosize_textarea-measuring"
|
||||
style={
|
||||
Object {
|
||||
"display": "inline-block",
|
||||
"height": "auto",
|
||||
"width": "auto",
|
||||
}
|
||||
}
|
||||
|
@ -2,97 +2,8 @@
|
||||
|
||||
exports[`components/TextBox should match snapshot with additional, optional props 1`] = `
|
||||
<div
|
||||
className="textarea-wrapper textarea-wrapper--preview"
|
||||
className="textarea-wrapper"
|
||||
>
|
||||
<Connect(SuggestionBox)
|
||||
channelId="channelId"
|
||||
className="form-control custom-textarea custom-textarea--emoji-picker bad-connection custom-textarea--preview"
|
||||
contextId="channelId"
|
||||
disabled={true}
|
||||
id="someid"
|
||||
inputComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
}
|
||||
}
|
||||
listComponent={[Function]}
|
||||
listPosition="top"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onComposition={[Function]}
|
||||
onHeightChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
openWhenEmpty={true}
|
||||
placeholder="placeholder text"
|
||||
providers={
|
||||
Array [
|
||||
AtMentionProvider {
|
||||
"addLastViewAtToProfiles": [Function],
|
||||
"autocompleteGroups": Array [
|
||||
Object {
|
||||
"id": "gid1",
|
||||
},
|
||||
Object {
|
||||
"id": "gid2",
|
||||
},
|
||||
],
|
||||
"autocompleteUsersInChannel": [Function],
|
||||
"channelId": "channelId",
|
||||
"currentUserId": "currentUserId",
|
||||
"data": null,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"getProfilesInChannel": [Function],
|
||||
"lastCompletedWord": "",
|
||||
"lastPrefixWithNoResults": "",
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"priorityProfiles": undefined,
|
||||
"requestStarted": false,
|
||||
"searchAssociatedGroupsForReference": [Function],
|
||||
"triggerCharacter": "@",
|
||||
"useChannelMentions": true,
|
||||
},
|
||||
ChannelMentionProvider {
|
||||
"autocompleteChannels": [MockFunction],
|
||||
"delayChannelAutocomplete": false,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"lastCompletedWord": "",
|
||||
"lastPrefixTrimmed": "",
|
||||
"lastPrefixWithNoResults": "",
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"triggerCharacter": "~",
|
||||
},
|
||||
EmoticonProvider {
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"triggerCharacter": ":",
|
||||
},
|
||||
]
|
||||
}
|
||||
renderDividers={
|
||||
Array [
|
||||
"all",
|
||||
]
|
||||
}
|
||||
spellCheck="true"
|
||||
style={
|
||||
Object {
|
||||
"visibility": "hidden",
|
||||
}
|
||||
}
|
||||
value="some test text"
|
||||
/>
|
||||
<div
|
||||
className="form-control custom-textarea textbox-preview-area"
|
||||
onBlur={[Function]}
|
||||
|
@ -27,19 +27,33 @@
|
||||
.custom-textarea {
|
||||
// fix for a miscalculation due to the heavily overwritten custom-textarea class
|
||||
height: 0;
|
||||
padding-right: calc(16px + var(--right-padding-for-scrollbar) + var(--right-padding-for-preview-button));
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.custom-textarea--emoji-picker,
|
||||
&.textbox-preview-area {
|
||||
height: auto;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable length-zero-no-unit */
|
||||
--right-padding-for-scrollbar: 0px;
|
||||
--right-padding-for-preview-button: 0px;
|
||||
/* stylelint-enable length-zero-no-unit */
|
||||
|
||||
&.scroll {
|
||||
--right-padding-for-scrollbar: 8px;
|
||||
}
|
||||
|
||||
// With the formatting bar disabled, leave room on the right for the post controls until the text takes up
|
||||
// multiple lines. At that point, leave space below the text as if the controls were still there.
|
||||
&.formatting-bar {
|
||||
--right-padding-for-preview-button: 32px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
|
@ -359,12 +359,7 @@ const AdvanceTextEditor = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const inputPaddingLeft = parseInt(window.getComputedStyle(input, null).paddingLeft || '0', 10);
|
||||
const inputPaddingRight = parseInt(window.getComputedStyle(input, null).paddingRight || '0', 10);
|
||||
const inputPaddingX = inputPaddingLeft + inputPaddingRight;
|
||||
const currentWidth = width + inputPaddingX;
|
||||
|
||||
if (currentWidth >= maxWidth) {
|
||||
if (width >= maxWidth) {
|
||||
setShowFormattingSpacer(true);
|
||||
} else {
|
||||
setShowFormattingSpacer(false);
|
||||
@ -377,22 +372,6 @@ const AdvanceTextEditor = ({
|
||||
}
|
||||
}, [handleWidthChange, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
let padding = 16;
|
||||
if (showFormattingBar) {
|
||||
padding += 32;
|
||||
}
|
||||
if (renderScrollbar) {
|
||||
padding += 8;
|
||||
}
|
||||
|
||||
input.style.paddingRight = `${padding}px`;
|
||||
}, [showFormattingBar, renderScrollbar, input]);
|
||||
|
||||
const formattingBar = (
|
||||
<AutoHeightSwitcher
|
||||
showSlot={showFormattingBar ? 1 : 2}
|
||||
@ -417,6 +396,7 @@ const AdvanceTextEditor = ({
|
||||
className={classNames('AdvancedTextEditor', {
|
||||
'AdvancedTextEditor__attachment-disabled': !canUploadFiles,
|
||||
scroll: renderScrollbar,
|
||||
'formatting-bar': showFormattingBar,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
|
@ -6,6 +6,7 @@ import type {ChangeEvent, FormEvent, CSSProperties} from 'react';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
@ -21,8 +22,7 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
private height: number;
|
||||
|
||||
private textarea?: HTMLTextAreaElement;
|
||||
private referenceRef: React.RefObject<HTMLTextAreaElement>;
|
||||
private measuringRef: React.RefObject<HTMLDivElement>;
|
||||
private referenceRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -30,7 +30,6 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
this.height = 0;
|
||||
|
||||
this.referenceRef = React.createRef();
|
||||
this.measuringRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -41,7 +40,6 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
componentDidUpdate() {
|
||||
this.recalculateHeight();
|
||||
this.recalculateWidth();
|
||||
this.recalculatePadding();
|
||||
}
|
||||
|
||||
private recalculateHeight = () => {
|
||||
@ -64,25 +62,12 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
private recalculatePadding = () => {
|
||||
if (!this.referenceRef.current || !this.textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = this.textarea;
|
||||
const {paddingRight} = getComputedStyle(textarea);
|
||||
|
||||
if (paddingRight && paddingRight !== this.referenceRef.current.style.paddingRight) {
|
||||
this.referenceRef.current.style.paddingRight = paddingRight;
|
||||
}
|
||||
};
|
||||
|
||||
private recalculateWidth = () => {
|
||||
if (!this.measuringRef) {
|
||||
if (!this.referenceRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.measuringRef.current?.offsetWidth || -1;
|
||||
const width = this.referenceRef.current?.offsetWidth || -1;
|
||||
if (width >= 0) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.props.onWidthChange?.(width);
|
||||
@ -154,6 +139,18 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
@ -173,23 +170,16 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
<div style={styles.container}>
|
||||
<textarea
|
||||
<div
|
||||
ref={this.referenceRef}
|
||||
id={id + '-reference'}
|
||||
className={otherProps.className}
|
||||
style={styles.reference}
|
||||
dir='auto'
|
||||
disabled={true}
|
||||
rows={1}
|
||||
{...otherProps}
|
||||
value={value || defaultValue}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<div
|
||||
ref={this.measuringRef}
|
||||
id={id + '-measuring'}
|
||||
style={styles.measuring}
|
||||
>
|
||||
{value || defaultValue}
|
||||
{referenceValue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -199,9 +189,8 @@ export class AutosizeTextarea extends React.PureComponent<Props> {
|
||||
|
||||
const styles: { [Key: string]: CSSProperties} = {
|
||||
container: {height: 0, overflow: 'hidden'},
|
||||
reference: {height: 'auto', width: '100%'},
|
||||
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'},
|
||||
measuring: {width: 'auto', display: 'inline-block'},
|
||||
};
|
||||
|
||||
const forwarded = React.forwardRef<HTMLTextAreaElement>((props, ref) => (
|
||||
|
@ -263,10 +263,7 @@ export default class Textbox extends React.PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
let preview = null;
|
||||
|
||||
let textboxClassName = 'form-control custom-textarea';
|
||||
let textWrapperClass = 'textarea-wrapper';
|
||||
if (this.props.emojiEnabled) {
|
||||
textboxClassName += ' custom-textarea--emoji-picker';
|
||||
}
|
||||
@ -276,25 +273,28 @@ export default class Textbox extends React.PureComponent<Props> {
|
||||
if (this.props.hasLabels) {
|
||||
textboxClassName += ' textarea--has-labels';
|
||||
}
|
||||
if (this.props.preview) {
|
||||
textboxClassName += ' custom-textarea--preview';
|
||||
textWrapperClass += ' textarea-wrapper--preview';
|
||||
|
||||
preview = (
|
||||
if (this.props.preview) {
|
||||
return (
|
||||
<div
|
||||
tabIndex={this.props.tabIndex || 0}
|
||||
ref={this.preview}
|
||||
className={classNames('form-control custom-textarea textbox-preview-area', {'textarea--has-labels': this.props.hasLabels})}
|
||||
onKeyPress={this.props.onKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleBlur}
|
||||
ref={this.wrapper}
|
||||
className='textarea-wrapper'
|
||||
>
|
||||
<PostMarkdown
|
||||
message={this.props.value}
|
||||
mentionKeys={[]}
|
||||
channelId={this.props.channelId}
|
||||
imageProps={{hideUtilities: true}}
|
||||
/>
|
||||
<div
|
||||
tabIndex={this.props.tabIndex || 0}
|
||||
ref={this.preview}
|
||||
className={classNames('form-control custom-textarea textbox-preview-area', {'textarea--has-labels': this.props.hasLabels})}
|
||||
onKeyPress={this.props.onKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
<PostMarkdown
|
||||
message={this.props.value}
|
||||
mentionKeys={[]}
|
||||
channelId={this.props.channelId}
|
||||
imageProps={{hideUtilities: true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -302,7 +302,7 @@ export default class Textbox extends React.PureComponent<Props> {
|
||||
return (
|
||||
<div
|
||||
ref={this.wrapper}
|
||||
className={textWrapperClass}
|
||||
className='textarea-wrapper'
|
||||
>
|
||||
<SuggestionBox
|
||||
id={this.props.id}
|
||||
@ -334,7 +334,6 @@ export default class Textbox extends React.PureComponent<Props> {
|
||||
openWhenEmpty={this.props.openWhenEmpty}
|
||||
alignWithTextbox={this.props.alignWithTextbox}
|
||||
/>
|
||||
{preview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
line-height: 20px;
|
||||
resize: none;
|
||||
transition: none;
|
||||
white-space: pre-wrap;
|
||||
white-space: break-spaces;
|
||||
word-wrap: break-word;
|
||||
|
||||
&:focus {
|
||||
@ -31,14 +31,6 @@
|
||||
position: relative;
|
||||
min-height: 37px;
|
||||
|
||||
&.textarea-wrapper--preview {
|
||||
> div {
|
||||
&:first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textbox-preview-area {
|
||||
ul {
|
||||
white-space: normal;
|
||||
@ -384,14 +376,6 @@
|
||||
scrollbar-width: thin;
|
||||
transition: height 150ms ease-in-out;
|
||||
|
||||
&:not(.custom-textarea--emoji-picker) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
&.custom-textarea--emoji-picker {
|
||||
padding-right: 90px;
|
||||
}
|
||||
|
||||
&.textbox-preview-area {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -412,14 +396,6 @@
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: auto;
|
||||
|
||||
&:not(.custom-textarea--emoji-picker) {
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
&.custom-textarea--emoji-picker {
|
||||
padding-right: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -696,7 +672,7 @@
|
||||
|
||||
.custom-textarea {
|
||||
max-height: calc(50vh - 40px);
|
||||
padding: 13px 0 12px 16px;
|
||||
padding: 13px 16px 12px 16px;
|
||||
background-color: var(--center-channel-bg);
|
||||
|
||||
&.custom-textarea--emoji-picker {
|
||||
|
@ -757,13 +757,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
&.custom-textarea--emoji-picker {
|
||||
max-height: 162px;
|
||||
padding-right: 135px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
.app-content & {
|
||||
border-right: none;
|
||||
|
Loading…
Reference in New Issue
Block a user