📋 fix: Ensure Textarea Resizes in Clipboard Edge Case (#2268)

* chore: ts-ignore fake conversation data used for testing

* chore(useTextarea): import helper functions to declutter hook

* fix(Textarea): reset textarea value explicitly by resetting `textAreaRef.current.value`
This commit is contained in:
Danny Avila 2024-04-01 13:40:21 -04:00 committed by GitHub
parent d07396d308
commit aff219c655
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 63 additions and 55 deletions

View file

@ -3,9 +3,9 @@ import { useForm } from 'react-hook-form';
import { memo, useCallback, useRef, useMemo } from 'react';
import {
supportsFiles,
EModelEndpoint,
mergeFileConfig,
fileConfig as defaultFileConfig,
EModelEndpoint,
} from 'librechat-data-provider';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useRequiresKey, useTextarea } from '~/hooks';
@ -43,9 +43,9 @@ const ChatForm = ({ index = 0 }) => {
setFiles,
conversation,
isSubmitting,
handleStopGenerating,
filesLoading,
setFilesLoading,
handleStopGenerating,
} = useChatContext();
const assistantMap = useAssistantsMapContext();
@ -57,7 +57,9 @@ const ChatForm = ({ index = 0 }) => {
}
ask({ text: data.text });
methods.reset();
textAreaRef.current?.setRangeText('', 0, data.text.length, 'end');
if (textAreaRef.current) {
textAreaRef.current.value = '';
}
},
[ask, methods],
);

View file

@ -4,6 +4,7 @@ import { EModelEndpoint } from 'librechat-data-provider';
import type { TEndpointOption } from 'librechat-data-provider';
import type { UseFormSetValue } from 'react-hook-form';
import type { KeyboardEvent } from 'react';
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
import useGetSender from '~/hooks/Conversations/useGetSender';
import useFileHandling from '~/hooks/Files/useFileHandling';
@ -12,58 +13,6 @@ import useLocalize from '~/hooks/useLocalize';
type KeyEvent = KeyboardEvent<HTMLTextAreaElement>;
function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) {
element.focus();
// Use the browser's built-in undoable actions if possible
if (window.getSelection() && document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, textToInsert);
} else {
console.warn('insertTextAtCursor: document.execCommand is not supported');
const startPos = element.selectionStart;
const endPos = element.selectionEnd;
const beforeText = element.value.substring(0, startPos);
const afterText = element.value.substring(endPos);
element.value = beforeText + textToInsert + afterText;
element.selectionStart = element.selectionEnd = startPos + textToInsert.length;
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
}
/**
* Necessary resize helper for edge cases where paste doesn't update the container height.
*
1) Resetting the height to 'auto' forces the component to recalculate height based on its current content
2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page,
ensuring that the reset height takes effect before resetting back to the scrollHeight.
This step is necessary because changes to the DOM do not instantly cause reflows.
3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically
*/
const forceResize = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.offsetHeight;
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
}
};
const getAssistantName = ({
name,
localize,
}: {
name?: string;
localize: (phraseKey: string, ...values: string[]) => string;
}) => {
if (name && name.length > 0) {
return name;
} else {
return localize('com_ui_assistant');
}
};
export default function useTextarea({
textAreaRef,
submitButtonRef,

View file

@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { EModelEndpoint, ImageDetail } from 'librechat-data-provider';
import type { ConversationData } from 'librechat-data-provider';

View file

@ -1,6 +1,20 @@
import { defaultEndpoints } from 'librechat-data-provider';
import type { EModelEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider';
export const getAssistantName = ({
name,
localize,
}: {
name?: string;
localize: (phraseKey: string, ...values: string[]) => string;
}) => {
if (name && name.length > 0) {
return name;
} else {
return localize('com_ui_assistant');
}
};
export const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => {
const filter: Record<string, boolean> = {};
if (!endpointsConfig) {

View file

@ -4,6 +4,7 @@ export * from './files';
export * from './latex';
export * from './convos';
export * from './presets';
export * from './textarea';
export * from './languages';
export * from './endpoints';
export { default as cn } from './cn';

View file

@ -0,0 +1,40 @@
/**
* Insert text at the cursor position in a textarea.
*/
export function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) {
element.focus();
// Use the browser's built-in undoable actions if possible
if (window.getSelection() && document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, textToInsert);
} else {
console.warn('insertTextAtCursor: document.execCommand is not supported');
const startPos = element.selectionStart;
const endPos = element.selectionEnd;
const beforeText = element.value.substring(0, startPos);
const afterText = element.value.substring(endPos);
element.value = beforeText + textToInsert + afterText;
element.selectionStart = element.selectionEnd = startPos + textToInsert.length;
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
}
/**
* Necessary resize helper for edge cases where paste doesn't update the container height.
*
1) Resetting the height to 'auto' forces the component to recalculate height based on its current content
2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page,
ensuring that the reset height takes effect before resetting back to the scrollHeight.
This step is necessary because changes to the DOM do not instantly cause reflows.
3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically
*/
export const forceResize = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.offsetHeight;
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
}
};