fix: keyboard nav for MQP with multiple questions (#6926)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Johannes
2025-12-01 22:40:30 -08:00
committed by GitHub
parent 04dfbe0777
commit ccf501d925
13 changed files with 32 additions and 36 deletions

View File

@@ -43,8 +43,6 @@ export function AddressElement({
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
}, [value]);
const isCurrent = element.id === currentElementId;
const fields = useMemo(
() => [
{
@@ -166,7 +164,7 @@ export function AddressElement({
handleChange(field.id, e.currentTarget.value);
}}
ref={index === 0 ? addressRef : null}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
aria-label={field.label}
dir={!safeValue[index] ? dir : "auto"}
/>

View File

@@ -32,7 +32,6 @@ export function ConsentElement({
}: Readonly<ConsentElementProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = element.imageUrl || element.videoUrl;
const isCurrent = element.id === currentElementId;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
@@ -66,7 +65,7 @@ export function ConsentElement({
/>
<label
ref={consentRef}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
id={`${element.id}-label`}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input

View File

@@ -37,7 +37,6 @@ export function ContactInfoElement({
const isMediaAvailable = element.imageUrl || element.videoUrl;
const formRef = useRef<HTMLFormElement>(null);
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const isCurrent = element.id === currentElementId;
const safeValue = useMemo(() => {
return Array.isArray(value) ? value : ["", "", "", "", ""];
}, [value]);
@@ -149,7 +148,7 @@ export function ContactInfoElement({
onChange={(e) => {
handleChange(field.id, e.currentTarget.value);
}}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
aria-label={field.label}
dir={!safeValue[index] ? dir : "auto"}
/>

View File

@@ -67,7 +67,7 @@ export function CTAElement({
<button
dir="auto"
type="button"
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
onClick={handleExternalButtonClick}
className="fb-text-heading focus:fb-ring-focus fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
{getLocalizedValue(element.ctaButtonLabel, languageCode)}

View File

@@ -86,7 +86,6 @@ export function DateElement({
const [errorMessage, setErrorMessage] = useState("");
const isMediaAvailable = element.imageUrl || element.videoUrl;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const isCurrent = element.id === currentElementId;
const [datePickerOpen, setDatePickerOpen] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
@@ -161,7 +160,7 @@ export function DateElement({
onClick={() => {
setDatePickerOpen(true);
}}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
type="button"
onKeyDown={(e) => {
if (e.key === " ") setDatePickerOpen(true);

View File

@@ -31,7 +31,6 @@ export function MatrixElement({
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = element.imageUrl || element.videoUrl;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const isCurrent = element.id === currentElementId;
const rowShuffleIdx = useMemo(() => {
if (element.shuffleOption !== "none") {
return getShuffledRowIndices(element.rows.length, element.shuffleOption);
@@ -127,7 +126,7 @@ export function MatrixElement({
{element.columns.map((column, columnIndex) => (
<td
key={`column-${columnIndex.toString()}`}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === element.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
onClick={() => {
handleSelect(

View File

@@ -57,7 +57,6 @@ export function MultipleChoiceMultiElement({
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = element.imageUrl || element.videoUrl;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const isCurrent = element.id === currentElementId;
const shuffledChoicesIds = useMemo(() => {
if (element.shuffleOption) {
return getShuffledChoicesIds(element.choices, element.shuffleOption);
@@ -212,9 +211,9 @@ export function MultipleChoiceMultiElement({
return (
<label
key={choice.id}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleKeyDown(choice.id)}
onKeyDown={handleKeyDown(choice.id)} // NOSONAR - needed for keyboard navigation through options
autoFocus={idx === 0 && autoFocusEnabled}>
<span className="fb-flex fb-items-center fb-text-sm">
<input
@@ -261,14 +260,15 @@ export function MultipleChoiceMultiElement({
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleKeyDown(otherOption.id)}>
onKeyDown={handleKeyDown(otherOption.id)} // NOSONAR - needed for keyboard navigation through options
>
<span className="fb-flex fb-items-center fb-text-sm">
<input
type="checkbox"
dir={dir}
tabIndex={isCurrent ? 0 : -1}
tabIndex={-1}
id={otherOption.id}
name={element.id}
value={otherLabel}
@@ -289,7 +289,7 @@ export function MultipleChoiceMultiElement({
id={`${otherOption.id}-specify`}
maxLength={250}
name={element.id}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
value={otherValue}
pattern=".*\S+.*"
onChange={(e) => setOtherValue(e.currentTarget.value)}
@@ -314,9 +314,10 @@ export function MultipleChoiceMultiElement({
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleKeyDown(noneOption.id)}>
onKeyDown={handleKeyDown(noneOption.id)} // NOSONAR - needed for keyboard navigation through options
>
<span className="fb-flex fb-items-center fb-text-sm">
<input
type="checkbox"

View File

@@ -36,7 +36,6 @@ export function MultipleChoiceSingleElement({
const otherSpecify = useRef<HTMLInputElement | null>(null);
const choicesContainerRef = useRef<HTMLDivElement | null>(null);
const isMediaAvailable = element.imageUrl || element.videoUrl;
const isCurrent = element.id === currentElementId;
const shuffledChoicesIds = useMemo(() => {
if (element.shuffleOption) {
return getShuffledChoicesIds(element.choices, element.shuffleOption);
@@ -158,9 +157,9 @@ export function MultipleChoiceSingleElement({
return (
<label
key={choice.id}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleKeyDown(choice.id)}
onKeyDown={handleKeyDown(choice.id)} // NOSONAR - needed for keyboard navigation through options
autoFocus={idx === 0 && autoFocusEnabled}>
<span className="fb-flex fb-items-center fb-text-sm">
<input
@@ -197,7 +196,11 @@ export function MultipleChoiceSingleElement({
: "Please specify";
return (
<label tabIndex={isCurrent ? 0 : -1} className={labelClassName} onKeyDown={handleOtherKeyDown}>
<label
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleOtherKeyDown} // NOSONAR - needed for keyboard navigation through options
>
<span className="fb-flex fb-items-center fb-text-sm">
<input
tabIndex={-1}
@@ -246,7 +249,7 @@ export function MultipleChoiceSingleElement({
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={labelClassName}
onKeyDown={handleKeyDown(noneOption.id)}>
<span className="fb-flex fb-items-center fb-text-sm">

View File

@@ -33,7 +33,6 @@ export function NPSElement({
const [startTime, setStartTime] = useState(performance.now());
const [hoveredNumber, setHoveredNumber] = useState(-1);
const isMediaAvailable = element.imageUrl || element.videoUrl;
const isCurrent = element.id === currentElementId;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const handleClick = (number: number) => {
@@ -74,7 +73,7 @@ export function NPSElement({
return (
<label
key={number}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
onMouseOver={() => {
setHoveredNumber(number);
}}

View File

@@ -169,7 +169,7 @@ export function OpenTextElement({
<input
ref={inputRef as RefObject<HTMLInputElement>}
autoFocus={isCurrent ? autoFocusEnabled : undefined}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
name={element.id}
id={element.id}
placeholder={getLocalizedValue(element.placeholder, languageCode)}
@@ -195,7 +195,7 @@ export function OpenTextElement({
rows={3}
autoFocus={isCurrent ? autoFocusEnabled : undefined}
name={element.id}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
aria-label="textarea"
id={element.id}
placeholder={getLocalizedValue(element.placeholder, languageCode, true)}

View File

@@ -148,7 +148,7 @@ export function PictureSelectionElement({
<div className="fb-relative" key={choice.id}>
<button
type="button"
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
onKeyDown={handleKeyDown}
onClick={() => handleChange(choice.id)}
className={getButtonClassName(choice.id)}>

View File

@@ -159,7 +159,7 @@ export function RankingElement({
)}>
<button
autoFocus={idx === 0 && autoFocusEnabled}
tabIndex={isCurrent ? 0 : -1}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === " ") {
e.preventDefault();

View File

@@ -46,7 +46,6 @@ export function RatingElement({
const [hoveredNumber, setHoveredNumber] = useState(0);
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = element.imageUrl || element.videoUrl;
const isCurrent = element.id === currentElementId;
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
const handleSelect = (number: number) => {
@@ -163,7 +162,7 @@ export function RatingElement({
const renderNumberScale = (number: number, totalLength: number) => {
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
onKeyDown={handleKeyDown(number)}
className={getNumberLabelClassName(number, totalLength)}>
{element.isColorCodingEnabled && (
@@ -180,7 +179,7 @@ export function RatingElement({
const renderStarScale = (number: number) => {
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
onKeyDown={handleKeyDown(number)}
className={getStarLabelClassName(number)}
onFocus={handleFocus(number)}
@@ -201,7 +200,7 @@ export function RatingElement({
const renderSmileyScale = (number: number, idx: number) => {
return (
<label
tabIndex={isCurrent ? 0 : -1}
tabIndex={0} // NOSONAR - needed for keyboard navigation through options
className={getSmileyLabelClassName(number)}
onKeyDown={handleKeyDown(number)}
onFocus={handleFocus(number)}