Skip to content

Commit

Permalink
refactor: update useEventListener composable (#1156)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek authored Dec 19, 2024
1 parent f0eccaf commit 0af89fa
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -535,9 +535,9 @@ function rightIconClick(event: Event): void {
if (isClient && props.checkScroll)
useEventListener(
computed(() => dropdownRef.value?.$content),
"scroll",
checkDropdownScroll,
computed(() => dropdownRef.value?.$content),
);
/** Check if the scroll list inside the dropdown reached the top or it's end. */
Expand Down
8 changes: 4 additions & 4 deletions packages/oruga/src/components/carousel/Carousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ onBeforeUnmount(() => {
// add dom event handler
if (isClient) {
useEventListener("resize", onResized, window);
useEventListener("animationend", onRefresh);
useEventListener("transitionend", onRefresh);
useEventListener("transitionstart", onRefresh);
useEventListener(window, "resize", onResized);
useEventListener(document, "animationend", onRefresh);
useEventListener(document, "transitionend", onRefresh);
useEventListener(document, "transitionstart", onRefresh);
}
function onResized(): void {
Expand Down
17 changes: 10 additions & 7 deletions packages/oruga/src/components/dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,21 @@ watch(
if (cancelOptions.value.includes("outside")) {
// set outside handler
eventCleanups.push(
useClickOutside(contentRef, onClickedOutside, {
ignore: [triggerRef],
immediate: true,
passive: true,
}),
useClickOutside(
[contentRef, triggerRef],
onClickedOutside,
{
immediate: true,
passive: true,
},
),
);
}
if (cancelOptions.value.includes("escape")) {
// set keyup handler
eventCleanups.push(
useEventListener("keyup", onKeyPress, document, {
useEventListener(document, "keyup", onKeyPress, {
immediate: true,
}),
);
Expand Down Expand Up @@ -287,7 +290,7 @@ function onClose(): void {
// --- InfitiveScroll Feature ---
if (isClient && props.checkScroll)
useEventListener("scroll", checkDropdownScroll, contentRef);
useEventListener(contentRef, "scroll", checkDropdownScroll);
/** Check if the scroll list inside the dropdown reached the top or it's end. */
function checkDropdownScroll(): void {
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/loading/Loading.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ watch(isActive, (value) => {
if (isClient) {
// register onKeyPress event when is active
useEventListener("keyup", onKeyPress, rootRef, { trigger: isActive });
useEventListener(rootRef, "keyup", onKeyPress, { trigger: isActive });
}
/** Keypress event that is bound to the document. */
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/modal/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ onMounted(() => {
if (isClient) {
// register onKeyPress event listener when is active
useEventListener("keyup", onKeyPress, rootRef, { trigger: isActive });
useEventListener(rootRef, "keyup", onKeyPress, { trigger: isActive });
if (!props.overlay)
// register outside click event listener when is active
Expand Down
3 changes: 2 additions & 1 deletion packages/oruga/src/components/sidebar/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ onBeforeUnmount(() => {
if (isClient) {
// register onKeyPress event listener when is active
useEventListener("keyup", onKeyPress, rootRef, { trigger: isActive });
useEventListener(rootRef, "keyup", onKeyPress, { trigger: isActive });
if (!props.overlay)
// register outside click event listener when is active
useClickOutside(contentRef, clickedOutside, { trigger: isActive });
Expand Down
15 changes: 9 additions & 6 deletions packages/oruga/src/components/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,21 @@ watch(isActive, (value) => {
if (cancelOptions.value.indexOf("outside") >= 0) {
// set outside handler
eventCleanups.push(
useClickOutside(contentRef, onClickedOutside, {
ignore: [triggerRef],
immediate: true,
passive: true,
}),
useClickOutside(
[contentRef, triggerRef],
onClickedOutside,
{
immediate: true,
passive: true,
},
),
);
}
if (cancelOptions.value.indexOf("escape") >= 0) {
// set keyup handler
eventCleanups.push(
useEventListener("keyup", onKeyPress, document, {
useEventListener(document, "keyup", onKeyPress, {
immediate: true,
}),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/utils/PickerWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const triggers = computed(() =>
isTrueish(props.pickerProps.openOnFocus) ? ["click"] : [],
);
if (isClient) useEventListener("keyup", onKeyPress);
if (isClient) useEventListener(document, "keyup", onKeyPress);
/** Keypress event that is bound to the document. */
function onKeyPress(event: KeyboardEvent): void {
Expand Down
68 changes: 68 additions & 0 deletions packages/oruga/src/composables/tests/useClickOutside.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
import { useClickOutside } from "../";

describe("useClickOutside test", () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.restoreAllMocks();
});

test("should call handler when clicking outside the element", () => {
const handler = vi.fn();
const element = document.createElement("div");
document.body.appendChild(element);

const stop = useClickOutside(element, handler, { immediate: true });
vi.runAllTimers();

// Simulate click outside
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));

expect(handler).toHaveBeenCalled();

stop();
document.body.removeChild(element);
});

test("should not call handler when clicking inside the element", () => {
const handler = vi.fn();
const element = document.createElement("div");
document.body.appendChild(element);

const stop = useClickOutside(element, handler, { immediate: true });
vi.runAllTimers();

// Simulate click inside
element.dispatchEvent(new MouseEvent("click", { bubbles: true }));

expect(handler).not.toHaveBeenCalled();

stop();
document.body.removeChild(element);
});

test("should respect ignore option", () => {
const handler = vi.fn();
const element = document.createElement("div");
const ignoreElement = document.createElement("div");
document.body.appendChild(element);
document.body.appendChild(ignoreElement);

const stop = useClickOutside([element, ignoreElement], handler, {
immediate: true,
});
vi.runAllTimers();

// Simulate click on ignored element
ignoreElement.dispatchEvent(new MouseEvent("click", { bubbles: true }));

expect(handler).not.toHaveBeenCalled();

stop();
document.body.removeChild(element);
document.body.removeChild(ignoreElement);
});
});
163 changes: 163 additions & 0 deletions packages/oruga/src/composables/tests/useEventListener.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
afterEach,
beforeEach,
describe,
expect,
test,
vi,
type MockInstance,
} from "vitest";
import { effectScope, nextTick, ref, type Ref } from "vue";

import { useEventListener, type EventListenerOptions } from "../";

describe("useEventListener test", () => {
const options: EventListenerOptions = { immediate: true };
let stop: () => void;
let target: HTMLDivElement;
let removeSpy: MockInstance;
let addSpy: MockInstance;
let listener: () => void;

beforeEach(() => {
vi.useFakeTimers();
target = document.createElement("div");
removeSpy = vi.spyOn(target, "removeEventListener");
addSpy = vi.spyOn(target, "addEventListener");
});

afterEach(() => {
vi.restoreAllMocks();
});

test("should be defined", () => {
expect(useEventListener).toBeDefined();
});

describe("given event", () => {
const event = "click";

beforeEach(() => {
listener = vi.fn();
});

test("should add listener", async () => {
stop = useEventListener(target, event, listener, {
immediate: true,
});
vi.runAllTimers();
expect(addSpy).toBeCalledTimes(1);
});

test("should trigger listener", () => {
stop = useEventListener(target, event, listener, {
immediate: true,
});
vi.runAllTimers();
expect(listener).not.toBeCalled();
target.dispatchEvent(new MouseEvent(event));
expect(listener).toBeCalledTimes(1);
});

test("should remove listener", () => {
stop = useEventListener(target, event, listener, {
immediate: true,
});
vi.runAllTimers();
expect(removeSpy).not.toBeCalled();

stop();

expect(removeSpy).toBeCalledTimes(1);
expect(removeSpy).toBeCalledWith(event, listener, options);
});
});

describe("reactive target", () => {
let target: Ref<HTMLDivElement | null>;

beforeEach(() => {
target = ref(document.createElement("div"));
});

test("should not listen when target is invalid", async () => {
useEventListener(target, "click", listener);
const el = target.value;
target.value = null;
await nextTick();
el?.dispatchEvent(new MouseEvent("click"));
await nextTick();

expect(listener).toHaveBeenCalledTimes(0);
});

test(`should listen event`, async () => {
useEventListener(target, "click", listener, { immediate: true });
vi.runAllTimers();
target.value!.dispatchEvent(new MouseEvent("click"));

await nextTick();

expect(listener).toHaveBeenCalledTimes(1);
});

test(`should manually stop listening event`, async () => {
const stop = useEventListener(target, "click", listener, {
immediate: true,
});

stop();

target.value!.dispatchEvent(new MouseEvent("click"));

await nextTick();

expect(listener).toHaveBeenCalledTimes(0);
});

test(`should auto stop listening event`, async () => {
const scope = effectScope();
await scope.run(async () => {
useEventListener(target, "click", listener, {
immediate: true,
});
});

await scope.stop();

target.value!.dispatchEvent(new MouseEvent("click"));

await nextTick();

expect(listener).toHaveBeenCalledTimes(0);
});
});

test("should auto register on trigger", async () => {
const trigger = ref(false);

useEventListener(target, "click", listener, { trigger });

vi.runAllTimers();
expect(addSpy).toHaveBeenCalledTimes(0);

trigger.value = true;
await nextTick();
vi.runAllTimers();

expect(addSpy).toHaveBeenCalledTimes(1);
expect(addSpy).toHaveBeenLastCalledWith("click", listener, { trigger });
expect(removeSpy).toHaveBeenCalledTimes(0);

trigger.value = false;
await nextTick();
vi.runAllTimers();

await nextTick();
expect(addSpy).toHaveBeenCalledTimes(1);
expect(removeSpy).toHaveBeenCalledTimes(1);
expect(removeSpy).toHaveBeenLastCalledWith("click", listener, {
trigger,
});
});
});
Loading

0 comments on commit 0af89fa

Please sign in to comment.