Skip to content

Commit

Permalink
feat: add useClickOutside
Browse files Browse the repository at this point in the history
  • Loading branch information
sviripa committed May 3, 2024
1 parent b7266cb commit 4bbb7aa
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-rabbits-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

feat: `useClickOutside`
1 change: 1 addition & 0 deletions packages/runed/src/lib/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./box/index.js";
export * from "./useActiveElement/index.js";
export * from "./useClickOutside/index.js";
export * from "./useDebounce/index.js";
export * from "./useElementSize/index.js";
export * from "./useEventListener/index.js";
Expand Down
1 change: 1 addition & 0 deletions packages/runed/src/lib/functions/useClickOutside/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useClickOutside.svelte.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { box, type WritableBox } from "$lib/functions/box/box.svelte.js";
import { watch } from "$lib/functions/watch/watch.svelte.js";

type ClickOutside = {
start: () => void;
stop: () => void;
};

/**
* Accepts a box which holds a container element and callback function.
* Invokes the callback function when the user clicks outside of the
* container.
*
* @returns an object with start and stop functions
*
* @see {@link https://runed.dev/docs/functions/use-click-outside}
*/
export function useClickOutside<T extends Element>(
container: WritableBox<T | null>,
fn: () => void
): ClickOutside {
const isEnabled = box<boolean>(true);

function start() {
isEnabled.value = true;
}

function stop() {
isEnabled.value = false;
}

function handleClick(event: MouseEvent) {
if (event.target && !container.value?.contains(event.target as Node)) {
fn();
}
}

watch([container, isEnabled], ([currentContainer, currentIsEnabled]) => {
if (currentContainer && currentIsEnabled) {
window.addEventListener("click", handleClick);
}

return () => {
window.removeEventListener("click", handleClick);
};
});

return {
start,
stop,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, vi } from "vitest";
import { tick } from "svelte";
import { testWithEffect } from "$lib/test/util.svelte.js";
import { box } from "$lib/functions/box/box.svelte.js";
import { useClickOutside } from "./useClickOutside.svelte.js";

describe("useClickOutside", () => {
testWithEffect("calls a given callback when on an outside of container click", async () => {
const container = document.createElement("div");
const innerButton = document.createElement("button");
const button = document.createElement("button");

document.body.appendChild(container);
document.body.appendChild(button);
container.appendChild(innerButton);

const callbackFn = vi.fn();

useClickOutside(box.from(container), callbackFn);
await tick();

button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(callbackFn).toHaveBeenCalledOnce();

innerButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(callbackFn).toHaveBeenCalledOnce();

container.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(callbackFn).toHaveBeenCalledOnce();
});

testWithEffect("can be paused and resumed", async () => {
const container = document.createElement("div");
const button = document.createElement("button");

document.body.appendChild(container);
document.body.appendChild(button);

const callbackFn = vi.fn();

const clickOutside = useClickOutside(box.from(container), callbackFn);

clickOutside.stop();
await tick();

button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(callbackFn).not.toHaveBeenCalled();

clickOutside.start();
await tick();

button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(callbackFn).toHaveBeenCalledOnce();
});
});

0 comments on commit 4bbb7aa

Please sign in to comment.