Skip to content

Commit

Permalink
📋 fix: Ensure Textarea Resizes in Clipboard Edge Case (danny-avila#2268)
Browse files Browse the repository at this point in the history
* 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`
  • Loading branch information
danny-avila authored Apr 1, 2024
1 parent d07396d commit aff219c
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 55 deletions.
8 changes: 5 additions & 3 deletions client/src/components/Chat/Input/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,9 +43,9 @@ const ChatForm = ({ index = 0 }) => {
setFiles,
conversation,
isSubmitting,
handleStopGenerating,
filesLoading,
setFilesLoading,
handleStopGenerating,
} = useChatContext();

const assistantMap = useAssistantsMapContext();
Expand All @@ -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],
);
Expand Down
53 changes: 1 addition & 52 deletions client/src/hooks/Input/useTextarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions client/src/utils/convos.fakeData.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
14 changes: 14 additions & 0 deletions client/src/utils/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
1 change: 1 addition & 0 deletions client/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
40 changes: 40 additions & 0 deletions client/src/utils/textarea.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
};

0 comments on commit aff219c

Please sign in to comment.