From a6a815c01455462fdcc19dc314649d3aed9b66cd Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:21:08 +0530 Subject: [PATCH] fix: autofocus and keyboard navigation issues (#3328) --- .../s/[surveyId]/components/LegalFooter.tsx | 7 +- .../src/components/general/EndingCard.tsx | 2 +- .../surveys/src/components/general/Input.tsx | 6 +- .../general/QuestionConditional.tsx | 2 + .../src/components/general/WelcomeCard.tsx | 3 +- .../components/questions/AddressQuestion.tsx | 35 +++++++--- .../src/components/questions/CTAQuestion.tsx | 64 ++++++++++--------- .../src/components/questions/CalQuestion.tsx | 20 +++--- .../components/questions/ConsentQuestion.tsx | 34 +++++++--- .../questions/ContactInfoQuestion.tsx | 35 ++++++---- .../src/components/questions/DateQuestion.tsx | 32 +++++----- .../questions/FileUploadQuestion.tsx | 14 ++-- .../components/questions/MatrixQuestion.tsx | 23 ++++--- .../questions/MultipleChoiceMultiQuestion.tsx | 23 ++++--- .../MultipleChoiceSingleQuestion.tsx | 24 ++++--- .../src/components/questions/NPSQuestion.tsx | 24 +++---- .../components/questions/OpenTextQuestion.tsx | 23 ++++--- .../questions/PictureSelectionQuestion.tsx | 23 ++++--- .../components/questions/RankingQuestion.tsx | 26 +++++--- .../components/questions/RatingQuestion.tsx | 28 ++++---- 20 files changed, 250 insertions(+), 198 deletions(-) diff --git a/apps/web/app/s/[surveyId]/components/LegalFooter.tsx b/apps/web/app/s/[surveyId]/components/LegalFooter.tsx index 425e3c22c8..9b99453b38 100644 --- a/apps/web/app/s/[surveyId]/components/LegalFooter.tsx +++ b/apps/web/app/s/[surveyId]/components/LegalFooter.tsx @@ -19,13 +19,13 @@ export const LegalFooter = ({
{IMPRINT_URL && ( - + Imprint )} {IMPRINT_URL && PRIVACY_URL && |} {PRIVACY_URL && ( - + Privacy Policy )} @@ -34,7 +34,8 @@ export const LegalFooter = ({ + className="hover:underline" + tabIndex={-1}> Report Survey )} diff --git a/packages/surveys/src/components/general/EndingCard.tsx b/packages/surveys/src/components/general/EndingCard.tsx index 094698f5bb..7ce964d090 100644 --- a/packages/surveys/src/components/general/EndingCard.tsx +++ b/packages/surveys/src/components/general/EndingCard.tsx @@ -128,7 +128,7 @@ export const EndingCard = ({ variablesData )} isLastQuestion={false} - focus={autoFocusEnabled} + focus={isCurrent ? autoFocusEnabled : false} onClick={handleSubmit} />
diff --git a/packages/surveys/src/components/general/Input.tsx b/packages/surveys/src/components/general/Input.tsx index 255d534336..2608a72aca 100644 --- a/packages/surveys/src/components/general/Input.tsx +++ b/packages/surveys/src/components/general/Input.tsx @@ -1,14 +1,16 @@ import { cn } from "@/lib/utils"; import { HTMLAttributes } from "preact/compat"; +import { forwardRef } from "preact/compat"; export interface InputProps extends HTMLAttributes { className?: string; } -export const Input = ({ className, ...props }: InputProps) => { +export const Input = forwardRef(({ className, ...props }, ref) => { return ( { dir="auto" /> ); -}; +}); diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/QuestionConditional.tsx index 0c1984ddfe..fb455e1ca0 100644 --- a/packages/surveys/src/components/general/QuestionConditional.tsx +++ b/packages/surveys/src/components/general/QuestionConditional.tsx @@ -282,6 +282,7 @@ export const QuestionConditional = ({ ttc={ttc} setTtc={setTtc} currentQuestionId={currentQuestionId} + autoFocusEnabled={autoFocusEnabled} /> ) : question.type === TSurveyQuestionTypeEnum.Ranking ? ( ) : null; }; diff --git a/packages/surveys/src/components/general/WelcomeCard.tsx b/packages/surveys/src/components/general/WelcomeCard.tsx index 9de9810f14..74b4847510 100644 --- a/packages/surveys/src/components/general/WelcomeCard.tsx +++ b/packages/surveys/src/components/general/WelcomeCard.tsx @@ -161,7 +161,8 @@ export const WelcomeCard = ({ e.key === "Enter" && e.preventDefault()} diff --git a/packages/surveys/src/components/questions/AddressQuestion.tsx b/packages/surveys/src/components/questions/AddressQuestion.tsx index 24b38caf0b..04390773e4 100644 --- a/packages/surveys/src/components/questions/AddressQuestion.tsx +++ b/packages/surveys/src/components/questions/AddressQuestion.tsx @@ -7,6 +7,7 @@ import { Subheader } from "@/components/general/Subheader"; import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useMemo, useRef, useState } from "preact/hooks"; +import { useCallback } from "react"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { TResponseData, TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyAddressQuestion } from "@formbricks/types/surveys/types"; @@ -23,6 +24,7 @@ interface AddressQuestionProps { ttc: TResponseTtc; setTtc: (ttc: TResponseTtc) => void; currentQuestionId: string; + autoFocusEnabled: boolean; } export const AddressQuestion = ({ @@ -37,15 +39,16 @@ export const AddressQuestion = ({ ttc, setTtc, currentQuestionId, + autoFocusEnabled, }: AddressQuestionProps) => { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; const formRef = useRef(null); useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); - const safeValue = useMemo(() => { return Array.isArray(value) ? value : ["", "", "", "", "", ""]; }, [value]); + const isCurrent = question.id === currentQuestionId; const fields = [ { @@ -103,6 +106,16 @@ export const AddressQuestion = ({ } }; + const addressRef = useCallback( + (currentElement: HTMLInputElement | null) => { + // will focus on current element when the question ID matches the current question + if (question.id && currentElement && autoFocusEnabled && question.id === currentQuestionId) { + currentElement.focus(); + } + }, + [question.id, autoFocusEnabled, currentQuestionId] + ); + return (
@@ -146,6 +159,8 @@ export const AddressQuestion = ({ className="fb-py-3" type={field.id === "email" ? "email" : "text"} onChange={(e) => handleChange(field.id, e?.currentTarget?.value ?? "")} + ref={index === 0 ? addressRef : null} + tabIndex={isCurrent ? 0 : -1} /> ) ); @@ -153,10 +168,17 @@ export const AddressQuestion = ({
-
+
+ {}} + /> +
{!isFirstQuestion && ( { const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -165,13 +187,6 @@ export const AddressQuestion = ({ }} /> )} -
- {}} - />
); diff --git a/packages/surveys/src/components/questions/CTAQuestion.tsx b/packages/surveys/src/components/questions/CTAQuestion.tsx index e957d36ee3..2450bc98b9 100644 --- a/packages/surveys/src/components/questions/CTAQuestion.tsx +++ b/packages/surveys/src/components/questions/CTAQuestion.tsx @@ -41,8 +41,8 @@ export const CTAQuestion = ({ }: CTAQuestionProps) => { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; - - useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); + const isCurrent = question.id === currentQuestionId; + useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent); return (
@@ -57,38 +57,13 @@ export const CTAQuestion = ({
-
- {!isFirstQuestion && ( - { - const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); - setTtc(updatedTtcObj); - onSubmit({ [question.id]: "" }, updatedTtcObj); - onBack(); - }} - /> - )} -
- {!question.required && ( - - )} +
+
{ if (question.buttonExternal && question.buttonUrl) { window?.open(question.buttonUrl, "_blank")?.focus(); @@ -100,7 +75,34 @@ export const CTAQuestion = ({ }} type="button" /> + {!question.required && ( + + )}
+ {!isFirstQuestion && ( + { + const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); + setTtc(updatedTtcObj); + onSubmit({ [question.id]: "" }, updatedTtcObj); + onBack(); + }} + /> + )}
); diff --git a/packages/surveys/src/components/questions/CalQuestion.tsx b/packages/surveys/src/components/questions/CalQuestion.tsx index 77d40c8376..53b319848d 100644 --- a/packages/surveys/src/components/questions/CalQuestion.tsx +++ b/packages/surveys/src/components/questions/CalQuestion.tsx @@ -44,7 +44,7 @@ export const CalQuestion = ({ const isMediaAvailable = question.imageUrl || question.videoUrl; const [errorMessage, setErrorMessage] = useState(""); useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); - + const isCurrent = question.id === currentQuestionId; const onSuccessfulBooking = useCallback(() => { onChange({ [question.id]: "booked" }); const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -87,20 +87,22 @@ export const CalQuestion = ({
-
+
+ {!question.required && ( + + )} +
{!isFirstQuestion && ( { onBack(); }} - /> - )} -
- {!question.required && ( - )}
diff --git a/packages/surveys/src/components/questions/ConsentQuestion.tsx b/packages/surveys/src/components/questions/ConsentQuestion.tsx index 544a4c7772..08116d82ba 100644 --- a/packages/surveys/src/components/questions/ConsentQuestion.tsx +++ b/packages/surveys/src/components/questions/ConsentQuestion.tsx @@ -5,7 +5,7 @@ import { HtmlBody } from "@/components/general/HtmlBody"; import { QuestionMedia } from "@/components/general/QuestionMedia"; import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; -import { useState } from "preact/hooks"; +import { useCallback, useState } from "preact/hooks"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { TResponseData, TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyConsentQuestion } from "@formbricks/types/surveys/types"; @@ -37,12 +37,24 @@ export const ConsentQuestion = ({ ttc, setTtc, currentQuestionId, + autoFocusEnabled, }: ConsentQuestionProps) => { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; + const isCurrent = question.id === currentQuestionId; useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); + const consentRef = useCallback( + (currentElement: HTMLLabelElement | null) => { + // will focus on current element when the question ID matches the current question + if (question.id && currentElement && autoFocusEnabled && question.id === currentQuestionId) { + currentElement.focus(); + } + }, + [question.id, autoFocusEnabled, currentQuestionId] + ); + return (
-
+
+ {!question.required && ( + + )} {!isFirstQuestion && ( { const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -153,14 +161,6 @@ export const NPSQuestion = ({ }} /> )} -
- {!question.required && ( - - )}
); diff --git a/packages/surveys/src/components/questions/OpenTextQuestion.tsx b/packages/surveys/src/components/questions/OpenTextQuestion.tsx index 9f729ea954..1c4b9b8609 100644 --- a/packages/surveys/src/components/questions/OpenTextQuestion.tsx +++ b/packages/surveys/src/components/questions/OpenTextQuestion.tsx @@ -44,7 +44,7 @@ export const OpenTextQuestion = ({ }: OpenTextQuestionProps) => { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; - + const isCurrent = question.id === currentQuestionId; useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); const handleInputChange = (inputValue: string) => { @@ -96,7 +96,7 @@ export const OpenTextQuestion = ({ {question.longAnswer === false ? ( handleInputChange(e.currentTarget.value)} - autoFocus={autoFocusEnabled} className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm" pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"} title={question.inputType === "phone" ? "Enter a valid phone number" : undefined} @@ -116,7 +115,7 @@ export const OpenTextQuestion = ({ ref={openTextRef} rows={3} name={question.id} - tabIndex={1} + tabIndex={isCurrent ? 0 : -1} aria-label="textarea" id={question.id} placeholder={getLocalizedValue(question.placeholder, languageCode)} @@ -128,7 +127,6 @@ export const OpenTextQuestion = ({ handleInputChange(e.currentTarget.value); handleInputResize(e); }} - autoFocus={autoFocusEnabled} className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0 sm:fb-text-sm" pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"} title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined} @@ -137,9 +135,16 @@ export const OpenTextQuestion = ({
-
+
+ {}} + /> {!isFirstQuestion && ( { const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -148,12 +153,6 @@ export const OpenTextQuestion = ({ }} /> )} -
- {}} - />
); diff --git a/packages/surveys/src/components/questions/PictureSelectionQuestion.tsx b/packages/surveys/src/components/questions/PictureSelectionQuestion.tsx index b7b83b0d98..98b6bc72f7 100644 --- a/packages/surveys/src/components/questions/PictureSelectionQuestion.tsx +++ b/packages/surveys/src/components/questions/PictureSelectionQuestion.tsx @@ -41,8 +41,8 @@ export const PictureSelectionQuestion = ({ }: PictureSelectionProps) => { const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; - - useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); + const isCurrent = question.id === currentQuestionId; + useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent); const addItem = (item: string) => { let values: string[] = []; @@ -111,10 +111,10 @@ export const PictureSelectionQuestion = ({
Options
- {questionChoices.map((choice, idx) => ( + {questionChoices.map((choice) => (
-
+
+ {!isFirstQuestion && ( { const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -208,12 +213,6 @@ export const PictureSelectionQuestion = ({ }} /> )} -
-
); diff --git a/packages/surveys/src/components/questions/RankingQuestion.tsx b/packages/surveys/src/components/questions/RankingQuestion.tsx index 634c4ac495..35f101e337 100644 --- a/packages/surveys/src/components/questions/RankingQuestion.tsx +++ b/packages/surveys/src/components/questions/RankingQuestion.tsx @@ -42,7 +42,7 @@ export const RankingQuestion = ({ currentQuestionId, }: RankingQuestionProps) => { const [startTime, setStartTime] = useState(performance.now()); - + const isCurrent = question.id === currentQuestionId; const shuffledChoicesIds = useMemo(() => { if (question.shuffleOption) { return getShuffledChoicesIds(question.choices, question.shuffleOption); @@ -148,7 +148,12 @@ export const RankingQuestion = ({ return (
{ + if (e.key === " ") { + handleItemClick(item); + } + }} className={cn( "fb-flex fb-h-12 fb-items-center fb-mb-2 fb-border fb-border-border fb-transition-all fb-text-heading focus-within:fb-border-brand hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-cursor-pointer focus:fb-outline-none fb-transform fb-duration-500 fb-ease-in-out", isSorted ? "fb-bg-input-bg-selected" : "fb-bg-input-bg" @@ -173,6 +178,7 @@ export const RankingQuestion = ({ {isSorted && (
-
+
+ {!isFirstQuestion && ( { const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); setTtc(updatedTtcObj); @@ -244,12 +256,6 @@ export const RankingQuestion = ({ }} /> )} -
-
); diff --git a/packages/surveys/src/components/questions/RatingQuestion.tsx b/packages/surveys/src/components/questions/RatingQuestion.tsx index 87d9481c7d..353b4ade53 100644 --- a/packages/surveys/src/components/questions/RatingQuestion.tsx +++ b/packages/surveys/src/components/questions/RatingQuestion.tsx @@ -54,7 +54,7 @@ export const RatingQuestion = ({ const [hoveredNumber, setHoveredNumber] = useState(0); const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; - + const isCurrent = question.id === currentQuestionId; useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); const handleSelect = (number: number) => { @@ -138,7 +138,7 @@ export const RatingQuestion = ({ className="fb-bg-survey-bg fb-flex-1 fb-text-center fb-text-sm"> {question.scale === "number" ? ( ) : question.scale === "star" ? ( ) : (
-
+
+ {!question.required && ( + + )} +
{!isFirstQuestion && ( { const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); @@ -252,14 +260,6 @@ export const RatingQuestion = ({ }} /> )} -
- {!question.required && ( - - )}
);