Skip to content

Commit

Permalink
fix(hooks/useClickOutside): add useClickOutside hook
Browse files Browse the repository at this point in the history
  • Loading branch information
dimaslz committed Mar 6, 2024
1 parent 30dbc65 commit 5c286ac
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 8 deletions.
22 changes: 22 additions & 0 deletions src/hooks/useClickOutside/useClickOutside.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
import { onMount } from "svelte";
import { useClickOutside } from "@/hooks";
export let onClick: () => void;
let element: HTMLElement;
const { removeListener, setElementRef } = useClickOutside({ handler: onClick, component: true });
onMount(() => {
setElementRef(element);
return () => {
removeListener();
};
});
</script>

<div class="w-full h-32">
<div bind:this={element}>hello</div>
</div>
43 changes: 43 additions & 0 deletions src/hooks/useClickOutside/useClickOutside.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
37 changes: 29 additions & 8 deletions src/hooks/useClickOutside/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { onMount } from "svelte";

import { useEventListener } from "..";

type Handler = (event: MouseEvent) => void;

export function useClickOutside<T extends HTMLElement = HTMLElement>(
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<T extends HTMLElement = HTMLElement>({
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,
};
}
4 changes: 4 additions & 0 deletions src/routes/guide/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export async function load({ route }) {
label: "useClickAnyWhere",
link: "/guide/useClickAnyWhere",
},
{
label: "useClickOutside",
link: "/guide/useClickOutside",
},
{
label: "useClipboard",
link: "/guide/useClipboard",
Expand Down
49 changes: 49 additions & 0 deletions src/routes/guide/useClickOutside/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import { onMount } from "svelte";
import { Browser, DocTpl, H2, Highlight, Link } from "@/components";
import { useClickOutside } from "@/hooks";
import code from "./code-snippet";
const onClickHandler = () => {
console.log("onClickHandler");
};
let element: HTMLElement;
const { removeListener, setElementRef } = useClickOutside({ handler: onClickHandler });
onMount(() => {
setElementRef(element);
return () => {
removeListener();
};
});
</script>

<DocTpl title="useClickOutside">
<div slot="description">
<p>Hook to catch the click on any part of the site</p>

<h3>Related hooks</h3>

<ul class="list-disc pl-6">
<li><Link href="/guide/useEventListener">useEventListener</Link></li>
</ul>
</div>

<div slot="visual-example">
<H2>Visual example</H2>

<Browser body="p-4 bg-gray-950/50">
<div class="h-24 w-24 bg-gray-600 p-2" bind:this={element}>click outside here</div>
</Browser>
</div>

<div slot="code-example">
<H2>Code example</H2>

<Highlight {code} />
</div>
</DocTpl>
24 changes: 24 additions & 0 deletions src/routes/guide/useClickOutside/code-snippet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default `
<!-- javascript -->
<script lang="ts">
import { onMount } from "svelte";
import { useClickAnyWhere } from "@dimaslz/svelteuse";
const someCallback = () => {
console.log("click anywhere!");
};
onMount(() => {
const eventClickAnyWhere = useClickAnyWhere(someCallback);
return () => {
eventClickAnyWhere();
}
})
</script>
<!-- html -->
<div>
content
</div>
`;

0 comments on commit 5c286ac

Please sign in to comment.