diff --git a/src/lib/components/dialog/dialog.test.ts b/src/lib/components/dialog/dialog.test.ts
index ea8f2ff..3e799f2 100644
--- a/src/lib/components/dialog/dialog.test.ts
+++ b/src/lib/components/dialog/dialog.test.ts
@@ -12,7 +12,12 @@ import { click, Keys, press } from "$lib/test-utils/interactions";
import Transition from "$lib/components/transitions/TransitionRoot.svelte";
import { tick } from "svelte";
-jest.mock('../../hooks/use-id')
+let id = 0;
+jest.mock('../../hooks/use-id', () => {
+ return {
+ useId: jest.fn(() => ++id),
+ }
+})
// @ts-expect-error
global.IntersectionObserver = class FakeIntersectionObserver {
@@ -20,6 +25,7 @@ global.IntersectionObserver = class FakeIntersectionObserver {
disconnect() { }
}
+beforeEach(() => id = 0)
afterAll(() => jest.restoreAllMocks())
describe('Safe guards', () => {
@@ -56,10 +62,7 @@ describe('Safe guards', () => {
]
})
- assertDialog({
- state: DialogState.InvisibleUnmounted,
- attributes: { id: 'headlessui-dialog-1' },
- })
+ assertDialog({ state: DialogState.InvisibleUnmounted })
})
)
})
@@ -355,4 +358,103 @@ describe('Composition', () => {
)
})
+describe('Keyboard interactions', () => {
+ describe('`Escape` key', () => {
+ it(
+ 'should be possible to close the dialog with Escape',
+ async () => {
+ render(
+ TestRenderer, {
+ allProps: [
+ [ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
+ "Contents",
+ [TestTabSentinel],
+ ]],
+ ]
+ })
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById("trigger"))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Close dialog
+ await press(Keys.Escape)
+
+ // Verify it is close
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+ }
+ )
+
+ it(
+ 'should be possible to close the dialog with Escape, when a field is focused',
+ suppressConsoleLogs(async () => {
+ render(
+ TestRenderer, {
+ allProps: [
+ [ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
+ ["Contents"],
+ [Input, { id: "name" }],
+ [TestTabSentinel],
+ ]],
+ ]
+ })
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Close dialog
+ await press(Keys.Escape)
+
+ // Verify it is close
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+ })
+ )
+
+ it(
+ 'should not be possible to close the dialog with Escape, when a field is focused but cancels the event',
+ async () => {
+ render(
+ TestRenderer, {
+ allProps: [
+ [ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
+ ["Contents"],
+ [Input, { id: "name", onKeydown: (e: CustomEvent) => { e.preventDefault(); e.stopPropagation(); } }],
+ [TestTabSentinel],
+ ]],
+ ]
+ })
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Try to close the dialog
+ await press(Keys.Escape)
+
+ // Verify it is still open
+ assertDialog({ state: DialogState.Visible })
+ })
+ })
+})
diff --git a/src/lib/test-utils/TestRenderer.svelte b/src/lib/test-utils/TestRenderer.svelte
index 06d013f..33adca9 100644
--- a/src/lib/test-utils/TestRenderer.svelte
+++ b/src/lib/test-utils/TestRenderer.svelte
@@ -4,18 +4,25 @@
onChange?: HandlerType;
onClose?: HandlerType;
onFocus?: HandlerType;
+ onKeydown?: HandlerType;
}
- type SingleComponent = [SvelteComponent, ComponentProps, TestRendererProps];
+ type SingleComponent =
+ | string
+ | [SvelteComponent, ComponentProps, TestRendererProps];
export type TestRendererProps =
| undefined
- | string
| SingleComponent
| SingleComponent[];
function isSingleComponent(
props: SingleComponent | SingleComponent[]
): props is SingleComponent {
- return Array.isArray(props) && !Array.isArray(props[0]);
+ return (
+ typeof props === "string" ||
+ (Array.isArray(props) &&
+ !Array.isArray(props[0]) &&
+ typeof props[0] !== "string")
+ );
}
@@ -28,32 +35,37 @@
let onChange: HandlerType = () => {};
let onClose: HandlerType = () => {};
let onFocus: HandlerType = () => {};
+ let onKeydown: HandlerType = () => {};
if (allProps && typeof allProps !== "string" && isSingleComponent(allProps)) {
({
onChange = onChange,
onClose = onClose,
onFocus = onFocus,
+ onKeydown = onKeydown,
...spreadProps
} = allProps[1] || {});
}
-{#if typeof allProps === "string"}
- {allProps}
-{:else if Array.isArray(allProps)}
- {#if Array.isArray(allProps[0])}
+{#if allProps}
+ {#if isSingleComponent(allProps)}
+ {#if typeof allProps === "string"}
+ {allProps}
+ {:else}
+
+
+
+ {/if}
+ {:else}
{#each allProps as childProps}
{/each}
- {:else}
-
-
-
{/if}
{/if}
diff --git a/src/lib/test-utils/interactions.ts b/src/lib/test-utils/interactions.ts
index 9bc0ce0..72b5f1b 100644
--- a/src/lib/test-utils/interactions.ts
+++ b/src/lib/test-utils/interactions.ts
@@ -181,9 +181,9 @@ export async function click(
if (button === MouseButton.Left) {
// Cancel in pointerDown cancels mouseDown, mouseUp
- let cancelled = !fireEvent.pointerDown(element, options)
+ let cancelled = !(await fireEvent.pointerDown(element, options))
if (!cancelled) {
- fireEvent.mouseDown(element, options)
+ await fireEvent.mouseDown(element, options)
}
// Ensure to trigger a `focus` event if the element is focusable, or within a focusable element
@@ -196,26 +196,25 @@ export async function click(
next = next.parentElement
}
- fireEvent.pointerUp(element, options)
+ await fireEvent.pointerUp(element, options)
if (!cancelled) {
- fireEvent.mouseUp(element, options)
+ await fireEvent.mouseUp(element, options)
}
- fireEvent.click(element, options)
+ await fireEvent.click(element, options)
} else if (button === MouseButton.Right) {
// Cancel in pointerDown cancels mouseDown, mouseUp
- let cancelled = !fireEvent.pointerDown(element, options)
+ let cancelled = !(await fireEvent.pointerDown(element, options))
if (!cancelled) {
- fireEvent.mouseDown(element, options)
+ await fireEvent.mouseDown(element, options)
}
// Only in Firefox:
- fireEvent.pointerUp(element, options)
+ await fireEvent.pointerUp(element, options)
if (!cancelled) {
- fireEvent.mouseUp(element, options)
+ await fireEvent.mouseUp(element, options)
}
}
- await tick()
} catch (err: any) {
Error.captureStackTrace(err, click)
throw err
@@ -226,7 +225,7 @@ export async function focus(element: Document | Element | Window | null) {
try {
if (element === null) return expect(element).not.toBe(null)
- fireEvent.focus(element)
+ await fireEvent.focus(element)
await tick()
} catch (err: any) {
@@ -238,9 +237,9 @@ export async function mouseEnter(element: Document | Element | Window | null) {
try {
if (element === null) return expect(element).not.toBe(null)
- fireEvent.pointerOver(element)
- fireEvent.pointerEnter(element)
- fireEvent.mouseOver(element)
+ await fireEvent.pointerOver(element)
+ await fireEvent.pointerEnter(element)
+ await fireEvent.mouseOver(element)
await tick()
} catch (err: any) {
@@ -253,8 +252,8 @@ export async function mouseMove(element: Document | Element | Window | null) {
try {
if (element === null) return expect(element).not.toBe(null)
- fireEvent.pointerMove(element)
- fireEvent.mouseMove(element)
+ await fireEvent.pointerMove(element)
+ await fireEvent.mouseMove(element)
await tick()
} catch (err: any) {
@@ -267,10 +266,10 @@ export async function mouseLeave(element: Document | Element | Window | null) {
try {
if (element === null) return expect(element).not.toBe(null)
- fireEvent.pointerOut(element)
- fireEvent.pointerLeave(element)
- fireEvent.mouseOut(element)
- fireEvent.mouseLeave(element)
+ await fireEvent.pointerOut(element)
+ await fireEvent.pointerLeave(element)
+ await fireEvent.mouseOut(element)
+ await fireEvent.mouseLeave(element)
await tick()
} catch (err: any) {