Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/scroll-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@solid-primitives/scroll": major
---

Migrate to Solid.js v2.0 (beta.10)

## Breaking Changes

**Peer dependency**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required.

### `@solid-primitives/scroll`

- `isServer` now imported from `@solidjs/web` (not `solid-js/web`)
- `onMount` replaced with `onSettled` for post-render position refresh
- `sharedConfig.context` replaced with `sharedConfig.hydrating` for hydration detection
- Internal signal pattern replaced: Solid 2.0's `createSignal(fn)` creates a derived signal rather than storing a function value; now uses a version counter to drive memo re-evaluation on scroll events
- Signal uses `{ ownedWrite: true }` to allow writes from DOM event handlers within reactive scopes
- Tests updated: `createComputed` removed (no longer in Solid 2.0), replaced with direct reactive reads and `flush()` for synchronous assertions; `createSignal` in tests uses `{ ownedWrite: true }`
- README: `onMount` example updated to `onSettled`
6 changes: 3 additions & 3 deletions packages/scroll/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ createEffect(() => {
```tsx
let ref: HTMLDivElement | undefined;

// pass as function
// pass as function — preferred, handles ref population automatically
const scroll = createScrollPosition(() => ref);
// or wrap with onMount
onMount(() => {
// or wrap with onSettled
onSettled(() => {
const scroll = createScrollPosition(ref!);
});

Expand Down
6 changes: 4 additions & 2 deletions packages/scroll/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@
"@solid-primitives/static-store": "workspace:^"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.10",
"solid-js": "^2.0.0-beta.10"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.10",
"solid-js": "2.0.0-beta.10"
}
}
40 changes: 23 additions & 17 deletions packages/scroll/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createEventListener } from "@solid-primitives/event-listener";
import { createHydratableSingletonRoot } from "@solid-primitives/rootless";
import { createDerivedStaticStore } from "@solid-primitives/static-store";
import { type Accessor, createSignal, onMount, sharedConfig } from "solid-js";
import { isServer } from "solid-js/web";
import { type Accessor, createSignal, onSettled, sharedConfig } from "solid-js";
import { isServer } from "@solidjs/web";

export function getScrollParent(node: Element | null): Element {
if (isServer) {
Expand Down Expand Up @@ -71,21 +71,27 @@ export function createScrollPosition(

target = target || window;

const isFn = typeof target === "function",
isHydrating = sharedConfig.context,
getTargetPos = isFn
? () => getScrollPosition((target as Extract<typeof target, Function>)())
: () => getScrollPosition(target as Element | Window),
// changing the calc signal will trigger the derived store to update
[calc, setCalc] = createSignal(isHydrating ? () => FALLBACK_SCROLL_POSITION : getTargetPos, {
equals: false,
}),
trigger = () => setCalc(() => getTargetPos),
pos = createDerivedStaticStore(() => calc()());

// update the position on mount if we are hydrating (initial pos is null)
// or if target is a function (which means it could be a ref that will be populated onMount)
if (isHydrating || isFn) onMount(trigger);
const isFn = typeof target === "function";
const isHydrating = sharedConfig.hydrating;

const getPos = (): Position =>
getScrollPosition(
isFn ? (target as Accessor<Element | Window | undefined>)() : (target as Element | Window),
);

// Plain integer counter — avoids Solid 2.0 createSignal(fn) derived-signal semantics.
// ownedWrite allows writes from DOM event handlers inside reactive scopes.
let tick = 1;
const [version, setVersion] = createSignal(0, { ownedWrite: true });
const trigger = () => void setVersion(tick++);

const pos = createDerivedStaticStore<Position>(() => {
const v = version();
return isHydrating && v === 0 ? { ...FALLBACK_SCROLL_POSITION } : getPos();
});

// Refs aren't populated until mount; also refreshes position post-hydration.
if (isHydrating || isFn) onSettled(trigger);

createEventListener(target, "scroll", trigger, { passive: true });

Expand Down
27 changes: 13 additions & 14 deletions packages/scroll/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createComputed, createRoot, createSignal } from "solid-js";
import { createRoot, createSignal, flush } from "solid-js";
import { describe, expect, it } from "vitest";

import { createScrollPosition, getScrollPosition } from "../src/index.js";
Expand All @@ -20,28 +20,25 @@ describe("getScrollPosition", () => {
describe("createScrollPosition", () => {
it("will observe scroll events", () =>
createRoot(dispose => {
const expectedX = [0, 100, 42];
const actualX: number[] = [];
const expectedY = [0, 34, 11];
const actualY: number[] = [];

const target = document.createElement("div");

const scroll = createScrollPosition(target);

createComputed(() => {
actualX.push(scroll.x);
actualY.push(scroll.y);
});
expect(scroll.x).toBe(0);
expect(scroll.y).toBe(0);

Object.assign(target, { scrollTop: 34, scrollLeft: 100 });
target.dispatchEvent(new Event("scroll"));
flush();

expect(scroll.x).toBe(100);
expect(scroll.y).toBe(34);

Object.assign(target, { scrollTop: 11, scrollLeft: 42 });
target.dispatchEvent(new Event("scroll"));
flush();

expect(actualX).toEqual(expectedX);
expect(actualY).toEqual(expectedY);
expect(scroll.x).toBe(42);
expect(scroll.y).toBe(11);

dispose();
}));
Expand All @@ -54,15 +51,17 @@ describe("createScrollPosition", () => {
const div2 = document.createElement("div");
Object.assign(div2, { scrollTop: 11, scrollLeft: 42 });

const [target, setTarget] = createSignal<Element | undefined>(div1);
const [target, setTarget] = createSignal<Element | undefined>(div1, { ownedWrite: true });

const scroll = createScrollPosition(target);
expect(scroll).toEqual({ x: 100, y: 34 });

setTarget(div2);
flush();
expect(scroll).toEqual({ x: 42, y: 11 });

setTarget();
flush();
expect(scroll).toEqual({ x: 0, y: 0 });

dispose();
Expand Down
7 changes: 5 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.