import { describe, expect, it } from "@effect/vitest"; import { applyOutsideDismissPolicy, containsFormField, dismissesOnOutsideInteraction, FORM_FIELD_SELECTOR, PORTALED_POPUP_SELECTOR, } from "./outside-dismiss"; /** * The property under test: a dialog or sheet keeps what the user typed. * * Radix closes an overlay surface whenever an outside interaction reaches it * un-prevented, so "does not dismiss" is the thing that has to be asserted, not * assumed. Escape is Radix's own `onEscapeKeyDown` path and never passes * through here, which is why no case below can close a surface with a key. */ describe("dismissesOnOutsideInteraction", () => { it("keeps a surface with a form field open by default", () => { expect( dismissesOnOutsideInteraction({ dismissOnOutsideClick: undefined, insidePortaledPopup: false, containsFormField: true, }), ).toBe(false); }); it("closes a surface with no form field by default", () => { expect( dismissesOnOutsideInteraction({ dismissOnOutsideClick: undefined, insidePortaledPopup: false, containsFormField: false, }), ).toBe(true); }); it("closes a form surface that explicitly opts in", () => { expect( dismissesOnOutsideInteraction({ dismissOnOutsideClick: true, insidePortaledPopup: false, containsFormField: true, }), ).toBe(true); }); it("keeps a field-free surface open when it explicitly opts out", () => { expect( dismissesOnOutsideInteraction({ dismissOnOutsideClick: false, insidePortaledPopup: false, containsFormField: false, }), ).toBe(false); }); it("never closes on a click inside a portaled popup, even when it opts in", () => { // A combobox or select renders its list outside the surface, so choosing an // option arrives as an outside interaction. Dismissing there would drop the // selection before it lands. expect( dismissesOnOutsideInteraction({ dismissOnOutsideClick: true, insidePortaledPopup: true, containsFormField: false, }), ).toBe(false); }); }); /** A stand-in for the event Radix dispatches, recording whether it was blocked. */ const outsideEvent = () => { let prevented = false; return { event: { detail: { originalEvent: { target: { nodeName: "DIV" } } }, preventDefault: () => { prevented = true; }, }, prevented: () => prevented, }; }; /** A stand-in for the surface node, recording what it was asked for. */ const surfaceStub = (options: { readonly hasFormField: boolean }) => { const queries: Array = []; return { surface: { querySelector: (selectors: string) => { queries.push(selectors); return options.hasFormField ? { nodeName: "INPUT" } : null; }, }, queries: () => queries, }; }; describe("applyOutsideDismissPolicy", () => { it("blocks a plain outside click while the surface holds a form field", () => { const outside = outsideEvent(); const stub = surfaceStub({ hasFormField: true }); applyOutsideDismissPolicy(outside.event, undefined, stub.surface); expect(outside.prevented()).toBe(true); expect(stub.queries()).toEqual([FORM_FIELD_SELECTOR]); }); it("lets a plain outside click through when the surface has no form field", () => { const outside = outsideEvent(); const stub = surfaceStub({ hasFormField: false }); applyOutsideDismissPolicy(outside.event, undefined, stub.surface); expect(outside.prevented()).toBe(false); }); it("lets an outside click through when a form surface opts in", () => { const outside = outsideEvent(); const stub = surfaceStub({ hasFormField: true }); applyOutsideDismissPolicy(outside.event, true, stub.surface); expect(outside.prevented()).toBe(false); }); it("blocks an outside click when a field-free surface opts out", () => { const outside = outsideEvent(); const stub = surfaceStub({ hasFormField: false }); applyOutsideDismissPolicy(outside.event, false, stub.surface); expect(outside.prevented()).toBe(true); }); it("blocks the click when the surface node is not mounted yet", () => { // No node means no way to prove the surface is safe to discard, so the // policy falls back to keeping it open. const outside = outsideEvent(); applyOutsideDismissPolicy(outside.event, undefined, null); expect(outside.prevented()).toBe(true); }); }); describe("containsFormField", () => { it("reports a field when the surface query matches", () => { expect(containsFormField(surfaceStub({ hasFormField: true }).surface)).toBe(true); }); it("reports no field for a missing surface", () => { expect(containsFormField(null)).toBe(false); }); }); describe("FORM_FIELD_SELECTOR", () => { it("covers native fields, contenteditable, and ARIA widget roles", () => { // base-ui and Radix render select/combobox triggers, checkboxes, switches, // radios, and sliders as buttons whose only signature is the ARIA role, so // every one of these must stay in the selector or a dialog holding it // starts dismissing on stray clicks. for (const part of [ "input", "textarea", "select", "[contenteditable]:not([contenteditable='false'])", "[role='checkbox']", "[role='combobox']", "[role='listbox']", "[role='radio']", "[role='searchbox']", "[role='slider']", "[role='spinbutton']", "[role='switch']", "[role='textbox']", ]) { expect(FORM_FIELD_SELECTOR.split(",")).toContain(part); } }); }); describe("PORTALED_POPUP_SELECTOR", () => { it("covers both popup slots that portal out of a surface", () => { expect(PORTALED_POPUP_SELECTOR).toBe( "[data-slot='combobox-content'],[data-slot='select-content']", ); }); });