mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-08 16:21:09 -06:00
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:
@@ -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'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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
34
apps/web/app/(app)/onboarding/utils.ts
Normal file
34
apps/web/app/(app)/onboarding/utils.ts
Normal 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);
|
||||
};
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user