fix: Onboarding page added aria-lable and keyboard navigation (#1562)

Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
This commit is contained in:
Harish Gautam
2023-11-10 17:49:06 +05:30
committed by GitHub
parent aa6d6df178
commit 11ede2e517
6 changed files with 99 additions and 7 deletions

View File

@@ -3,6 +3,7 @@
import { Button } from "@formbricks/ui/Button";
import type { Session } from "next-auth";
import Link from "next/link";
import { useEffect, useRef } from "react";
type Greeting = {
next: () => void;
@@ -13,6 +14,27 @@ type Greeting = {
const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
const legacyUser = !session ? false : new Date(session?.user?.createdAt) < new Date("2023-05-03T00:00:00"); // if user is created before onboarding deployment
const buttonRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
next();
}
};
const button = buttonRef.current;
if (button) {
button.focus();
button.addEventListener("keydown", handleKeyDown);
}
return () => {
if (button) {
button.removeEventListener("keydown", handleKeyDown);
}
};
}, []);
return (
<div className="flex h-full w-full max-w-xl flex-col justify-around gap-8 px-8">
@@ -30,7 +52,7 @@ const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
<Button size="lg" variant="minimal" onClick={skip}>
I&apos;ll do it later
</Button>
<Button size="lg" variant="darkCTA" onClick={next}>
<Button size="lg" variant="darkCTA" onClick={next} ref={buttonRef} tabIndex={0}>
Begin (1 min)
</Button>
</div>

View File

@@ -7,8 +7,9 @@ import { cn } from "@formbricks/lib/cn";
import { TProfileObjective } from "@formbricks/types/profile";
import { TProfile } from "@formbricks/types/profile";
import { Button } from "@formbricks/ui/Button";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { handleTabNavigation } from "../utils";
type ObjectiveProps = {
next: () => void;
@@ -35,6 +36,16 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
const [isProfileUpdating, setIsProfileUpdating] = useState(false);
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
useEffect(() => {
const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [fieldsetRef, setSelectedChoice]);
const handleNextClick = async () => {
if (selectedChoice) {
const selectedObjective = objectives.find((objective) => objective.label === selectedChoice);
@@ -71,14 +82,14 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
return (
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
<div className="px-4">
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
What do you want to achieve?
</label>
<label className="block text-sm font-normal leading-6 text-slate-500">
We have 85+ templates, help us select the best for your need.
</label>
<div className="mt-4">
<fieldset>
<fieldset id="choices" aria-label="What do you want to achieve?" ref={fieldsetRef}>
<legend className="sr-only">Choices</legend>
<div className=" relative space-y-2 rounded-md">
{objectives.map((choice) => (
@@ -101,6 +112,11 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleNextClick();
}
}}
/>
<span id={`${choice.id}-label`} className="ml-3 font-medium">
{choice.label}

View File

@@ -96,6 +96,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId, product })
placeholder="e.g. Formbricks"
value={name}
onChange={handleNameChange}
aria-label="Your product name"
/>
</div>
</div>

View File

@@ -5,8 +5,9 @@ import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
import { env } from "@/env.mjs";
import { cn } from "@formbricks/lib/cn";
import { Button } from "@formbricks/ui/Button";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { handleTabNavigation } from "../utils";
type RoleProps = {
next: () => void;
@@ -22,6 +23,15 @@ type RoleChoice = {
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
useEffect(() => {
const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [fieldsetRef, setSelectedChoice]);
const roles: Array<RoleChoice> = [
{ label: "Project Manager", id: "project_manager" },
@@ -63,19 +73,20 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
return (
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
<div className="px-4">
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
What is your role?
</label>
<label className="block text-sm font-normal leading-6 text-slate-500">
Make your Formbricks experience more personalised.
</label>
<div className="mt-4">
<fieldset>
<fieldset id="choices" aria-label="What is your role?" ref={fieldsetRef}>
<legend className="sr-only">Choices</legend>
<div className=" relative space-y-2 rounded-md">
{roles.map((choice) => (
<label
key={choice.id}
htmlFor={choice.id}
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-100"
@@ -87,12 +98,18 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
type="radio"
id={choice.id}
value={choice.label}
name="role"
checked={choice.label === selectedChoice}
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleNextClick();
}
}}
/>
<span id={`${choice.id}-label`} className="ml-3 font-medium">
{choice.label}

View File

@@ -0,0 +1,34 @@
// util.js
export const handleTabNavigation = (fieldsetRef, setSelectedChoice) => (event) => {
if (event.key !== "Tab") {
return;
}
event.preventDefault();
const radioButtons = fieldsetRef.current?.querySelectorAll('input[type="radio"]');
if (!radioButtons || radioButtons.length === 0) {
return;
}
const focusedRadioButton = fieldsetRef.current?.querySelector(
'input[type="radio"]:focus'
) as HTMLInputElement;
if (!focusedRadioButton) {
// If no radio button is focused, then it will focus on the first one by default
const firstRadioButton = radioButtons[0] as HTMLInputElement;
firstRadioButton.focus();
setSelectedChoice(firstRadioButton.value);
return;
}
const focusedIndex = Array.from(radioButtons).indexOf(focusedRadioButton);
const lastIndex = radioButtons.length - 1;
// Calculating the next index, considering wrapping from the last to the first element
const nextIndex = focusedIndex === lastIndex ? 0 : focusedIndex + 1;
const nextRadioButton = radioButtons[nextIndex] as HTMLInputElement;
nextRadioButton.focus();
setSelectedChoice(nextRadioButton.value);
};

View File

@@ -15,6 +15,8 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
className="ml-2 mr-2 h-10 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
color={color}
onChange={onChange}
id="color"
aria-label="Primary color"
/>
</div>
<PopoverPicker color={color} onChange={onChange} />