diff --git a/src/hooks/useClickOutside/useClickOutside.svelte b/src/hooks/useClickOutside/useClickOutside.svelte new file mode 100644 index 0000000..3ef1b3a --- /dev/null +++ b/src/hooks/useClickOutside/useClickOutside.svelte @@ -0,0 +1,22 @@ + + +
+
hello
+
diff --git a/src/hooks/useClickOutside/useClickOutside.test.ts b/src/hooks/useClickOutside/useClickOutside.test.ts new file mode 100644 index 0000000..22eef17 --- /dev/null +++ b/src/hooks/useClickOutside/useClickOutside.test.ts @@ -0,0 +1,43 @@ +import { render, screen } from "@testing-library/svelte"; +import userEvent from "@testing-library/user-event"; + +import UseClickOutside from "@/hooks/useClickOutside/useClickOutside.svelte"; + +vi.mock("esm-env", async (importOriginal) => { + const actual: any = await importOriginal(); + + return { + ...actual, + BROWSER: true, + }; +}); + +describe("Hooks - useClickOutside", () => { + test("should work on click outside the element", async () => { + const clickMockFn = vi.fn(); + + render(UseClickOutside, { onClick: clickMockFn }); + + const element = screen.getByText(/hello/i); + + await userEvent.click(element); + + expect(clickMockFn).not.toBeCalled(); + + await userEvent.click(document.body); + + expect(clickMockFn).toBeCalled(); + }); + + test("should NOT work after unmount component", async () => { + const clickMockFn = vi.fn(); + + const { unmount } = render(UseClickOutside, { onClick: clickMockFn }); + + await unmount(); + + await userEvent.click(document.body); + + expect(clickMockFn).not.toBeCalled(); + }); +}); diff --git a/src/hooks/useClickOutside/useClickOutside.ts b/src/hooks/useClickOutside/useClickOutside.ts index 7957061..ee7ca5e 100644 --- a/src/hooks/useClickOutside/useClickOutside.ts +++ b/src/hooks/useClickOutside/useClickOutside.ts @@ -1,19 +1,40 @@ +import { onMount } from "svelte"; + import { useEventListener } from ".."; type Handler = (event: MouseEvent) => void; -export function useClickOutside( - ref: T, - handler: Handler, - mouseEvent: "mousedown" | "mouseup" = "mousedown", -): void { - useEventListener(mouseEvent, (event: MouseEvent) => { - const el = ref; +type Options = { + handler: Handler; + mouseEvent?: "mousedown" | "mouseup"; + component?: boolean; +}; + +export function useClickOutside({ + handler, + mouseEvent = "mousedown", + component = false, +}: Options): { setElementRef: (elm: T) => void; removeListener: () => void } { + let element: T; - if (!el || el.contains(event.target as Node)) { + const removeListener = useEventListener(mouseEvent, (event: MouseEvent) => { + if (!element || element.contains(event.target as Node)) { return; } handler(event); }); + + if (component) { + onMount(() => { + return () => { + removeListener(); + }; + }); + } + + return { + setElementRef: (elm) => (element = elm), + removeListener, + }; } diff --git a/src/routes/guide/+layout.server.ts b/src/routes/guide/+layout.server.ts index e65a248..c1e2e13 100644 --- a/src/routes/guide/+layout.server.ts +++ b/src/routes/guide/+layout.server.ts @@ -14,6 +14,10 @@ export async function load({ route }) { label: "useClickAnyWhere", link: "/guide/useClickAnyWhere", }, + { + label: "useClickOutside", + link: "/guide/useClickOutside", + }, { label: "useClipboard", link: "/guide/useClipboard", diff --git a/src/routes/guide/useClickOutside/+page.svelte b/src/routes/guide/useClickOutside/+page.svelte new file mode 100644 index 0000000..3bf3aa2 --- /dev/null +++ b/src/routes/guide/useClickOutside/+page.svelte @@ -0,0 +1,49 @@ + + + +
+

Hook to catch the click on any part of the site

+ +

Related hooks

+ +
    +
  • useEventListener
  • +
+
+ +
+

Visual example

+ + +
click outside here
+
+
+ +
+

Code example

+ + +
+
diff --git a/src/routes/guide/useClickOutside/code-snippet.js b/src/routes/guide/useClickOutside/code-snippet.js new file mode 100644 index 0000000..48896eb --- /dev/null +++ b/src/routes/guide/useClickOutside/code-snippet.js @@ -0,0 +1,24 @@ +export default ` + + + + +
+ content +
+`;