Skip to content
Open
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`
15 changes: 15 additions & 0 deletions packages/scroll/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ MIT License

Copyright (c) 2021 Solid Primitives Working Group

---

The `createPreventScroll` primitive is adapted from
[solid-prevent-scroll](https://github.com/corvudev/corvu/tree/main/packages/solid-prevent-scroll)
by Jasmin Noetzli (GiyoMoon), part of the [corvu](https://corvu.dev) project,
which is itself inspired by [react-remove-scroll](https://github.com/theKashey/react-remove-scroll)
by Anton Korzunov.

MIT License

Copyright (c) Jasmin Noetzli
Copyright (c) Anton Korzunov

---

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand Down
45 changes: 41 additions & 4 deletions packages/scroll/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
[![size](https://img.shields.io/npm/v/@solid-primitives/scroll?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/scroll)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)

Reactive primitives to react to element/window scrolling.
Reactive primitives to react to element/window scrolling, and to prevent scroll outside of a given element.

- [`createScrollPosition`](#createscrollposition) - Reactive primitive providing a store-like object with current scroll position of specified target.
- [`useWindowScrollPosition`](#usewindowscrollposition) - Returns a reactive object with current window scroll position.
- [`createPreventScroll`](#createpreventscroll) - Prevents scrolling outside of a given element.

## Installation

Expand Down Expand Up @@ -45,10 +46,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 Expand Up @@ -119,6 +120,42 @@ createEffect(() => {

Get an `{ x: number, y: number }` object of element/window scroll position.

## `createPreventScroll`

Prevents scrolling outside of the given element by intercepting `wheel` and `touchmove` events and optionally hiding the `<body>` scrollbar.

Adapted from [solid-prevent-scroll](https://github.com/corvudev/corvu/tree/main/packages/solid-prevent-scroll) by [Jasmin Noetzli (GiyoMoon)](https://github.com/GiyoMoon), part of the [corvu](https://corvu.dev) project, which is itself inspired by [react-remove-scroll](https://github.com/theKashey/react-remove-scroll) by Anton Korzunov. Adapted for Solid 2.0 and solid-primitives conventions.

### How to use it

```ts
import { createPreventScroll } from "@solid-primitives/scroll";

// Prevent all page scroll (no element specified)
createPreventScroll();

// Prevent scroll outside a specific element
createPreventScroll({ element: () => myElement });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should showcase using a signal ref.

createPreventScroll({ element: refAccessor });


// Reactive enabled toggle
const [open, setOpen] = createSignal(false);
createPreventScroll({ enabled: open });
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `element` | `MaybeAccessor<HTMLElement \| null>` | `null` | Allow scroll inside this element. Events outside it are cancelled. |
| `enabled` | `MaybeAccessor<boolean>` | `true` | Whether scroll prevention is active. |
| `hideScrollbar` | `MaybeAccessor<boolean>` | `true` | Hide the `<body>` scrollbar while active. |
| `preventScrollbarShift` | `MaybeAccessor<boolean>` | `true` | Compensate for the hidden scrollbar width to avoid layout shift. |
| `preventScrollbarShiftMode` | `MaybeAccessor<"padding" \| "margin">` | `"padding"` | Which CSS property to use for the scrollbar shift compensation. |
| `restoreScrollPosition` | `MaybeAccessor<boolean>` | `true` | Restore `<body>` scroll position via `window.scrollTo` when disabled. |
| `allowPinchZoom` | `MaybeAccessor<boolean>` | `false` | Allow two-finger pinch-zoom gestures. |

Multiple active instances are stacked; only the topmost one installs event listeners. Body styles are shared and only restored once all instances clean up.

## Primitive ideas:

