diff --git a/packages/ui/InputCombobox/index.tsx b/packages/ui/InputCombobox/index.tsx index b7ac0fd9d6..fc28849e2b 100644 --- a/packages/ui/InputCombobox/index.tsx +++ b/packages/ui/InputCombobox/index.tsx @@ -1,6 +1,7 @@ +import debounce from "lodash/debounce"; import { CheckIcon, ChevronDownIcon, LucideProps, XIcon } from "lucide-react"; import Image from "next/image"; -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { ForwardRefExoticComponent, RefAttributes } from "react"; import { cn } from "@formbricks/lib/cn"; import { @@ -62,13 +63,23 @@ export const InputCombobox = ({ comboboxClasses, emptyDropdownText = "No option found.", }: InputComboboxProps) => { - const [open, setOpen] = React.useState(false); - const [localValue, setLocalValue] = React.useState< - TComboboxOption | TComboboxOption[] | string | number | null - >(null); - const [inputType, setInputType] = React.useState<"dropdown" | "input" | null>(null); + const [open, setOpen] = useState(false); + const [localValue, setLocalValue] = useState( + null + ); + const [inputType, setInputType] = useState<"dropdown" | "input" | null>(null); + const [inputValue, setInputValue] = useState(value || ""); - showCheckIcon = allowMultiSelect ? true : showCheckIcon; + // Debounced function to call onChangeValue + const debouncedOnChangeValue = useMemo( + () => debounce((val) => onChangeValue(val, undefined, true), 300), + [onChangeValue] + ); + + useEffect(() => { + // Sync inputValue when value changes externally + setInputValue(value || ""); + }, [value]); useEffect(() => { const validOptions = options?.length ? options : groupedOptions?.flatMap((group) => group.options); @@ -136,22 +147,13 @@ export const InputCombobox = ({ }; const onInputChange = (e: React.ChangeEvent) => { - const inputType = e.target.type; const value = e.target.value; - if (value === "") { - setLocalValue(null); - onChangeValue(""); - } + // Set the local input value immediately + setInputValue(value); - if (inputType !== "input") { - setInputType("input"); - } - - const val = inputType === "number" ? Number(value) : value; - - setLocalValue(val); - onChangeValue(val, undefined, true); + // Trigger the debounced onChangeValue + debouncedOnChangeValue(value); }; const getDisplayValue = useMemo(() => { @@ -196,7 +198,7 @@ export const InputCombobox = ({ className="min-w-0 rounded-none border-0 border-r border-slate-300 bg-white focus:border-slate-300" {...inputProps} id={`${id}-input`} - value={localValue as string | number} + value={inputValue as string | number} onChange={onInputChange} /> )} @@ -224,9 +226,7 @@ export const InputCombobox = ({ {showSearch && ( @@ -237,7 +237,7 @@ export const InputCombobox = ({ )} {emptyDropdownText} - {options && options.length > 0 ? ( + {options && options.length > 0 && ( {options.map((option) => ( {showCheckIcon && - ((allowMultiSelect && - Array.isArray(localValue) && - localValue.find((item) => item.value === option.value)) || - (!allowMultiSelect && - typeof localValue === "object" && - !Array.isArray(localValue) && - localValue?.value === option.value)) && ( + allowMultiSelect && + Array.isArray(localValue) && + localValue.find((item) => item.value === option.value) && ( )} {option.icon && } @@ -269,8 +265,7 @@ export const InputCombobox = ({ ))} - ) : null} - + )} {groupedOptions?.map((group, idx) => ( <> {idx !== 0 && } @@ -281,14 +276,10 @@ export const InputCombobox = ({ onSelect={() => handleSelect(option)} className="cursor-pointer truncate hover:text-slate-500"> {showCheckIcon && - ((allowMultiSelect && - Array.isArray(localValue) && - localValue.find((item) => item.value === option.value)) || - (!allowMultiSelect && - typeof localValue === "object" && - !Array.isArray(localValue) && - localValue?.value === option.value)) && ( - + allowMultiSelect && + Array.isArray(localValue) && + localValue.find((item) => item.value === option.value) && ( + )} {option.icon && } {option.imgSrc && ( diff --git a/packages/ui/QuestionFormInput/index.tsx b/packages/ui/QuestionFormInput/index.tsx index 04031be44d..46939fc556 100644 --- a/packages/ui/QuestionFormInput/index.tsx +++ b/packages/ui/QuestionFormInput/index.tsx @@ -1,7 +1,7 @@ "use client"; import { ImagePlusIcon, PencilIcon, TrashIcon } from "lucide-react"; -import { RefObject, useEffect, useMemo, useRef, useState } from "react"; +import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { extractLanguageCodes, getEnabledLanguages, getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; @@ -117,7 +117,7 @@ export const QuestionFormInput = ({ [value, id, isInvalid, surveyLanguageCodes] ); - const getElementTextBasedOnType = (): TI18nString => { + const getElementTextBasedOnType = useCallback((): TI18nString => { if (isChoice && typeof index === "number") { return getChoiceLabel(question, index, surveyLanguageCodes); } @@ -138,9 +138,22 @@ export const QuestionFormInput = ({ (question && (question[id as keyof TSurveyQuestion] as TI18nString)) || createI18nString("", surveyLanguageCodes) ); - }; + }, [ + id, + index, + isChoice, + isEndingCard, + isMatrixLabelColumn, + isMatrixLabelRow, + isWelcomeCard, + localSurvey, + question, + questionIdx, + surveyLanguageCodes, + ]); const [text, setText] = useState(getElementTextBasedOnType()); + // const [debouncedText, setDebouncedText] = useState(text); // Added debouncedText state const [renderedText, setRenderedText] = useState(); const [showImageUploader, setShowImageUploader] = useState( determineImageUploaderVisibility(questionIdx, localSurvey) @@ -356,50 +369,79 @@ export const QuestionFormInput = ({ // choice -> updateChoice // matrixLabel -> updateMatrixLabel - const handleUpdate = (updatedText: string) => { - const translatedText = createUpdatedText(updatedText); + const createUpdatedText = useCallback( + (updatedText: string): TI18nString => { + return { + ...getElementTextBasedOnType(), + [usedLanguageCode]: updatedText, + }; + }, + [getElementTextBasedOnType, usedLanguageCode] + ); - if (isChoice) { - updateChoiceDetails(translatedText); - } else if (isEndingCard || isWelcomeCard) { - updateSurveyDetails(translatedText); - } else if (isMatrixLabelRow || isMatrixLabelColumn) { - updateMatrixLabelDetails(translatedText); - } else { - updateQuestionDetails(translatedText); - } - }; + const updateChoiceDetails = useCallback( + (translatedText: TI18nString) => { + if (updateChoice && typeof index === "number") { + updateChoice(index, { label: translatedText }); + } + }, + [index, updateChoice] + ); - const createUpdatedText = (updatedText: string): TI18nString => { - return { - ...getElementTextBasedOnType(), - [usedLanguageCode]: updatedText, - }; - }; + const updateSurveyDetails = useCallback( + (translatedText: TI18nString) => { + if (updateSurvey) { + updateSurvey({ [id]: translatedText }); + } + }, + [id, updateSurvey] + ); - const updateChoiceDetails = (translatedText: TI18nString) => { - if (updateChoice && typeof index === "number") { - updateChoice(index, { label: translatedText }); - } - }; + const updateMatrixLabelDetails = useCallback( + (translatedText: TI18nString) => { + if (updateMatrixLabel && typeof index === "number") { + updateMatrixLabel(index, isMatrixLabelRow ? "row" : "column", translatedText); + } + }, + [index, isMatrixLabelRow, updateMatrixLabel] + ); - const updateSurveyDetails = (translatedText: TI18nString) => { - if (updateSurvey) { - updateSurvey({ [id]: translatedText }); - } - }; + const updateQuestionDetails = useCallback( + (translatedText: TI18nString) => { + if (updateQuestion) { + updateQuestion(questionIdx, { [id]: translatedText }); + } + }, + [id, questionIdx, updateQuestion] + ); - const updateMatrixLabelDetails = (translatedText: TI18nString) => { - if (updateMatrixLabel && typeof index === "number") { - updateMatrixLabel(index, isMatrixLabelRow ? "row" : "column", translatedText); - } - }; + const handleUpdate = useCallback( + (updatedText: string) => { + const translatedText = createUpdatedText(updatedText); - const updateQuestionDetails = (translatedText: TI18nString) => { - if (updateQuestion) { - updateQuestion(questionIdx, { [id]: translatedText }); - } - }; + if (isChoice) { + updateChoiceDetails(translatedText); + } else if (isEndingCard || isWelcomeCard) { + updateSurveyDetails(translatedText); + } else if (isMatrixLabelRow || isMatrixLabelColumn) { + updateMatrixLabelDetails(translatedText); + } else { + updateQuestionDetails(translatedText); + } + }, + [ + createUpdatedText, + isChoice, + isEndingCard, + isMatrixLabelColumn, + isMatrixLabelRow, + isWelcomeCard, + updateChoiceDetails, + updateMatrixLabelDetails, + updateQuestionDetails, + updateSurveyDetails, + ] + ); const getFileUrl = (): string | undefined => { if (isWelcomeCard) return localSurvey.welcomeCard.fileUrl; @@ -417,6 +459,18 @@ export const QuestionFormInput = ({ } else return question.videoUrl; }; + const handleInputChange = (e: React.ChangeEvent) => { + const updatedText = { + ...getElementTextBasedOnType(), + [usedLanguageCode]: e.target.value, + }; + setText(recallToHeadline(updatedText, localSurvey, false, usedLanguageCode, attributeClasses)); + + setTimeout(() => { + handleUpdate(headlineToRecall(e.target.value, recallItems, fallbacks)); + }, 100); + }; + return (
@@ -454,7 +508,9 @@ export const QuestionFormInput = ({
1 ? "pr-24" : ""}`} + className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${ + localSurvey.languages?.length > 1 ? "pr-24" : "" + }`} dir="auto"> {renderedText}
@@ -472,7 +528,9 @@ export const QuestionFormInput = ({ 1 ? "pr-24" : ""} ${className}`} + className={`absolute top-0 text-black caret-black ${ + localSurvey.languages?.length > 1 ? "pr-24" : "" + } ${className}`} placeholder={placeholder ? placeholder : getPlaceHolderById(id)} id={id} name={id} @@ -483,18 +541,9 @@ export const QuestionFormInput = ({ usedLanguageCode ] } + onChange={handleInputChange} ref={inputRef} onBlur={onBlur} - onChange={(e) => { - let translatedText = { - ...getElementTextBasedOnType(), - [usedLanguageCode]: e.target.value, - }; - setText( - recallToHeadline(translatedText, localSurvey, false, usedLanguageCode, attributeClasses) - ); - handleUpdate(headlineToRecall(e.target.value, recallItems, fallbacks)); - }} maxLength={maxLength ?? undefined} isInvalid={ isInvalid &&