diff --git a/.changeset/cursor-solid2-migration.md b/.changeset/cursor-solid2-migration.md
new file mode 100644
index 000000000..c5a0427a8
--- /dev/null
+++ b/.changeset/cursor-solid2-migration.md
@@ -0,0 +1,19 @@
+---
+"@solid-primitives/cursor": major
+---
+
+Migrate to Solid.js v2.0 and add new primitives
+
+## Breaking Changes
+
+**Peer dependency**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required.
+
+- `isServer` now imported from `@solidjs/web` (not `solid-js/web`)
+- `createElementCursor` and `createBodyCursor` updated to the split compute/apply effect pattern required by Solid 2.0 — cleanup is returned from the apply phase instead of using `onCleanup`
+
+## New Exports
+
+- `makeBodyCursor(cursor)` — sets cursor on body immediately, returns a cleanup function
+- `makeElementCursor(target, cursor)` — sets cursor on an element immediately, returns a cleanup function
+- `createDragCursor(target, options?)` — reactively sets `"grab"` on a target element and switches to `"grabbing"` on the body during pointer drag
+- `cursorRef(cursor)` — ref factory for inline JSX use: `
`
diff --git a/packages/cursor/README.md b/packages/cursor/README.md
index e01d17576..9c8a752a4 100644
--- a/packages/cursor/README.md
+++ b/packages/cursor/README.md
@@ -8,10 +8,14 @@
[](https://www.npmjs.com/package/@solid-primitives/cursor)
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
-Two simple primitives for setting cursor css property reactively.
+Utilities for setting the CSS cursor property via reactive primitives (`createBodyCursor`, `createElementCursor`) and imperative APIs (`makeBodyCursor`, `makeElementCursor`).
-- [`createElementCursor`](#createelementcursor) - Set provided cursor to given HTML Element styles reactively.
-- [`createBodyCursor`](#createbodycursor) - Set selected cursor to body element styles reactively.
+- [`makeBodyCursor`](#makebodycursor) - Set cursor on body immediately; returns a cleanup function.
+- [`makeElementCursor`](#makeelementcursor) - Set cursor on an element immediately; returns a cleanup function.
+- [`createBodyCursor`](#createbodycursor) - Set cursor on body reactively.
+- [`createElementCursor`](#createelementcursor) - Set cursor on a specific element reactively.
+- [`createDragCursor`](#createdragcursor) - Show `grab`/`grabbing` cursors during pointer drag.
+- [`cursorRef`](#cursorref) - Ref factory for inline JSX use.
## Installation
@@ -23,14 +27,50 @@ yarn add @solid-primitives/cursor
pnpm add @solid-primitives/cursor
```
-## `createElementCursor`
+## `makeBodyCursor`
+
+Sets a cursor on the body element immediately and returns a cleanup function that restores the previous value. No reactive owner required.
+
+```ts
+import { makeBodyCursor } from "@solid-primitives/cursor";
+
+// Show a loading cursor during an async operation
+const restore = makeBodyCursor("wait");
+await doSomething();
+restore();
+```
+
+## `makeElementCursor`
+
+Sets a cursor on a specific element immediately and returns a cleanup function that restores the previous value. No reactive owner required.
+
+```ts
+import { makeElementCursor } from "@solid-primitives/cursor";
+
+const el = document.querySelector("#element")!;
+const restore = makeElementCursor(el, "not-allowed");
+// ... later
+restore();
+```
+
+## `createBodyCursor`
+
+Sets a cursor on the body element reactively. The cursor is removed when the owner is disposed or when the signal returns a falsy value.
+
+```ts
+import { createBodyCursor } from "@solid-primitives/cursor";
+
+const [cursor, setCursor] = createSignal("pointer");
+const [enabled, setEnabled] = createSignal(true);
+
+createBodyCursor(() => enabled() && cursor());
-Set provided cursor to given HTML Element styles reactively.
+setCursor("help");
+```
-It takes two arguments:
+## `createElementCursor`
-- `element` - HTMLElement or a reactive signal returning one. Returning falsy value will unset the cursor.
-- `cursor` - Cursor css property. E.g. "pointer", "grab", "zoom-in", "wait", etc.
+Sets a cursor on a specific element reactively. Accepts an element or a signal returning one — returning a falsy value unsets the cursor.
```ts
import { createElementCursor } from "@solid-primitives/cursor";
@@ -44,23 +84,39 @@ createElementCursor(() => enabled() && target, cursor);
setCursor("help");
```
-## `createBodyCursor`
+## `createDragCursor`
+
+Shows `"grab"` on a target element and switches to `"grabbing"` on the body during a pointer drag. Setting `"grabbing"` on the body ensures the cursor renders correctly everywhere during drag, not just over the target element.
+
+```ts
+import { createDragCursor } from "@solid-primitives/cursor";
-Set selected cursor to body element styles reactively.
+const [ref, setRef] = createSignal
();
-It takes only one argument:
+createDragCursor(ref);
-- `cursor` - Signal returing a cursor css property. E.g. "pointer", "grab", "zoom-in", "wait", etc. Returning falsy value will unset the cursor.
+Drag me
+```
+
+Custom cursor values can be provided via options:
```ts
-import { createBodyCursor } from "@solid-primitives/cursor";
+createDragCursor(el, { grab: "crosshair", grabbing: "move" });
+```
-const [cursor, setCursor] = createSignal("pointer");
-const [enabled, setEnabled] = createSignal(true);
+## `cursorRef`
-createBodyCursor(() => enabled() && cursor());
+A ref factory for setting a cursor inline in JSX. Accepts a static cursor value or a reactive signal. The cursor is removed when the component unmounts.
-setCursor("help");
+```tsx
+import { cursorRef } from "@solid-primitives/cursor";
+
+// Static
+...
;
+
+// Reactive
+const [cursor, setCursor] = createSignal("pointer");
+...
;
```
## Changelog
diff --git a/packages/cursor/package.json b/packages/cursor/package.json
index ca52fabee..3527088ee 100644
--- a/packages/cursor/package.json
+++ b/packages/cursor/package.json
@@ -1,7 +1,7 @@
{
"name": "@solid-primitives/cursor",
- "version": "0.1.3",
- "description": "Two simple primitives for setting cursor css property reactively.",
+ "version": "0.2.0",
+ "description": "Primitives for setting CSS cursor property reactively.",
"author": "Damian Tarnawski ",
"contributors": [],
"license": "MIT",
@@ -17,8 +17,12 @@
"name": "cursor",
"stage": 0,
"list": [
+ "makeBodyCursor",
+ "makeElementCursor",
+ "createBodyCursor",
"createElementCursor",
- "createBodyCursor"
+ "createDragCursor",
+ "cursorRef"
],
"category": "Utilities"
},
@@ -55,10 +59,12 @@
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
- "solid-js": "^1.6.12"
+ "@solidjs/web": "^2.0.0-beta.14",
+ "solid-js": "^2.0.0-beta.14"
},
"typesVersions": {},
"devDependencies": {
- "solid-js": "^1.9.7"
+ "@solidjs/web": "2.0.0-beta.14",
+ "solid-js": "2.0.0-beta.14"
}
}
diff --git a/packages/cursor/src/index.ts b/packages/cursor/src/index.ts
index 6420521bb..e290589f3 100644
--- a/packages/cursor/src/index.ts
+++ b/packages/cursor/src/index.ts
@@ -1,47 +1,55 @@
-import { type Accessor, createEffect, onCleanup } from "solid-js";
-import { isServer } from "solid-js/web";
-import { access, type FalsyValue, type MaybeAccessor } from "@solid-primitives/utils";
+import { type Accessor, createEffect, createSignal } from "solid-js";
+import { isServer } from "@solidjs/web";
+import { access, noop, type FalsyValue, type MaybeAccessor } from "@solid-primitives/utils";
+import { type CursorProperty } from "./types.js";
-export type CursorProperty =
- | "-moz-grab"
- | "-webkit-grab"
- | "alias"
- | "all-scroll"
- | "auto"
- | "cell"
- | "col-resize"
- | "context-menu"
- | "copy"
- | "crosshair"
- | "default"
- | "e-resize"
- | "ew-resize"
- | "grab"
- | "grabbing"
- | "help"
- | "move"
- | "n-resize"
- | "ne-resize"
- | "nesw-resize"
- | "no-drop"
- | "none"
- | "not-allowed"
- | "ns-resize"
- | "nw-resize"
- | "nwse-resize"
- | "pointer"
- | "progress"
- | "row-resize"
- | "s-resize"
- | "se-resize"
- | "sw-resize"
- | "text"
- | "vertical-text"
- | "w-resize"
- | "wait"
- | "zoom-in"
- | "zoom-out"
- | (string & {});
+export type { CursorProperty };
+
+/**
+ * Set selected {@link cursor} to body element styles immediately.
+ *
+ * Returns a cleanup function that restores the previous cursor.
+ *
+ * @param cursor Cursor css property. E.g. "pointer", "grab", "zoom-in", "wait", etc.
+ *
+ * @example
+ * ```ts
+ * const restore = makeBodyCursor("wait");
+ * // ... async operation ...
+ * restore();
+ * ```
+ */
+export function makeBodyCursor(cursor: CursorProperty): VoidFunction {
+ if (isServer) return noop;
+ return makeElementCursor(document.body, cursor);
+}
+
+/**
+ * Set selected {@link cursor} to {@link target} element styles immediately.
+ *
+ * Returns a cleanup function that restores the previous cursor.
+ *
+ * @param target HTMLElement to set the cursor on.
+ * @param cursor Cursor css property. E.g. "pointer", "grab", "zoom-in", "wait", etc.
+ *
+ * @example
+ * ```ts
+ * const restore = makeElementCursor(el, "wait");
+ * // ... async operation ...
+ * restore();
+ * ```
+ */
+export function makeElementCursor(target: HTMLElement, cursor: CursorProperty): VoidFunction {
+ if (isServer) return noop;
+ const prevValue = target.style.getPropertyValue("cursor");
+ const prevPriority = target.style.getPropertyPriority("cursor");
+ target.style.setProperty("cursor", cursor, "important");
+ return () => {
+ prevValue
+ ? target.style.setProperty("cursor", prevValue, prevPriority)
+ : target.style.removeProperty("cursor");
+ };
+}
/**
* Set selected {@link cursor} to {@link target} styles reactively.
@@ -66,14 +74,26 @@ export function createElementCursor(
): void {
if (isServer) return;
- createEffect(() => {
- const el = access(target);
- const cursorValue = access(cursor);
+ type State = { el: HTMLElement | FalsyValue; cursorValue: CursorProperty };
+
+ const compute = (): State => ({
+ el: access(target),
+ cursorValue: access(cursor),
+ });
+
+ const apply = ({ el, cursorValue }: State) => {
if (!el) return;
- const overwritten = el.style.cursor;
+ const prevValue = el.style.getPropertyValue("cursor");
+ const prevPriority = el.style.getPropertyPriority("cursor");
el.style.setProperty("cursor", cursorValue, "important");
- onCleanup(() => (el.style.cursor = overwritten));
- });
+ return () => {
+ prevValue
+ ? el.style.setProperty("cursor", prevValue, prevPriority)
+ : el.style.removeProperty("cursor");
+ };
+ };
+
+ createEffect(compute, apply);
}
/**
@@ -94,11 +114,93 @@ export function createElementCursor(
export function createBodyCursor(cursor: Accessor): void {
if (isServer) return;
- createEffect(() => {
- const cursorValue = cursor();
+ createEffect(cursor, cursorValue => {
if (!cursorValue) return;
- const overwritten = document.body.style.cursor;
+ const prevValue = document.body.style.getPropertyValue("cursor");
+ const prevPriority = document.body.style.getPropertyPriority("cursor");
document.body.style.setProperty("cursor", cursorValue, "important");
- onCleanup(() => (document.body.style.cursor = overwritten));
+ return () => {
+ prevValue
+ ? document.body.style.setProperty("cursor", prevValue, prevPriority)
+ : document.body.style.removeProperty("cursor");
+ };
});
}
+
+/**
+ * Reactively sets "grab" cursor on {@link target} and switches to "grabbing" on the body during drag.
+ *
+ * Setting "grabbing" on the body ensures the cursor renders correctly everywhere during drag,
+ * not just over the target element.
+ *
+ * @param target HTMLElement or a reactive signal returning one. Returning falsy value will disable the cursor.
+ * @param options Optional overrides for the grab and grabbing cursor values.
+ *
+ * @example
+ * ```ts
+ * const [ref, setRef] = createSignal();
+ *
+ * createDragCursor(ref);
+ *
+ * Drag me
+ * ```
+ */
+export function createDragCursor(
+ target: Accessor | HTMLElement,
+ options?: { grab?: CursorProperty; grabbing?: CursorProperty },
+): void {
+ if (isServer) return;
+
+ const grab = options?.grab ?? "grab";
+ const grabbing = options?.grabbing ?? "grabbing";
+ const [dragging, setDragging] = createSignal(false);
+
+ // During drag, "grabbing" is set on body so it shows globally.
+ // "grab" is cleared from the element so the body cursor can inherit through —
+ // element inline styles (even without !important) would otherwise win over body.
+ createBodyCursor(() => dragging() && grabbing);
+ createElementCursor(() => {
+ const el = access(target);
+ return dragging() ? false : el;
+ }, grab);
+
+ createEffect(
+ () => access(target),
+ el => {
+ if (!el) {
+ setDragging(false);
+ return;
+ }
+ const onDown = () => setDragging(true);
+ const onUp = () => setDragging(false);
+ el.addEventListener("pointerdown", onDown);
+ document.addEventListener("pointerup", onUp);
+ document.addEventListener("pointercancel", onUp);
+ return () => {
+ el.removeEventListener("pointerdown", onDown);
+ document.removeEventListener("pointerup", onUp);
+ document.removeEventListener("pointercancel", onUp);
+ };
+ },
+ );
+}
+
+/**
+ * Returns a ref callback that sets a cursor on the element it is attached to.
+ *
+ * Accepts a static cursor value or a reactive signal. The cursor is removed when the
+ * component unmounts.
+ *
+ * @example
+ * ```tsx
+ * // static
+ * ...
+ *
+ * // reactive
+ * const [cursor, setCursor] = createSignal("pointer");
+ * ...
+ * ```
+ */
+export function cursorRef(cursor: MaybeAccessor): (el: HTMLElement) => void {
+ return el => createElementCursor(el, cursor);
+}
diff --git a/packages/cursor/src/types.ts b/packages/cursor/src/types.ts
new file mode 100644
index 000000000..56e727df9
--- /dev/null
+++ b/packages/cursor/src/types.ts
@@ -0,0 +1,40 @@
+export type CursorProperty =
+ | "-moz-grab"
+ | "-webkit-grab"
+ | "alias"
+ | "all-scroll"
+ | "auto"
+ | "cell"
+ | "col-resize"
+ | "context-menu"
+ | "copy"
+ | "crosshair"
+ | "default"
+ | "e-resize"
+ | "ew-resize"
+ | "grab"
+ | "grabbing"
+ | "help"
+ | "move"
+ | "n-resize"
+ | "ne-resize"
+ | "nesw-resize"
+ | "no-drop"
+ | "none"
+ | "not-allowed"
+ | "ns-resize"
+ | "nw-resize"
+ | "nwse-resize"
+ | "pointer"
+ | "progress"
+ | "row-resize"
+ | "s-resize"
+ | "se-resize"
+ | "sw-resize"
+ | "text"
+ | "vertical-text"
+ | "w-resize"
+ | "wait"
+ | "zoom-in"
+ | "zoom-out"
+ | (string & {});
diff --git a/packages/cursor/test/index.test.ts b/packages/cursor/test/index.test.ts
index 019cb787c..8d3fff1ef 100644
--- a/packages/cursor/test/index.test.ts
+++ b/packages/cursor/test/index.test.ts
@@ -1,6 +1,54 @@
import { describe, test, expect } from "vitest";
-import { createRoot, createSignal } from "solid-js";
-import { createBodyCursor, createElementCursor, CursorProperty } from "../src/index.js";
+import { createRoot, createSignal, flush } from "solid-js";
+import {
+ createBodyCursor,
+ createDragCursor,
+ createElementCursor,
+ cursorRef,
+ makeBodyCursor,
+ makeElementCursor,
+ type CursorProperty,
+} from "../src/index.js";
+
+describe("makeBodyCursor", () => {
+ test("sets cursor and returns cleanup", () => {
+ const restore = makeBodyCursor("pointer");
+ expect(document.body.style.cursor).toBe("pointer");
+ restore();
+ expect(document.body.style.cursor).toBe("");
+ });
+
+ test("restores nested cursors in stack order", () => {
+ const restore1 = makeBodyCursor("pointer");
+ const restore2 = makeBodyCursor("help");
+ expect(document.body.style.cursor).toBe("help");
+ restore2();
+ expect(document.body.style.cursor).toBe("pointer");
+ restore1();
+ expect(document.body.style.cursor).toBe("");
+ });
+});
+
+describe("makeElementCursor", () => {
+ test("sets cursor on element and returns cleanup", () => {
+ const el = document.createElement("div");
+ const restore = makeElementCursor(el, "pointer");
+ expect(el.style.cursor).toBe("pointer");
+ restore();
+ expect(el.style.cursor).toBe("");
+ });
+
+ test("restores previous cursor value", () => {
+ const el = document.createElement("div");
+ const restore1 = makeElementCursor(el, "pointer");
+ const restore2 = makeElementCursor(el, "help");
+ expect(el.style.cursor).toBe("help");
+ restore2();
+ expect(el.style.cursor).toBe("pointer");
+ restore1();
+ expect(el.style.cursor).toBe("");
+ });
+});
describe("createBodyCursor", () => {
test("switches previous cursor to provided one", () => {
@@ -11,16 +59,20 @@ describe("createBodyCursor", () => {
createBodyCursor(() => enabled() && cursor());
return dispose;
});
+ flush();
expect(document.body.style.cursor).toBe("pointer");
setCursor("help");
+ flush();
expect(document.body.style.cursor).toBe("help");
setEnabled(false);
+ flush();
expect(document.body.style.cursor, "unsets cursor").toBe("");
setEnabled(true);
+ flush();
expect(document.body.style.cursor).toBe("help");
dispose();
@@ -38,16 +90,20 @@ describe("createElementCursor", () => {
createElementCursor(() => enabled() && div, cursor);
return dispose;
});
+ flush();
expect(div.style.cursor).toBe("pointer");
setCursor("help");
+ flush();
expect(div.style.cursor).toBe("help");
setEnabled(false);
+ flush();
expect(div.style.cursor, "unsets cursor").toBe("");
setEnabled(true);
+ flush();
expect(div.style.cursor).toBe("help");
dispose();
@@ -65,28 +121,35 @@ describe("createElementCursor", () => {
createElementCursor(() => enabled() && target(), cursor);
return dispose;
});
+ flush();
expect(div1.style.cursor).toBe("pointer");
setCursor("help");
+ flush();
expect(div1.style.cursor).toBe("help");
setTarget(div2);
+ flush();
expect(div1.style.cursor).toBe("");
expect(div2.style.cursor).toBe("help");
setEnabled(false);
+ flush();
expect(div2.style.cursor).toBe("");
setTarget(div1);
+ flush();
expect(div1.style.cursor).toBe("");
expect(div2.style.cursor).toBe("");
setCursor("pointer");
+ flush();
expect(div1.style.cursor).toBe("");
expect(div2.style.cursor).toBe("");
setEnabled(true);
+ flush();
expect(div1.style.cursor).toBe("pointer");
expect(div2.style.cursor).toBe("");
@@ -94,3 +157,129 @@ describe("createElementCursor", () => {
expect(div1.style.cursor).toBe("");
});
});
+
+describe("createDragCursor", () => {
+ test("shows grab/grabbing cursors during drag", () => {
+ const el = document.createElement("div");
+
+ const dispose = createRoot(dispose => {
+ createDragCursor(el);
+ return dispose;
+ });
+ flush();
+
+ expect(el.style.cursor).toBe("grab");
+ expect(document.body.style.cursor).toBe("");
+
+ el.dispatchEvent(new Event("pointerdown"));
+ flush();
+ expect(document.body.style.cursor).toBe("grabbing");
+ expect(el.style.cursor).toBe("");
+
+ document.dispatchEvent(new Event("pointerup"));
+ flush();
+ expect(document.body.style.cursor).toBe("");
+ expect(el.style.cursor).toBe("grab");
+
+ dispose();
+ expect(el.style.cursor).toBe("");
+ });
+
+ test("resets on pointercancel", () => {
+ const el = document.createElement("div");
+
+ const dispose = createRoot(dispose => {
+ createDragCursor(el);
+ return dispose;
+ });
+ flush();
+
+ el.dispatchEvent(new Event("pointerdown"));
+ flush();
+ expect(document.body.style.cursor).toBe("grabbing");
+
+ document.dispatchEvent(new Event("pointercancel"));
+ flush();
+ expect(document.body.style.cursor).toBe("");
+ expect(el.style.cursor).toBe("grab");
+
+ dispose();
+ });
+
+ test("supports custom cursor values", () => {
+ const el = document.createElement("div");
+
+ const dispose = createRoot(dispose => {
+ createDragCursor(el, { grab: "crosshair", grabbing: "move" });
+ return dispose;
+ });
+ flush();
+
+ expect(el.style.cursor).toBe("crosshair");
+
+ el.dispatchEvent(new Event("pointerdown"));
+ flush();
+ expect(document.body.style.cursor).toBe("move");
+
+ document.dispatchEvent(new Event("pointerup"));
+ flush();
+ expect(el.style.cursor).toBe("crosshair");
+
+ dispose();
+ });
+
+ test("cleans up listeners and cursors on dispose", () => {
+ const el = document.createElement("div");
+
+ const dispose = createRoot(dispose => {
+ createDragCursor(el);
+ return dispose;
+ });
+ flush();
+
+ dispose();
+ expect(el.style.cursor).toBe("");
+ expect(document.body.style.cursor).toBe("");
+
+ el.dispatchEvent(new Event("pointerdown"));
+ flush();
+ expect(document.body.style.cursor).toBe("");
+ });
+});
+
+describe("cursorRef", () => {
+ test("applies cursor to element", () => {
+ const el = document.createElement("div");
+
+ const dispose = createRoot(dispose => {
+ cursorRef("pointer")(el);
+ return dispose;
+ });
+ flush();
+
+ expect(el.style.cursor).toBe("pointer");
+
+ dispose();
+ expect(el.style.cursor).toBe("");
+ });
+
+ test("reacts to cursor signal changes", () => {
+ const el = document.createElement("div");
+ const [cursor, setCursor] = createSignal("pointer");
+
+ const dispose = createRoot(dispose => {
+ cursorRef(cursor)(el);
+ return dispose;
+ });
+ flush();
+
+ expect(el.style.cursor).toBe("pointer");
+
+ setCursor("help");
+ flush();
+ expect(el.style.cursor).toBe("help");
+
+ dispose();
+ expect(el.style.cursor).toBe("");
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ee1923c5f..c274838a0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -233,9 +233,12 @@ importers:
specifier: workspace:^
version: link:../utils
devDependencies:
+ '@solidjs/web':
+ specifier: 2.0.0-beta.14
+ version: 2.0.0-beta.14(solid-js@2.0.0-beta.14)
solid-js:
- specifier: ^1.9.7
- version: 1.9.7
+ specifier: 2.0.0-beta.14
+ version: 2.0.0-beta.14
packages/date:
dependencies: