From eb9f97bcc81c7f2b764a0776a420e4dc99bc227b Mon Sep 17 00:00:00 2001 From: Jayden Carey Date: Mon, 8 Jul 2024 18:57:09 +0800 Subject: [PATCH 1/5] create isHovered utility --- .../utilities/IsHovered/IsHovered.svelte.ts | 40 +++++++++++++++++++ .../src/lib/utilities/IsHovered/index.ts | 1 + packages/runed/src/lib/utilities/index.ts | 1 + sites/docs/content/utilities/is-hovered.md | 30 ++++++++++++++ .../lib/components/demos/is-hovered.svelte | 21 ++++++++++ 5 files changed, 93 insertions(+) create mode 100644 packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts create mode 100644 packages/runed/src/lib/utilities/IsHovered/index.ts create mode 100644 sites/docs/content/utilities/is-hovered.md create mode 100644 sites/docs/src/lib/components/demos/is-hovered.svelte diff --git a/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts new file mode 100644 index 00000000..aadc19cd --- /dev/null +++ b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts @@ -0,0 +1,40 @@ +import { extract } from "../extract/extract.js"; + +import type { MaybeGetter } from "$lib/internal/types.js"; +import { addEventListener } from "$lib/internal/utils/event.js"; + +export class IsHovered { + #node: MaybeGetter; + #target = $derived.by(() => extract(this.#node)); + #current = $state(false); + + constructor(node: MaybeGetter) { + this.#node = node; + + const handleMouseEnter = () => { + this.#current = true; + }; + const handleMouseLeave = () => { + this.#current = false; + }; + + $effect(() => { + if (!this.#target) { + return; + } + + const callbacks: VoidFunction[] = []; + + callbacks.push(addEventListener(this.#target, "mouseenter", handleMouseEnter)); + callbacks.push(addEventListener(this.#target, "mouseleave", handleMouseLeave)); + + return () => { + return () => callbacks.forEach((c) => c()); + }; + }); + } + + get current(): boolean { + return this.#current; + } +} diff --git a/packages/runed/src/lib/utilities/IsHovered/index.ts b/packages/runed/src/lib/utilities/IsHovered/index.ts new file mode 100644 index 00000000..0f9d8c84 --- /dev/null +++ b/packages/runed/src/lib/utilities/IsHovered/index.ts @@ -0,0 +1 @@ +export * from "./IsHovered.svelte.js"; \ No newline at end of file diff --git a/packages/runed/src/lib/utilities/index.ts b/packages/runed/src/lib/utilities/index.ts index e9614b7d..38879346 100644 --- a/packages/runed/src/lib/utilities/index.ts +++ b/packages/runed/src/lib/utilities/index.ts @@ -20,3 +20,4 @@ export * from "./AnimationFrames/index.js"; export * from "./useIntersectionObserver/index.js"; export * from "./IsFocusWithin/index.js"; export * from "./FiniteStateMachine/index.js"; +export * from "./IsHovered/index.js"; diff --git a/sites/docs/content/utilities/is-hovered.md b/sites/docs/content/utilities/is-hovered.md new file mode 100644 index 00000000..860a2d53 --- /dev/null +++ b/sites/docs/content/utilities/is-hovered.md @@ -0,0 +1,30 @@ +--- +title: IsHovered +description: Determine if an element is hovered. +category: Utilities +--- + + + +## Demo + + + +## Usage + +```svelte + + + + +

Is hovered: {isHovered.current ? "true" : "false"}

+``` diff --git a/sites/docs/src/lib/components/demos/is-hovered.svelte b/sites/docs/src/lib/components/demos/is-hovered.svelte new file mode 100644 index 00000000..96b2bd76 --- /dev/null +++ b/sites/docs/src/lib/components/demos/is-hovered.svelte @@ -0,0 +1,21 @@ + + + + + + + +

+ Is hovered: {isHovered.current ? "true" : "false"} +

+
From 79080192bd0469ca0983026b68e397ab9e0b52b7 Mon Sep 17 00:00:00 2001 From: Jayden Carey Date: Mon, 8 Jul 2024 19:07:56 +0800 Subject: [PATCH 2/5] attempt test --- .../utilities/IsHovered/IsHovered.svelte.ts | 4 ++ .../IsHovered/IsHovered.test.svelte.ts | 38 +++++++++++++++++++ .../utilities/IsHovered/TestIsHovered.svelte | 9 +++++ 3 files changed, 51 insertions(+) create mode 100644 packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts create mode 100644 packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte diff --git a/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts index aadc19cd..b6da5a04 100644 --- a/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts +++ b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts @@ -3,6 +3,10 @@ import { extract } from "../extract/extract.js"; import type { MaybeGetter } from "$lib/internal/types.js"; import { addEventListener } from "$lib/internal/utils/event.js"; +/** + * Tracks whether the user is hovering over the target element. + * @see {@link https://runed.dev/docs/utilities/is-hovered} + */ export class IsHovered { #node: MaybeGetter; #target = $derived.by(() => extract(this.#node)); diff --git a/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts b/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts new file mode 100644 index 00000000..2ea36cef --- /dev/null +++ b/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts @@ -0,0 +1,38 @@ +import { render } from "@testing-library/svelte/svelte5"; +import { describe, expect, it } from "vitest"; +import { userEvent } from "@testing-library/user-event"; +import TestIsHovered from "./TestIsHovered.svelte"; + +function setup() { + const user = userEvent.setup(); + const result = render(TestIsHovered); + const hoverTarget = result.getByTestId("hover-target"); + + return { + ...result, + user, + hoverTarget, + }; +} + +describe("IsHovered", () => { + it("should be false on initial render", async () => { + const { hoverTarget } = setup(); + expect(hoverTarget).toHaveTextContent("false"); + }); + + it("should be true when hovered", async () => { + const { user, hoverTarget } = setup(); + expect(hoverTarget).toHaveTextContent("false"); + await user.hover(hoverTarget); + expect(hoverTarget).toHaveTextContent("true"); + }); + + it("should be false when unhovered", async () => { + const { user, hoverTarget } = setup(); + await user.hover(hoverTarget); + expect(hoverTarget).toHaveTextContent("true"); + await user.unhover(hoverTarget); + expect(hoverTarget).toHaveTextContent("false"); + }); +}); diff --git a/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte b/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte new file mode 100644 index 00000000..f35b50a7 --- /dev/null +++ b/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte @@ -0,0 +1,9 @@ + + +
{isHovered.current ? "true" : "false"}
From 59a6687d46af5d3192282f6e75c2f570f4c1dd84 Mon Sep 17 00:00:00 2001 From: Jayden Carey Date: Mon, 8 Jul 2024 20:20:46 +0800 Subject: [PATCH 3/5] fix test --- .../IsHovered/IsHovered.test.svelte.ts | 19 +++++++------------ .../utilities/IsHovered/TestIsHovered.svelte | 4 +++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts b/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts index 2ea36cef..d3fa341e 100644 --- a/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts +++ b/packages/runed/src/lib/utilities/IsHovered/IsHovered.test.svelte.ts @@ -1,38 +1,33 @@ -import { render } from "@testing-library/svelte/svelte5"; +import { fireEvent, render } from "@testing-library/svelte/svelte5"; import { describe, expect, it } from "vitest"; -import { userEvent } from "@testing-library/user-event"; import TestIsHovered from "./TestIsHovered.svelte"; function setup() { - const user = userEvent.setup(); const result = render(TestIsHovered); const hoverTarget = result.getByTestId("hover-target"); - return { ...result, - user, hoverTarget, }; } describe("IsHovered", () => { - it("should be false on initial render", async () => { + it("should be false on initial render", () => { const { hoverTarget } = setup(); expect(hoverTarget).toHaveTextContent("false"); }); it("should be true when hovered", async () => { - const { user, hoverTarget } = setup(); - expect(hoverTarget).toHaveTextContent("false"); - await user.hover(hoverTarget); + const { hoverTarget } = setup(); + await fireEvent.mouseEnter(hoverTarget); expect(hoverTarget).toHaveTextContent("true"); }); it("should be false when unhovered", async () => { - const { user, hoverTarget } = setup(); - await user.hover(hoverTarget); + const { hoverTarget } = setup(); + await fireEvent.mouseEnter(hoverTarget); expect(hoverTarget).toHaveTextContent("true"); - await user.unhover(hoverTarget); + await fireEvent.mouseLeave(hoverTarget); expect(hoverTarget).toHaveTextContent("false"); }); }); diff --git a/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte b/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte index f35b50a7..0d32a0ed 100644 --- a/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte +++ b/packages/runed/src/lib/utilities/IsHovered/TestIsHovered.svelte @@ -6,4 +6,6 @@ const isHovered = new IsHovered(() => el); -
{isHovered.current ? "true" : "false"}
+
+ {isHovered.current ? "true" : "false"} +
From 9315ce4b9753d6f5a4441ae954feffa36632a025 Mon Sep 17 00:00:00 2001 From: Jayden Carey Date: Mon, 8 Jul 2024 20:21:35 +0800 Subject: [PATCH 4/5] add changeset --- .changeset/empty-papayas-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/empty-papayas-wash.md diff --git a/.changeset/empty-papayas-wash.md b/.changeset/empty-papayas-wash.md new file mode 100644 index 00000000..6a52ef26 --- /dev/null +++ b/.changeset/empty-papayas-wash.md @@ -0,0 +1,5 @@ +--- +"runed": minor +--- + +add `IsHovered` From 21e6e822c20b348f145a98c3f10f432fd00aa82f Mon Sep 17 00:00:00 2001 From: Jayden Carey Date: Sat, 13 Jul 2024 02:23:04 +0800 Subject: [PATCH 5/5] use matchMedia to detect hover capability --- .../lib/utilities/IsHovered/IsHovered.svelte.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts index b6da5a04..efcdc99a 100644 --- a/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts +++ b/packages/runed/src/lib/utilities/IsHovered/IsHovered.svelte.ts @@ -3,6 +3,20 @@ import { extract } from "../extract/extract.js"; import type { MaybeGetter } from "$lib/internal/types.js"; import { addEventListener } from "$lib/internal/utils/event.js"; +// Whether the primary input device supports hover +function isHoverSupported() { + return window.matchMedia("(hover: hover)").matches; +} + +// Whether the primary input device supports fine pointer accuracy +function hasFinePointer() { + return window.matchMedia("(pointer: fine)").matches; +} + +function canHover() { + return isHoverSupported() && hasFinePointer(); +} + /** * Tracks whether the user is hovering over the target element. * @see {@link https://runed.dev/docs/utilities/is-hovered} @@ -23,7 +37,7 @@ export class IsHovered { }; $effect(() => { - if (!this.#target) { + if (!this.#target || !canHover()) { return; }