_PRs Welcome :)_
Expand Down
27 changes: 21 additions & 6 deletions packages/scroll/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
{
"name": "@solid-primitives/scroll",
"version": "2.1.5",
"description": "Reactive primitives to react to element/window scrolling.",
"description": "Reactive primitives to react to element/window scrolling, and to prevent scroll outside of a given element.",
"author": "David Di Biase <dave@solidjs.com>",
"contributors": [
"Damian Tarnawski <gthetarnav@gmail.com>"
{
"name": "Damian Tarnawski",
"email": "gthetarnav@gmail.com"
},
{
"name": "Jasmin Noetzli",
"url": "https://github.com/GiyoMoon"
}
],
"license": "MIT",
"homepage": "https://primitives.solidjs.community/package/scroll",
Expand All @@ -17,7 +24,8 @@
"stage": 2,
"list": [
"createScrollPosition",
"useWindowScrollPosition"
"useWindowScrollPosition",
"createPreventScroll"
],
"category": "Inputs"
},
Expand Down Expand Up @@ -49,19 +57,26 @@
"monitor",
"scrollTo",
"scroll",
"prevent-scroll",
"scroll-lock",
"accessibility",
"a11y",
"solid",
"primitives"
],
"dependencies": {
"@solid-primitives/event-listener": "workspace:^",
"@solid-primitives/rootless": "workspace:^",
"@solid-primitives/static-store": "workspace:^"
"@solid-primitives/static-store": "workspace:^",
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.12",
"solid-js": "^2.0.0-beta.12"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.12",
"solid-js": "2.0.0-beta.12"
}
}
119 changes: 11 additions & 108 deletions packages/scroll/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,11 @@
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";

export function getScrollParent(node: Element | null): Element {
if (isServer) {
return {} as Element;
}
while (node && !isScrollable(node)) {
node = node.parentElement;
}

return node || document.scrollingElement || document.documentElement;
}

export function isScrollable(node: Element): boolean {
if (isServer) {
return false;
}
const style = window.getComputedStyle(node);
return /(auto|scroll)/.test(style.overflow + style.overflowX + style.overflowY);
}

export type Position = {
x: number;
y: number;
};

const FALLBACK_SCROLL_POSITION = { x: 0, y: 0 } as const satisfies Position;

/**
* Get an `{ x: number, y: number }` object of element/window scroll position.
*/
export function getScrollPosition(target: Element | Window | undefined): Position {
if (isServer || !target) {
return { ...FALLBACK_SCROLL_POSITION };
}
if (target instanceof Window)
return {
x: target.scrollX,
y: target.scrollY,
};
return {
x: target.scrollLeft,
y: target.scrollTop,
};
}

/**
* Reactive primitive providing a store-like object with current scroll position of specified target.
* @param target element/window to listen to scroll events. can be a reactive singal.
* @returns a store-like reactive object `{ x: number, y: number }` of current scroll position of {@link target}
* @example
* // target will be window by default
* const windowScroll = createScrollPosition();
*
* createEffect(() => {
* // returned object is a reactive store-like structure
* windowScroll.x; // => number
* windowScroll.y; // => number
* });
*/
export function createScrollPosition(
target?: Accessor<Element | Window | undefined> | Element | Window,
): Readonly<Position> {
if (isServer) {
return FALLBACK_SCROLL_POSITION;
}

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);

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

return pos;
}

/**
* Returns a reactive object with current window scroll position.
*
* This is a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) primitive.
*
* @example
* const scroll = useWindowScrollPosition();
* createEffect(() => {
* console.log(scroll.x, scroll.y)
* })
*/
export const useWindowScrollPosition = /*#__PURE__*/ createHydratableSingletonRoot(() =>
createScrollPosition(isServer ? () => undefined : window),
);
export {
getScrollParent,
isScrollable,
getScrollPosition,
createScrollPosition,
useWindowScrollPosition,
} from "./scrollPosition.js";
export type { Position } from "./scrollPosition.js";

export { createPreventScroll } from "./preventScroll.js";
export type { CreatePreventScrollProps } from "./preventScroll.js";
Loading