Skip to content

Commit 8db4063

Browse files
authored
fix: Doesn't scroll in the MessageInput when pasting (#1224)
[CLNP-5328](https://sendbird.atlassian.net/browse/CLNP-5328) [SBISSUE-17384](https://sendbird.atlassian.net/browse/SBISSUE-17384) #### ChangeLog Fixes * Modified the `MessageInput` to scroll to the caret position when pasting text. * The maximum height of the `MessageInput` has been extended to `'92px'` #### Fix Functions * Created a `scrollToCaret` and replaced the `displayCaret` * Created a `handleCommonBehavior` for the common behavior of event handling logics * Removed an unused logic `setHeight` Variables * Set a default value to the `internalRef` CSS * Extended the maximum input height from `56px` to `92px` Chores * Fixed typo 'easiily' to 'easily'
1 parent d058370 commit 8db4063

File tree

4 files changed

+37
-65
lines changed

4 files changed

+37
-65
lines changed

src/ui/MessageInput/hooks/usePaste/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { createPasteNode, domToMessageTemplate, extractTextFromNodes, getLeafNod
1717
export function usePaste({
1818
ref,
1919
setIsInput,
20-
setHeight,
2120
channel,
2221
setMentionedUsers,
2322
}: DynamicProps): (e: React.ClipboardEvent<HTMLDivElement>) => void {
@@ -29,7 +28,6 @@ export function usePaste({
2928
const text = e.clipboardData.getData('text') || getURIListText(e);
3029
document.execCommand('insertText', false, sanitizeString(text));
3130
setIsInput(true);
32-
setHeight();
3331
return;
3432
}
3533

@@ -46,7 +44,6 @@ export function usePaste({
4644
document.execCommand('insertText', false, sanitizeString(text));
4745
pasteNode.remove();
4846
setIsInput(true);
49-
setHeight();
5047
return;
5148
}
5249

@@ -62,8 +59,7 @@ export function usePaste({
6259
}
6360

6461
setIsInput(true);
65-
setHeight();
66-
}, [ref, setIsInput, setHeight, channel, setMentionedUsers]);
62+
}, [ref, setIsInput, channel, setMentionedUsers]);
6763
}
6864

6965
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#dragging_links

src/ui/MessageInput/hooks/usePaste/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ export type DynamicProps = {
1212
channel: OpenChannel | GroupChannel;
1313
setMentionedUsers: React.Dispatch<React.SetStateAction<User[]>>;
1414
setIsInput: React.Dispatch<React.SetStateAction<boolean>>;
15-
setHeight: () => void;
1615
};

src/ui/MessageInput/index.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
font-stretch: normal;
1515
font-style: normal;
1616
line-height: 1.43;
17-
height: 56px;
17+
max-height: 92px;
1818
overflow-y: scroll;
1919
letter-spacing: normal;
2020
padding: 18px 64px 18px 16px;

src/ui/MessageInput/index.tsx

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,27 @@ import { OpenChannel } from '@sendbird/chat/openChannel';
2828
import { UserMessage } from '@sendbird/chat/message';
2929

3030
const TEXT_FIELD_ID = 'sendbird-message-input-text-field';
31-
const LINE_HEIGHT = 76;
32-
const DEFAULT_CHAT_VIEW_HEIGHT = 600;
3331
const noop = () => {
3432
return null;
3533
};
3634

37-
const displayCaret = (element: HTMLInputElement, position: number) => {
38-
const range = document.createRange();
39-
const sel = window.getSelection();
40-
range.setStart(element.childNodes[0], position);
41-
range.collapse(true);
42-
sel?.removeAllRanges();
43-
sel?.addRange(range);
44-
element.focus();
35+
const scrollToCaret = () => {
36+
const selection = window.getSelection();
37+
if (selection && selection.rangeCount > 0) {
38+
const range = selection.getRangeAt(0);
39+
const caretNode = range.endContainer;
40+
41+
// Ensure the caret is in a text node
42+
if (caretNode.nodeType === NodeTypes.TextNode) {
43+
const parentElement = caretNode.parentElement;
44+
45+
// Scroll the parent element of the caret into view
46+
parentElement?.scrollIntoView?.({
47+
behavior: 'smooth',
48+
block: 'nearest',
49+
});
50+
}
51+
}
4552
};
4653

4754
const resetInput = (ref: MutableRefObject<HTMLInputElement | null> | null) => {
@@ -133,7 +140,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
133140
acceptableMimeTypes,
134141
} = props;
135142

136-
const internalRef = (externalRef && 'current' in externalRef) ? externalRef : null;
143+
const internalRef = (externalRef && 'current' in externalRef) ? externalRef : useRef(null);
137144
const ghostInputRef = useRef<HTMLInputElement>(null);
138145

139146
const textFieldId = messageFieldId || TEXT_FIELD_ID;
@@ -149,46 +156,15 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
149156
const [isInput, setIsInput] = useState(false);
150157
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([]);
151158
const [targetStringInfo, setTargetStringInfo] = useState({ ...initialTargetStringInfo });
152-
const setHeight = useCallback(
153-
() => {
154-
const elem = internalRef?.current;
155-
if (!elem) return;
156-
157-
try {
158-
const estimatedChatViewHeight = window.document.body.offsetHeight || DEFAULT_CHAT_VIEW_HEIGHT;
159-
const MAX_HEIGHT = estimatedChatViewHeight * 0.6;
160-
if (elem.scrollHeight >= LINE_HEIGHT) {
161-
if (MAX_HEIGHT < elem.scrollHeight) {
162-
elem.style.height = 'auto';
163-
elem.style.height = `${MAX_HEIGHT}px`;
164-
} else {
165-
elem.style.height = '';
166-
}
167-
}
168-
} catch (error) {
169-
// error
170-
}
171-
},
172-
[],
173-
);
174159

175160
// #Edit mode
176-
// for easilly initialize input value from outside, but
161+
// for easily initialize input value from outside, but
177162
// useEffect(_, [channelUrl]) erase it
178163
const initialValue = props?.value;
179164
useEffect(() => {
180165
const textField = internalRef?.current;
181-
try {
182-
if (textField && initialValue) {
183-
textField.innerHTML = initialValue;
184-
displayCaret(textField, initialValue?.length);
185-
}
186-
} catch {
187-
//
188-
}
189166
setMentionedUserIds([]);
190167
setIsInput(textField?.textContent ? textField.textContent.trim().length > 0 : false);
191-
setHeight();
192168
}, [initialValue]);
193169

194170
// #Mention | Clear input value when channel changes
@@ -243,7 +219,6 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
243219
setMentionedUserIds([]);
244220
}
245221
setIsInput(textField?.textContent ? textField?.textContent?.trim().length > 0 : false);
246-
setHeight();
247222
}
248223
}, [isEdit, message]);
249224

