mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 13:49:54 -06:00
keyboard usablity
This commit is contained in:
@@ -3,11 +3,13 @@ import { cn } from "../../../lib/cn";
|
||||
interface BackButtonProps {
|
||||
onClick: () => void;
|
||||
backButtonLabel?: string;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export function BackButton({ onClick, backButtonLabel }: BackButtonProps) {
|
||||
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) {
|
||||
return (
|
||||
<button
|
||||
tabIndex={tabIndex}
|
||||
type={"button"}
|
||||
className={cn(
|
||||
"flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
|
||||
|
||||
@@ -33,9 +33,10 @@ export default function CTAQuestion({
|
||||
{!isFirstQuestion && (
|
||||
<BackButton backButtonLabel={question.backButtonLabel} onClick={() => onBack()} />
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<div className="flex w-full justify-end">
|
||||
{!question.required && (
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSubmit({ [question.id]: "dismissed" });
|
||||
@@ -48,6 +49,7 @@ export default function CTAQuestion({
|
||||
question={question}
|
||||
isLastQuestion={isLastQuestion}
|
||||
brandColor={brandColor}
|
||||
focus={true}
|
||||
onClick={() => {
|
||||
if (question.buttonExternal && question.buttonUrl) {
|
||||
window?.open(question.buttonUrl, "_blank")?.focus();
|
||||
|
||||
@@ -3,6 +3,7 @@ export default function FormbricksSignature() {
|
||||
<a
|
||||
href="https://formbricks.com?utm_source=survey_branding"
|
||||
target="_blank"
|
||||
tabIndex={-1}
|
||||
className="mb-5 mt-2 flex justify-center">
|
||||
<p className="text-xs text-slate-400">
|
||||
Powered by{" "}
|
||||
|
||||
@@ -86,12 +86,18 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md bg-white py-0.5 pr-2">
|
||||
{questionChoices.map((choice) => (
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
addItem(choice.label);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === choice.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 hover:bg-slate-50 focus:outline-none"
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 hover:bg-slate-50 focus:bg-slate-50 focus:outline-none focus:[&>input]:ring-0 focus:[&>input]:ring-offset-0"
|
||||
)}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
@@ -110,9 +116,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
}}
|
||||
checked={Array.isArray(value) && value.includes(choice.label)}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
required={
|
||||
question.required && Array.isArray(value) && value.length ? false : question.required
|
||||
}
|
||||
required={question.required && idx === 0}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
@@ -173,9 +177,16 @@ export default function MultipleChoiceSingleQuestion({
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{!isFirstQuestion && <BackButton backButtonLabel={question.backButtonLabel} onClick={onBack} />}
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={questionChoices.length + 3}
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={onBack}
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
<SubmitButton
|
||||
tabIndex={questionChoices.length + 2}
|
||||
question={question}
|
||||
isLastQuestion={isLastQuestion}
|
||||
brandColor={brandColor}
|
||||
|
||||
@@ -55,7 +55,6 @@ export default function MultipleChoiceSingleQuestion({
|
||||
otherSpecify.current?.focus();
|
||||
}
|
||||
}, [otherSelected]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -68,13 +67,19 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md bg-white py-0.5 pr-2">
|
||||
<div
|
||||
className="relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md bg-white py-0.5 pr-2"
|
||||
role="radiogroup">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") onChange({ [question.id]: choice.label });
|
||||
}}
|
||||
className={cn(
|
||||
value === choice.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 hover:bg-slate-50 focus:outline-none"
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 focus-within:border-slate-400 focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none focus:[&>input]:ring-0 focus:[&>input]:ring-offset-0"
|
||||
)}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
@@ -108,6 +113,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<input
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
tabIndex={questionChoices.length + 1}
|
||||
name={question.id}
|
||||
value={otherOption.label}
|
||||
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
|
||||
@@ -144,9 +150,16 @@ export default function MultipleChoiceSingleQuestion({
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{!isFirstQuestion && <BackButton backButtonLabel={question.backButtonLabel} onClick={onBack} />}
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
tabIndex={questionChoices.length + 3}
|
||||
onClick={onBack}
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
<SubmitButton
|
||||
tabIndex={questionChoices.length + 2}
|
||||
question={question}
|
||||
isLastQuestion={isLastQuestion}
|
||||
brandColor={brandColor}
|
||||
|
||||
@@ -39,12 +39,18 @@ export default function NPSQuestion({
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number) => (
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={idx + 1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
onSubmit({ [question.id]: number });
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === number ? "z-10 border-slate-400 bg-slate-50" : "",
|
||||
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none"
|
||||
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
|
||||
)}>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -76,6 +82,7 @@ export default function NPSQuestion({
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={question.required ? 12 : 13}
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
@@ -85,6 +92,7 @@ export default function NPSQuestion({
|
||||
<div></div>
|
||||
{!question.required && (
|
||||
<SubmitButton
|
||||
tabIndex={12}
|
||||
question={question}
|
||||
isLastQuestion={isLastQuestion}
|
||||
brandColor={brandColor}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BackButton } from "./BackButton";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
import { useCallback } from "react";
|
||||
|
||||
interface OpenTextQuestionProps {
|
||||
question: TSurveyOpenTextQuestion;
|
||||
@@ -28,6 +29,12 @@ export default function OpenTextQuestion({
|
||||
brandColor,
|
||||
autoFocus = true,
|
||||
}: OpenTextQuestionProps) {
|
||||
const openTextRef = useCallback((currentElement: HTMLInputElement | HTMLTextAreaElement | null) => {
|
||||
if (currentElement && autoFocus) {
|
||||
currentElement.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -40,6 +47,8 @@ export default function OpenTextQuestion({
|
||||
<div className="mt-4">
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={openTextRef}
|
||||
tabIndex={1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={question.placeholder}
|
||||
@@ -48,13 +57,17 @@ export default function OpenTextQuestion({
|
||||
onInput={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
autoFocus={autoFocus}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") onSubmit({ [question.id]: value });
|
||||
}}
|
||||
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm"
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
ref={openTextRef}
|
||||
rows={3}
|
||||
name={question.id}
|
||||
tabIndex={1}
|
||||
id={question.id}
|
||||
placeholder={question.placeholder}
|
||||
required={question.required}
|
||||
@@ -62,7 +75,9 @@ export default function OpenTextQuestion({
|
||||
onInput={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
autoFocus={autoFocus}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") onSubmit({ [question.id]: value });
|
||||
}}
|
||||
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 sm:text-sm"></textarea>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -81,9 +81,10 @@ export default function RatingQuestion({
|
||||
key={number}
|
||||
onMouseOver={() => setHoveredNumber(number)}
|
||||
onMouseLeave={() => setHoveredNumber(0)}
|
||||
className="max-w-10 relative flex max-h-10 flex-1 cursor-pointer justify-center bg-white text-center text-sm leading-10">
|
||||
className="max-w-10 relative max-h-10 flex-1 cursor-pointer bg-white text-center text-sm leading-10">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
tabIndex={value ? -1 : i + 1}
|
||||
className={cn(
|
||||
value === number ? "z-10 border-slate-400 bg-slate-50" : "",
|
||||
a.length === number ? "rounded-r-md" : "",
|
||||
@@ -95,6 +96,7 @@ export default function RatingQuestion({
|
||||
</label>
|
||||
) : question.scale === "star" ? (
|
||||
<label
|
||||
tabIndex={value ? -1 : i + 1}
|
||||
className={cn(
|
||||
number <= hoveredNumber ? "text-yellow-500" : "",
|
||||
"flex h-full w-full justify-center"
|
||||
@@ -132,10 +134,8 @@ export default function RatingQuestion({
|
||||
</label>
|
||||
) : (
|
||||
<label
|
||||
className={cn(
|
||||
"flex items-center justify-center text-slate-800",
|
||||
question.range === 10 ? "h-6 w-6" : "h-full w-full"
|
||||
)}>
|
||||
tabIndex={value ? -1 : i + 1}
|
||||
className="flex h-full w-full justify-center text-slate-800">
|
||||
<HiddenRadioInput number={number} />
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from "preact/hooks";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { isLight } from "../lib/utils";
|
||||
import { TSurveyQuestion } from "../../../types/v1/surveys";
|
||||
@@ -7,13 +8,32 @@ interface SubmitButtonProps {
|
||||
isLastQuestion: boolean;
|
||||
brandColor: string;
|
||||
onClick: () => void;
|
||||
focus?: boolean;
|
||||
tabIndex?: number;
|
||||
type?: "submit" | "button";
|
||||
}
|
||||
|
||||
function SubmitButton({ question, isLastQuestion, brandColor, onClick, type = "submit" }: SubmitButtonProps) {
|
||||
function SubmitButton({
|
||||
question,
|
||||
isLastQuestion,
|
||||
brandColor,
|
||||
onClick,
|
||||
tabIndex = 1,
|
||||
focus = false,
|
||||
type = "submit",
|
||||
}: SubmitButtonProps) {
|
||||
const buttonRef = useCallback((currentButton: HTMLButtonElement | null) => {
|
||||
if (currentButton && focus) {
|
||||
currentButton.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
tabIndex={tabIndex}
|
||||
autoFocus={focus}
|
||||
className={cn(
|
||||
"flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2",
|
||||
isLight(brandColor) ? "text-black" : "text-white"
|
||||
|
||||
Reference in New Issue
Block a user