mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-06 02:46:46 -05:00
fix: better a11y for modal surveys using a focus trap
This commit is contained in:
@@ -64,7 +64,6 @@ export function RenderSurvey(props: SurveyContainerProps) {
|
||||
onClose={close}
|
||||
isOpen={isOpen}
|
||||
dir={dir}>
|
||||
{/* @ts-expect-error -- TODO: fix this */}
|
||||
<Survey
|
||||
{...props}
|
||||
clickOutside={hasOverlay ? props.clickOutside : true}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, render, screen } from "@testing-library/preact";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { SurveyContainer } from "./survey-container";
|
||||
|
||||
describe("SurveyContainer", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("marks modal surveys as labelled modal dialogs", () => {
|
||||
render(
|
||||
<SurveyContainer mode="modal">
|
||||
<button>Start</button>
|
||||
</SurveyContainer>
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole("dialog", { name: "Dialog" });
|
||||
|
||||
expect(dialog.getAttribute("aria-modal")).toBe("true");
|
||||
});
|
||||
|
||||
test("does not add dialog semantics to inline surveys", () => {
|
||||
render(
|
||||
<SurveyContainer mode="inline">
|
||||
<button>Start</button>
|
||||
</SurveyContainer>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
test("wires the modal dialog to the survey content", () => {
|
||||
render(
|
||||
<SurveyContainer mode="modal">
|
||||
<button>Start</button>
|
||||
</SurveyContainer>
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole("dialog", { name: "Dialog" });
|
||||
|
||||
expect(dialog.contains(screen.getByRole("button", { name: "Start" }))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { type ComponentChildren } from "preact";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { type TOverlay, type TPlacement } from "@formbricks/types/common";
|
||||
import { useFocusTrap } from "@/lib/use-focus-trap";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SurveyContainerProps {
|
||||
mode: "modal" | "inline";
|
||||
placement?: TPlacement;
|
||||
overlay?: TOverlay;
|
||||
children: React.ReactNode;
|
||||
children: ComponentChildren;
|
||||
onClose?: () => void;
|
||||
clickOutside?: boolean;
|
||||
isOpen?: boolean;
|
||||
@@ -23,8 +25,8 @@ export function SurveyContainer({
|
||||
isOpen = true,
|
||||
dir = "auto",
|
||||
}: Readonly<SurveyContainerProps>) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const isModal = mode === "modal";
|
||||
const modalRef = useFocusTrap<HTMLDivElement>({ enabled: isModal && isOpen, onEscapeKeyDown: onClose });
|
||||
const hasOverlay = overlay !== "none";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,7 +49,7 @@ export function SurveyContainer({
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [clickOutside, onClose, isModal, isOpen]);
|
||||
}, [clickOutside, hasOverlay, modalRef, onClose, isModal, isOpen]);
|
||||
|
||||
const getPlacementStyle = (placement: TPlacement): string => {
|
||||
switch (placement) {
|
||||
@@ -92,6 +94,10 @@ export function SurveyContainer({
|
||||
)}>
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Dialog"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
getPlacementStyle(placement),
|
||||
isOpen ? "opacity-100" : "opacity-0",
|
||||
|
||||
Reference in New Issue
Block a user