@@ -314,7 +289,6 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
314289
textField.focus();
315290
}
316291
setTargetStringInfo({ ...initialTargetStringInfo });
317-
setHeight();
318292
useMentionedLabelDetection();
319293
}
320294
}
@@ -411,7 +385,6 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
411385
}
412386

413387
setIsInput(false);
414-
setHeight();
415388
}
416389
} catch (error) {
417390
eventHandlers?.message?.onSendMessageFailed?.(message, error);
@@ -437,7 +410,6 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
437410
setMentionedUsers,
438411
channel,
439412
setIsInput,
440-
setHeight,
441413
});
442414

443415
const uploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -453,6 +425,12 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
453425
}
454426
};
455427

428+
const handleCommonBehavior = (handleEvent) => {
429+
scrollToCaret();
430+
type CommonEvent<T> = React.KeyboardEvent<T> | React.MouseEvent<T> | React.ClipboardEvent<T>;
431+
return (e: CommonEvent<HTMLDivElement>) => handleEvent(e);
432+
};
433+
456434
return (
457435
<form className={classnames(
458436
...(Array.isArray(className) ? className : [className]),
@@ -474,11 +452,11 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
474452
contentEditable={!disabled}
475453
role="textbox"
476454
aria-label="Text Input"
477-
ref={externalRef}
455+
ref={internalRef}
478456
// @ts-ignore
479457
disabled={disabled}
480458
maxLength={maxLength}
481-
onKeyDown={(e) => {
459+
onKeyDown={handleCommonBehavior((e) => {
482460
const preventEvent = onKeyDown(e);
483461
if (preventEvent) {
484462
e.preventDefault();
@@ -509,25 +487,24 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
509487
internalRef.current.removeChild(internalRef.current.childNodes[1]);
510488
}
511489
}
512-
}}
513-
onKeyUp={(e) => {
490+
})}
491+
onKeyUp={handleCommonBehavior((e) => {
514492
const preventEvent = onKeyUp(e);
515493
if (preventEvent) {
516494
e.preventDefault();
517495
} else {
518496
useMentionInputDetection();
519497
}
520-
}}
521-
onClick={() => {
498+
})}
499+
onClick={handleCommonBehavior(() => {
522500
useMentionInputDetection();
523-
}}
524-
onInput={() => {
525-
setHeight();
501+
})}
502+
onInput={handleCommonBehavior(() => {
526503
onStartTyping();
527504
setIsInput(internalRef?.current?.textContent ? internalRef.current.textContent.trim().length > 0 : false);
528505
useMentionedLabelDetection();
529-
}}
530-
onPaste={onPaste}
506+
})}
507+
onPaste={handleCommonBehavior(onPaste)}
531508
/>
532509
{/* placeholder */}
533510
{(internalRef?.current?.textContent?.length ?? 0) === 0 && (

0 commit comments

Comments
 (0)