From 4cb6ec9a57affbfdfc32e0f0b3ab1602a5331d0d Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 3 May 2026 12:40:53 -0400 Subject: [PATCH 1/4] Adapt to Solid 2.0 beta 10 --- .changeset/scroll-solid2-migration.md | 19 +++++++++++++ packages/scroll/README.md | 6 ++-- packages/scroll/package.json | 6 ++-- packages/scroll/src/index.ts | 40 +++++++++++++++------------ packages/scroll/test/index.test.ts | 27 +++++++++--------- pnpm-lock.yaml | 7 +++-- 6 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 .changeset/scroll-solid2-migration.md diff --git a/.changeset/scroll-solid2-migration.md b/.changeset/scroll-solid2-migration.md new file mode 100644 index 000000000..094a0b00f --- /dev/null +++ b/.changeset/scroll-solid2-migration.md @@ -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` diff --git a/packages/scroll/README.md b/packages/scroll/README.md index ed2ed67ac..4a83147ae 100644 --- a/packages/scroll/README.md +++ b/packages/scroll/README.md @@ -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!); }); diff --git a/packages/scroll/package.json b/packages/scroll/package.json index 130c6d923..8406cb779 100644 --- a/packages/scroll/package.json +++ b/packages/scroll/package.json @@ -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" } } diff --git a/packages/scroll/src/index.ts b/packages/scroll/src/index.ts index 92f44ef32..055e02e36 100644 --- a/packages/scroll/src/index.ts +++ b/packages/scroll/src/index.ts @@ -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) { @@ -71,21 +71,27 @@ export function createScrollPosition( target = target || window; - const isFn = typeof target === "function", - isHydrating = sharedConfig.context, - getTargetPos = isFn - ? () => getScrollPosition((target as Extract)()) - : () => 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)() : (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(() => { + 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 }); diff --git a/packages/scroll/test/index.test.ts b/packages/scroll/test/index.test.ts index 7c8534a46..9df9cf771 100644 --- a/packages/scroll/test/index.test.ts +++ b/packages/scroll/test/index.test.ts @@ -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"; @@ -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(); })); @@ -54,15 +51,17 @@ describe("createScrollPosition", () => { const div2 = document.createElement("div"); Object.assign(div2, { scrollTop: 11, scrollLeft: 42 }); - const [target, setTarget] = createSignal(div1); + const [target, setTarget] = createSignal(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(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f59c63a9c..21dfe127b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -829,9 +829,12 @@ importers: specifier: workspace:^ version: link:../static-store devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/selection: devDependencies: From 63f2c4d6bfa74cf09fbcedd825921afa5a041d2c Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Wed, 13 May 2026 11:11:54 -0400 Subject: [PATCH 2/4] Add prevent-scroll from corvu --- packages/scroll/LICENSE | 15 + packages/scroll/README.md | 39 ++- packages/scroll/package.json | 29 +- packages/scroll/src/index.ts | 125 +------- packages/scroll/src/preventScroll.ts | 323 +++++++++++++++++++ packages/scroll/src/scrollPosition.ts | 114 +++++++ packages/scroll/test/index.test.ts | 178 ++++++++++- packages/scroll/test/server.test.ts | 14 +- packages/scroll/tsconfig.json | 3 + packages/static-store/package.json | 8 +- packages/static-store/src/index.ts | 40 +-- packages/utils/package.json | 8 +- packages/utils/src/index.ts | 36 ++- packages/utils/src/types.ts | 2 +- pnpm-lock.yaml | 435 ++++++++++++++------------ 15 files changed, 997 insertions(+), 372 deletions(-) create mode 100644 packages/scroll/src/preventScroll.ts create mode 100644 packages/scroll/src/scrollPosition.ts diff --git a/packages/scroll/LICENSE b/packages/scroll/LICENSE index 38b41d975..303b92f6b 100644 --- a/packages/scroll/LICENSE +++ b/packages/scroll/LICENSE @@ -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 diff --git a/packages/scroll/README.md b/packages/scroll/README.md index 4a83147ae..419314639 100644 --- a/packages/scroll/README.md +++ b/packages/scroll/README.md @@ -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 @@ -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 `` 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 }); + +// Reactive enabled toggle +const [open, setOpen] = createSignal(false); +createPreventScroll({ enabled: open }); +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `element` | `MaybeAccessor` | `null` | Allow scroll inside this element. Events outside it are cancelled. | +| `enabled` | `MaybeAccessor` | `true` | Whether scroll prevention is active. | +| `hideScrollbar` | `MaybeAccessor` | `true` | Hide the `` scrollbar while active. | +| `preventScrollbarShift` | `MaybeAccessor` | `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` | `true` | Restore `` scroll position via `window.scrollTo` when disabled. | +| `allowPinchZoom` | `MaybeAccessor` | `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 :)_ diff --git a/packages/scroll/package.json b/packages/scroll/package.json index 8406cb779..f1e87299c 100644 --- a/packages/scroll/package.json +++ b/packages/scroll/package.json @@ -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 ", "contributors": [ - "Damian Tarnawski " + { + "name": "Damian Tarnawski", + "email": "gthetarnav@gmail.com" + }, + { + "name": "Jasmin Noetzli", + "url": "https://github.com/GiyoMoon" + } ], "license": "MIT", "homepage": "https://primitives.solidjs.community/package/scroll", @@ -17,7 +24,8 @@ "stage": 2, "list": [ "createScrollPosition", - "useWindowScrollPosition" + "useWindowScrollPosition", + "createPreventScroll" ], "category": "Inputs" }, @@ -49,21 +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": { - "@solidjs/web": "^2.0.0-beta.10", - "solid-js": "^2.0.0-beta.10" + "@solidjs/web": "^2.0.0-beta.12", + "solid-js": "^2.0.0-beta.12" }, "typesVersions": {}, "devDependencies": { - "@solidjs/web": "2.0.0-beta.10", - "solid-js": "2.0.0-beta.10" + "@solidjs/web": "2.0.0-beta.12", + "solid-js": "2.0.0-beta.12" } } diff --git a/packages/scroll/src/index.ts b/packages/scroll/src/index.ts index 055e02e36..92695036b 100644 --- a/packages/scroll/src/index.ts +++ b/packages/scroll/src/index.ts @@ -1,114 +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, onSettled, sharedConfig } from "solid-js"; -import { isServer } from "@solidjs/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, -): Readonly { - if (isServer) { - return FALLBACK_SCROLL_POSITION; - } - - target = target || window; - - const isFn = typeof target === "function"; - const isHydrating = sharedConfig.hydrating; - - const getPos = (): Position => - getScrollPosition( - isFn ? (target as Accessor)() : (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(() => { - 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 }); - - 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"; diff --git a/packages/scroll/src/preventScroll.ts b/packages/scroll/src/preventScroll.ts new file mode 100644 index 000000000..76d12b57a --- /dev/null +++ b/packages/scroll/src/preventScroll.ts @@ -0,0 +1,323 @@ +/**! + * Part of this code is adapted from solid-prevent-scroll by Jasmin Noetzli (GiyoMoon), + * part of the corvu project. Adapted for Solid 2.0 and solid-primitives conventions. + * MIT License, Copyright (c) Jasmin Noetzli + * + * https://github.com/corvudev/corvu/tree/main/packages/solid-prevent-scroll + * + * Part of this code is inspired by react-remove-scroll. + * MIT License, Copyright (c) Anton Korzunov + * https://github.com/theKashey/react-remove-scroll + */ + +import { createEffect, createSignal } from "solid-js"; +import { isServer } from "@solidjs/web"; +import { contains, INTERNAL_OPTIONS, access, type MaybeAccessor } from "@solid-primitives/utils"; + +type Axis = "x" | "y"; + +export type CreatePreventScrollProps = { + /** Element that is allowed to scroll. Wheel/touch events inside it are passed through. *Default = `null`* */ + element?: MaybeAccessor; + /** Whether scroll prevention is active. *Default = `true`* */ + enabled?: MaybeAccessor; + /** Hide the `` scrollbar while active. *Default = `true`* */ + hideScrollbar?: MaybeAccessor; + /** Add padding/margin to `` to compensate for the hidden scrollbar. *Default = `true`* */ + preventScrollbarShift?: MaybeAccessor; + /** Whether to use `padding` or `margin` for the scrollbar shift compensation. *Default = `"padding"`* */ + preventScrollbarShiftMode?: MaybeAccessor<"padding" | "margin">; + /** Restore `` scroll position via `window.scrollTo` when disabled to avoid layout shift. *Default = `true`* */ + restoreScrollPosition?: MaybeAccessor; + /** Allow two-finger pinch-zoom gestures. *Default = `false`* */ + allowPinchZoom?: MaybeAccessor; +}; + +// ─── Module-level stack ─────────────────────────────────────────────────────── +// Tracks active instances; only the topmost one installs wheel/touch handlers. + +const [preventScrollStack, setPreventScrollStack] = createSignal([], INTERNAL_OPTIONS); + +const isActive = (id: string): boolean => { + const stack = preventScrollStack(); + return stack.length > 0 && stack[stack.length - 1] === id; +}; + +// ─── Body style tracker ─────────────────────────────────────────────────────── +// Multiple nested instances share a key; the original styles are only restored +// once the last instance cleans up. + +type ActiveStyle = { + activeCount: number; + originalStyles: Partial; + properties: string[]; +}; + +const activeBodyStyles = new Map(); + +function applyBodyStyle( + key: string, + element: HTMLElement, + style: Partial, + properties: { key: string; value: string }[], +): () => void { + const originalStyles: Partial = {}; + for (const k in style) { + originalStyles[k] = element.style[k as keyof CSSStyleDeclaration] as string; + } + + const existing = activeBodyStyles.get(key); + if (existing) { + existing.activeCount++; + } else { + activeBodyStyles.set(key, { + activeCount: 1, + originalStyles, + properties: properties.map(p => p.key), + }); + } + + Object.assign(element.style, style); + for (const prop of properties) { + element.style.setProperty(prop.key, prop.value); + } + + return () => { + const active = activeBodyStyles.get(key); + if (!active) return; + if (active.activeCount !== 1) { + active.activeCount--; + return; + } + activeBodyStyles.delete(key); + + for (const [k, v] of Object.entries(active.originalStyles)) { + (element.style as any)[k] = v; + } + for (const prop of active.properties) { + element.style.removeProperty(prop); + } + if (element.style.length === 0) { + element.removeAttribute("style"); + } + }; +} + +// ─── Scroll helpers ─────────────────────────────────────────────────────────── + +function getScrollDimensions(element: HTMLElement, axis: Axis): [number, number, number] { + return axis === "x" + ? [element.clientWidth, element.scrollLeft, element.scrollWidth] + : [element.clientHeight, element.scrollTop, element.scrollHeight]; +} + +function isScrollContainer(element: HTMLElement, axis: Axis): boolean { + const styles = getComputedStyle(element); + const overflow = axis === "x" ? styles.overflowX : styles.overflowY; + return ( + overflow === "auto" || + overflow === "scroll" || + (element.tagName === "HTML" && overflow === "visible") + ); +} + +function getScrollAtLocation( + location: HTMLElement, + axis: Axis, + stopAt?: HTMLElement, +): [number, number] { + const directionFactor = + axis === "x" && window.getComputedStyle(location).direction === "rtl" ? -1 : 1; + + let currentElement: HTMLElement | null = location; + let availableScroll = 0; + let availableScrollTop = 0; + let wrapperReached = false; + + do { + const [clientSize, scrollOffset, scrollSize] = getScrollDimensions(currentElement, axis); + const scrolled = scrollSize - clientSize - directionFactor * scrollOffset; + + if ((scrollOffset !== 0 || scrolled !== 0) && isScrollContainer(currentElement, axis)) { + availableScroll += scrolled; + availableScrollTop += scrollOffset; + } + + if (currentElement === (stopAt ?? document.documentElement)) { + wrapperReached = true; + } else { + // @ts-expect-error: _$host is a SolidJS-internal property set on portal roots + currentElement = currentElement._$host ?? currentElement.parentElement; + } + } while (currentElement && !wrapperReached); + + return [availableScroll, availableScrollTop]; +} + +function wouldScroll( + target: HTMLElement, + axis: Axis, + delta: number, + wrapper: HTMLElement | null, +): boolean { + const targetInWrapper = wrapper !== null && contains(wrapper, target); + const [availableScroll, availableScrollTop] = getScrollAtLocation( + target, + axis, + targetInWrapper ? wrapper : undefined, + ); + // Firefox can report availableScroll as 1 even when no scroll is possible. + if (delta > 0 && Math.abs(availableScroll) <= 1) return false; + if (delta < 0 && Math.abs(availableScrollTop) < 1) return false; + return true; +} + +// ─── Primitive ──────────────────────────────────────────────────────────────── + +let _nextId = 0; + +/** + * Prevents scrolling outside of the given element. + * + * Adapted from [solid-prevent-scroll](https://github.com/corvudev/corvu/tree/main/packages/solid-prevent-scroll) + * by Jasmin Noetzli (GiyoMoon), part of the corvu project. + */ +export const createPreventScroll = (props: CreatePreventScrollProps = {}): void => { + if (isServer) return; + + const id = String(_nextId++); + + let currentTouchStart: [number, number] = [0, 0]; + let currentTouchStartAxis: Axis | null = null; + let currentTouchStartDelta: number | null = null; + + // 1. Manage the active-instance stack. + createEffect( + () => ({ enabled: access(props.enabled) ?? true }), + ({ enabled }) => { + if (!enabled) return; + setPreventScrollStack(stack => [...stack, id]); + return () => setPreventScrollStack(stack => stack.filter(s => s !== id)); + }, + ); + + // 2. Apply body overflow styles. + createEffect( + () => ({ + enabled: access(props.enabled) ?? true, + hideScrollbar: access(props.hideScrollbar) ?? true, + preventScrollbarShift: access(props.preventScrollbarShift) ?? true, + preventScrollbarShiftMode: access(props.preventScrollbarShiftMode) ?? "padding", + restoreScrollPosition: access(props.restoreScrollPosition) ?? true, + }), + ({ enabled, hideScrollbar, preventScrollbarShift, preventScrollbarShiftMode, restoreScrollPosition }) => { + if (!enabled || !hideScrollbar) return; + + const { body } = document; + const scrollbarWidth = window.innerWidth - body.offsetWidth; + const offsetTop = window.scrollY; + const offsetLeft = window.scrollX; + + const style: Partial = { overflow: "hidden" }; + const properties: { key: string; value: string }[] = []; + + if (preventScrollbarShift && scrollbarWidth > 0) { + if (preventScrollbarShiftMode === "padding") { + style.paddingRight = `calc(${window.getComputedStyle(body).paddingRight} + ${scrollbarWidth}px)`; + } else { + style.marginRight = `calc(${window.getComputedStyle(body).marginRight} + ${scrollbarWidth}px)`; + } + properties.push({ key: "--scrollbar-width", value: `${scrollbarWidth}px` }); + } + + const restoreStyle = applyBodyStyle("prevent-scroll", body, style, properties); + + return () => { + restoreStyle(); + if (restoreScrollPosition && scrollbarWidth > 0) { + window.scrollTo(offsetLeft, offsetTop); + } + }; + }, + ); + + // 3. Install wheel/touch event listeners (only when this is the topmost instance). + createEffect( + () => ({ + active: isActive(id), + enabled: access(props.enabled) ?? true, + wrapper: access(props.element) ?? null, + allowPinchZoom: access(props.allowPinchZoom) ?? false, + }), + ({ active, enabled, wrapper, allowPinchZoom }) => { + if (!active || !enabled) return; + + const maybePreventWheel = (event: WheelEvent) => { + const target = event.target as HTMLElement; + const delta: [number, number] = [event.deltaX, event.deltaY]; + const axis: Axis = Math.abs(delta[0]) > Math.abs(delta[1]) ? "x" : "y"; + const axisDelta = axis === "x" ? delta[0] : delta[1]; + + const shouldCancel = + wrapper && contains(wrapper, target) + ? !wouldScroll(target, axis, axisDelta, wrapper) + : true; + + if (shouldCancel && event.cancelable) event.preventDefault(); + }; + + const logTouchStart = (event: TouchEvent) => { + const touch = event.changedTouches[0]; + currentTouchStart = touch ? [touch.clientX, touch.clientY] : [0, 0]; + currentTouchStartAxis = null; + currentTouchStartDelta = null; + }; + + const maybePreventTouch = (event: TouchEvent) => { + const target = event.target as HTMLElement; + let shouldCancel: boolean; + + if (event.touches.length === 2) { + shouldCancel = !allowPinchZoom; + } else { + if (currentTouchStartAxis === null || currentTouchStartDelta === null) { + const touch = event.changedTouches[0]; + const curr: [number, number] = touch ? [touch.clientX, touch.clientY] : [0, 0]; + const delta: [number, number] = [ + currentTouchStart[0] - curr[0], + currentTouchStart[1] - curr[1], + ]; + const axis: Axis = Math.abs(delta[0]) > Math.abs(delta[1]) ? "x" : "y"; + currentTouchStartAxis = axis; + currentTouchStartDelta = axis === "x" ? delta[0] : delta[1]; + } + + if ((target as HTMLInputElement).type === "range") { + shouldCancel = false; + } else { + const wouldResultInScroll = wouldScroll( + target, + currentTouchStartAxis, + currentTouchStartDelta, + wrapper, + ); + shouldCancel = + wrapper && contains(wrapper, target) ? !wouldResultInScroll : true; + } + } + + if (shouldCancel && event.cancelable) event.preventDefault(); + }; + + document.addEventListener("wheel", maybePreventWheel, { passive: false }); + document.addEventListener("touchstart", logTouchStart, { passive: false }); + document.addEventListener("touchmove", maybePreventTouch, { passive: false }); + + return () => { + document.removeEventListener("wheel", maybePreventWheel); + document.removeEventListener("touchstart", logTouchStart); + document.removeEventListener("touchmove", maybePreventTouch); + }; + }, + ); +}; diff --git a/packages/scroll/src/scrollPosition.ts b/packages/scroll/src/scrollPosition.ts new file mode 100644 index 000000000..055e02e36 --- /dev/null +++ b/packages/scroll/src/scrollPosition.ts @@ -0,0 +1,114 @@ +import { createEventListener } from "@solid-primitives/event-listener"; +import { createHydratableSingletonRoot } from "@solid-primitives/rootless"; +import { createDerivedStaticStore } from "@solid-primitives/static-store"; +import { type Accessor, createSignal, onSettled, sharedConfig } from "solid-js"; +import { isServer } from "@solidjs/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, +): Readonly { + if (isServer) { + return FALLBACK_SCROLL_POSITION; + } + + target = target || window; + + const isFn = typeof target === "function"; + const isHydrating = sharedConfig.hydrating; + + const getPos = (): Position => + getScrollPosition( + isFn ? (target as Accessor)() : (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(() => { + 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 }); + + 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), +); diff --git a/packages/scroll/test/index.test.ts b/packages/scroll/test/index.test.ts index 9df9cf771..280573960 100644 --- a/packages/scroll/test/index.test.ts +++ b/packages/scroll/test/index.test.ts @@ -1,7 +1,7 @@ import { createRoot, createSignal, flush } from "solid-js"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import { createScrollPosition, getScrollPosition } from "../src/index.js"; +import { createScrollPosition, getScrollPosition, createPreventScroll } from "../src/index.js"; describe("getScrollPosition", () => { it("no target returns null", () => { @@ -67,3 +67,177 @@ describe("createScrollPosition", () => { dispose(); })); }); + +describe("createPreventScroll", () => { + beforeEach(() => { + document.body.removeAttribute("style"); + vi.stubGlobal("scrollTo", vi.fn()); + }); + + afterEach(() => { + document.body.removeAttribute("style"); + vi.unstubAllGlobals(); + }); + + it("sets overflow:hidden on body when enabled", () => + createRoot(dispose => { + createPreventScroll(); + flush(); + + expect(document.body.style.overflow).toBe("hidden"); + + dispose(); + })); + + it("restores body style on cleanup", () => + createRoot(dispose => { + createPreventScroll(); + flush(); + + expect(document.body.style.overflow).toBe("hidden"); + + dispose(); + expect(document.body.style.overflow).toBe(""); + })); + + it("does not set overflow when hideScrollbar is false", () => + createRoot(dispose => { + createPreventScroll({ hideScrollbar: false }); + flush(); + + expect(document.body.style.overflow).not.toBe("hidden"); + + dispose(); + })); + + it("does not apply styles when enabled is false", () => + createRoot(dispose => { + createPreventScroll({ enabled: false }); + flush(); + + expect(document.body.style.overflow).not.toBe("hidden"); + + dispose(); + })); + + it("reacts to enabled signal", () => + createRoot(dispose => { + const [enabled, setEnabled] = createSignal(false, { ownedWrite: true }); + createPreventScroll({ enabled }); + flush(); + + expect(document.body.style.overflow).not.toBe("hidden"); + + setEnabled(true); + flush(); + expect(document.body.style.overflow).toBe("hidden"); + + setEnabled(false); + flush(); + expect(document.body.style.overflow).not.toBe("hidden"); + + dispose(); + })); + + it("installs wheel event listener when enabled", () => + createRoot(dispose => { + const addSpy = vi.spyOn(document, "addEventListener"); + + createPreventScroll(); + flush(); + + const wheelCalls = addSpy.mock.calls.filter(([event]) => event === "wheel"); + expect(wheelCalls.length).toBeGreaterThan(0); + + addSpy.mockRestore(); + dispose(); + })); + + it("removes wheel event listener on cleanup", () => + createRoot(dispose => { + const removeSpy = vi.spyOn(document, "removeEventListener"); + + createPreventScroll(); + flush(); + dispose(); + + const wheelCalls = removeSpy.mock.calls.filter(([event]) => event === "wheel"); + expect(wheelCalls.length).toBeGreaterThan(0); + + removeSpy.mockRestore(); + })); + + it("prevents wheel events outside element", () => + createRoot(dispose => { + const container = document.createElement("div"); + document.body.appendChild(container); + + createPreventScroll({ element: container }); + flush(); + + const outside = document.createElement("div"); + document.body.appendChild(outside); + + const event = new WheelEvent("wheel", { + bubbles: true, + cancelable: true, + deltaY: 100, + target: outside, + }); + Object.defineProperty(event, "target", { value: outside, configurable: true }); + document.dispatchEvent(event); + + expect(event.defaultPrevented).toBe(true); + + document.body.removeChild(container); + document.body.removeChild(outside); + dispose(); + })); + + it("stacks multiple instances — only top one handles events", () => + createRoot(dispose1 => { + createPreventScroll(); + flush(); + + const addSpy = vi.spyOn(document, "addEventListener"); + + createRoot(dispose2 => { + createPreventScroll(); + flush(); + + // Both instances active; second is on top + expect(document.body.style.overflow).toBe("hidden"); + + dispose2(); + }); + + flush(); + // First instance should still keep body locked + expect(document.body.style.overflow).toBe("hidden"); + + addSpy.mockRestore(); + dispose1(); + })); + + it("restores body style only after all stacked instances clean up", () => + createRoot(dispose1 => { + createPreventScroll(); + flush(); + + const cleanup2 = createRoot(dispose2 => { + createPreventScroll(); + flush(); + return dispose2; + }); + + expect(document.body.style.overflow).toBe("hidden"); + + cleanup2(); + flush(); + // First instance still active + expect(document.body.style.overflow).toBe("hidden"); + + dispose1(); + expect(document.body.style.overflow).toBe(""); + })); +}); diff --git a/packages/scroll/test/server.test.ts b/packages/scroll/test/server.test.ts index 8427402c2..c8f509e41 100644 --- a/packages/scroll/test/server.test.ts +++ b/packages/scroll/test/server.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { createScrollPosition, getScrollPosition, useWindowScrollPosition } from "../src/index.js"; +import { + createScrollPosition, + createPreventScroll, + getScrollPosition, + useWindowScrollPosition, +} from "../src/index.js"; describe("getScrollPosition", () => { it("returns null", () => { @@ -18,3 +23,10 @@ describe("useWindowScrollPosition", () => { expect(useWindowScrollPosition()).toEqual({ x: 0, y: 0 }); }); }); + +describe("createPreventScroll", () => { + it("does nothing on the server", () => { + expect(() => createPreventScroll()).not.toThrow(); + expect(() => createPreventScroll({ enabled: true, hideScrollbar: true })).not.toThrow(); + }); +}); diff --git a/packages/scroll/tsconfig.json b/packages/scroll/tsconfig.json index f696f1546..536eac32d 100644 --- a/packages/scroll/tsconfig.json +++ b/packages/scroll/tsconfig.json @@ -14,6 +14,9 @@ }, { "path": "../static-store" + }, + { + "path": "../utils" } ], "include": [ diff --git a/packages/static-store/package.json b/packages/static-store/package.json index 0e2312b40..74bdfdcc0 100644 --- a/packages/static-store/package.json +++ b/packages/static-store/package.json @@ -51,14 +51,14 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "peerDependencies": { - "@solidjs/web": "^2.0.0-beta.10", - "solid-js": "^2.0.0-beta.10" + "@solidjs/web": "^2.0.0-beta.12", + "solid-js": "^2.0.0-beta.12" }, "dependencies": { "@solid-primitives/utils": "workspace:^" }, "devDependencies": { - "@solidjs/web": "2.0.0-beta.10", - "solid-js": "2.0.0-beta.10" + "@solidjs/web": "2.0.0-beta.12", + "solid-js": "2.0.0-beta.12" } } diff --git a/packages/static-store/src/index.ts b/packages/static-store/src/index.ts index 4a1f2b651..7e30fa12f 100644 --- a/packages/static-store/src/index.ts +++ b/packages/static-store/src/index.ts @@ -3,10 +3,9 @@ import { type Accessor, createMemo, createSignal, - type EffectFunction, + type ComputeFunction, getObserver, getOwner, - type MemoOptions, type NoInfer, onSettled, runWithOwner, @@ -54,7 +53,8 @@ export function createStaticStore( let signal = cache[key]; if (!signal) { if (!getObserver()) return copy[key]; - cache[key] = signal = createSignal(copy[key], { pureWrite: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cache[key] = signal = createSignal(copy[key] as any, { ownedWrite: true }); delete copy[key]; } return signal[0](); @@ -103,7 +103,7 @@ export function createHydratableStaticStore( if (sharedConfig.hydrating) { const [state, setState] = createStaticStore(serverValue); - onSettled(() => setState(update())); + onSettled(() => { setState(update()); }); return [state, setState]; } @@ -131,35 +131,27 @@ export function createHydratableStaticStore( * ``` */ export function createDerivedStaticStore( - fn: EffectFunction, Next>, -): Next; -export function createDerivedStaticStore( - fn: EffectFunction, - value: Init, - options?: MemoOptions, -): Next; -export function createDerivedStaticStore( - fn: EffectFunction, - value?: T, - options?: MemoOptions, -): T { + fn: ComputeFunction, Next>, +): Next { const o = getOwner(), - fnMemo = createMemo(fn, value, options), - store = { ...untrack(fnMemo) }, - cache: Partial>> = {}; + fnMemo = createMemo(fn as ComputeFunction, Next>), + store = { ...untrack(fnMemo) } as Next, + cache: Partial>> = {}; - for (const key in store) - Object.defineProperty(store, key, { + for (const key in store) { + const k = key as keyof Next; + Object.defineProperty(store, k, { get() { - let keyMemo = cache[key]; + let keyMemo = cache[k]; if (!keyMemo) { - if (!getObserver()) return fnMemo()[key]; - runWithOwner(o, () => (cache[key] = keyMemo = createMemo(() => fnMemo()[key]))); + if (!getObserver()) return fnMemo()![k]; + runWithOwner(o, () => (cache[k] = keyMemo = createMemo(() => fnMemo()![k]))); } return keyMemo!(); }, enumerable: true, }); + } return store; } diff --git a/packages/utils/package.json b/packages/utils/package.json index 4fe598e02..61b69f3c3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -61,11 +61,11 @@ "primitives" ], "peerDependencies": { - "@solidjs/web": "^2.0.0-beta.10", - "solid-js": "^2.0.0-beta.10" + "@solidjs/web": "^2.0.0-beta.12", + "solid-js": "^2.0.0-beta.12" }, "devDependencies": { - "@solidjs/web": "2.0.0-beta.10", - "solid-js": "2.0.0-beta.10" + "@solidjs/web": "2.0.0-beta.12", + "solid-js": "2.0.0-beta.12" } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7c5e33cb1..45dc437a4 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,7 +4,7 @@ import { createSignal, type Accessor, untrack, - type EffectFunction, + type ComputeFunction, type NoInfer, type SignalOptions, sharedConfig, @@ -155,17 +155,17 @@ export function defer( deps: Accessor[] | Accessor, fn: (input: S, prevInput: S, prev: undefined | NoInfer) => Next, initialValue: Next, -): EffectFunction, NoInfer>; +): ComputeFunction, NoInfer>; export function defer( deps: Accessor[] | Accessor, fn: (input: S, prevInput: S, prev: undefined | NoInfer) => Next, initialValue?: undefined, -): EffectFunction>; +): ComputeFunction>; export function defer( deps: Accessor[] | Accessor, fn: (input: S, prevInput: S, prev: undefined | NoInfer) => Next, initialValue?: Next, -): EffectFunction> { +): ComputeFunction> { const isArray = Array.isArray(deps); let prevInput: S; let shouldDefer = true; @@ -173,7 +173,7 @@ export function defer( let input: S; if (isArray) { input = Array(deps.length) as S; - for (let i = 0; i < deps.length; i++) (input as any[])[i] = deps[i](); + for (let i = 0; i < deps.length; i++) (input as any[])[i] = deps[i]!(); } else input = deps(); if (shouldDefer) { shouldDefer = false; @@ -253,14 +253,14 @@ export function createHydratableSignal( options?: SignalOptions, ): ReturnType> { if (isServer) { - return createSignal(serverValue, options); + return createSignal(serverValue as Exclude, options); } if (sharedConfig.hydrating) { - const [state, setState] = createSignal(serverValue, options); - onSettled(() => setState(() => update())); + const [state, setState] = createSignal(serverValue as Exclude, options); + onSettled(() => { setState(() => update()); }); return [state, setState]; } - return createSignal(update(), options); + return createSignal(update() as Exclude, options); } /** @deprecated use {@link createHydratableSignal} instead */ @@ -396,3 +396,21 @@ export function safe( export function pipe(a: (raw: string) => A, b: (a: A) => B): (raw: string) => B { return (raw: string): B => b(a(raw)); } + +// ─── DOM helpers ───────────────────────────────────────────────────────────── + +/** + * Check if a wrapper element contains a target element. + * Portal-aware: follows SolidJS `_$host` links so elements rendered inside + * a `` are correctly treated as children of the portal's anchor. + */ +export const contains = (wrapper: HTMLElement, target: HTMLElement): boolean => { + if (wrapper.contains(target)) return true; + let current: HTMLElement | null = target; + while (current) { + if (current === wrapper) return true; + // @ts-expect-error: _$host is a SolidJS-internal property set on portal roots + current = current._$host ?? current.parentElement; + } + return false; +}; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index e91bfa027..0b3d91894 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1,6 +1,6 @@ import type { Accessor, Setter } from "solid-js"; -export type { EffectOptions, OnOptions } from "solid-js"; +export type { EffectOptions } from "solid-js"; // TODO delete in next major version export type { ResolvedJSXElement, ResolvedChildren } from "solid-js/types/reactive/signal.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40ec8c020..6e8338dbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,11 +126,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/bounds: dependencies: @@ -145,20 +145,20 @@ importers: specifier: workspace:^ version: link:../scheduled '@solidjs/web': - specifier: ^2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: ^2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: ^2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: ^2.0.0-beta.12 + version: 2.0.0-beta.12 packages/broadcast-channel: devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/clipboard: dependencies: @@ -302,17 +302,17 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/event-props: devDependencies: solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/fetch: dependencies: @@ -458,8 +458,8 @@ importers: packages/input-mask: devDependencies: solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/intersection-observer: dependencies: @@ -553,8 +553,8 @@ importers: specifier: workspace:^ version: link:../media solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/match: devDependencies: @@ -578,11 +578,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/memo: dependencies: @@ -651,11 +651,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/page-visibility: dependencies: @@ -708,11 +708,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/presence: dependencies: @@ -721,8 +721,8 @@ importers: version: link:../utils devDependencies: solid-js: - specifier: ^2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: ^2.0.0-beta.12 + version: 2.0.0-beta.12 packages/promise: dependencies: @@ -751,11 +751,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/range: dependencies: @@ -815,11 +815,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/scheduled: devDependencies: @@ -836,11 +836,11 @@ importers: packages/script-loader: devDependencies: '@solidjs/web': - specifier: ^2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: ^2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: ^2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: ^2.0.0-beta.12 + version: 2.0.0-beta.12 packages/scroll: dependencies: @@ -853,13 +853,16 @@ importers: '@solid-primitives/static-store': specifier: workspace:^ version: link:../static-store + '@solid-primitives/utils': + specifier: workspace:^ + version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/selection: devDependencies: @@ -922,11 +925,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/storage: dependencies: @@ -961,11 +964,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/timer: devDependencies: @@ -992,11 +995,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/tween: devDependencies: @@ -1017,11 +1020,11 @@ importers: packages/utils: devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/vibrate: dependencies: @@ -1030,11 +1033,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/virtual: dependencies: @@ -1046,14 +1049,14 @@ importers: specifier: ^7.27.0 version: 7.29.0 '@solidjs/web': - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(solid-js@2.0.0-beta.12) babel-preset-solid: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10(@babel/core@7.29.0)(solid-js@2.0.0-beta.10) + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12(@babel/core@7.29.0)(solid-js@2.0.0-beta.12) solid-js: - specifier: 2.0.0-beta.10 - version: 2.0.0-beta.10 + specifier: 2.0.0-beta.12 + version: 2.0.0-beta.12 packages/websocket: devDependencies: @@ -1122,10 +1125,10 @@ importers: version: link:../packages/utils '@tanstack/solid-router': specifier: ^1.168.16 - version: 1.169.1(solid-js@2.0.0-beta.10) + version: 1.169.1(solid-js@2.0.0-beta.12) '@tanstack/solid-start': specifier: ^1.167.28 - version: 1.167.59(solid-js@2.0.0-beta.10)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 1.167.59(solid-js@2.0.0-beta.12)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1152,10 +1155,10 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@2.0.0-beta.10) + version: 1.8.2(solid-js@2.0.0-beta.12) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@2.0.0-beta.10) + version: 1.1.0(solid-js@2.0.0-beta.12) undici: specifier: ^5.28.2 version: 5.28.4 @@ -1192,7 +1195,7 @@ importers: version: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vite-plugin-solid: specifier: ^2.11.12 - version: 2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) packages: @@ -2935,15 +2938,18 @@ packages: '@solidjs/signals@2.0.0-beta.10': resolution: {integrity: sha512-McdmbLNiSlz616zcykS8Rb1t9QTOTKdNAoaWd4/OjXEbcAUrPqRX1CWgR+caiWUk4qn0a+LesTTV4jZhFFPaSg==} + '@solidjs/signals@2.0.0-beta.12': + resolution: {integrity: sha512-xR8782xHcpcPnhENignNQNydn2tfaU68VP2MndsFT7VPQLG1RF4kAaWhrVWjvuzqY0cxOYcFeOTA3gInYHL50A==} + '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: vinxi: ^0.5.3 - '@solidjs/web@2.0.0-beta.10': - resolution: {integrity: sha512-Ox7MBv19kuxHoHhWoLCCcc6aykSgaqzWvWT7RB66VqlFnQ8Lid2ncd30g5L4XC0GB+MN/WZVb68tiYrAFUDIAg==} + '@solidjs/web@2.0.0-beta.12': + resolution: {integrity: sha512-Wc+/LctUqfNQs98VnijoEu4gWFOSu/kUcZiBIjQ+S9ZUuT6Z77CRkmiZ0C8dyOhNPbTgpU2JYH6B5wqY2eqS0A==} peerDependencies: - solid-js: ^2.0.0-beta.10 + solid-js: ^2.0.0-beta.12 '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} @@ -3553,8 +3559,8 @@ packages: peerDependencies: '@babel/core': ^7.20.12 - babel-plugin-jsx-dom-expressions@0.50.0-next.6: - resolution: {integrity: sha512-D7SSrMu1EupiCFT3hBhWJj0EWzaI27HV1ysbLSKFcH1ROZe61DmnNVchrnr5QeAw5O8bqSdlMDLdEqMYzi4tTA==} + babel-plugin-jsx-dom-expressions@0.50.0-next.10: + resolution: {integrity: sha512-NULWGRO6dlFaHn+ntDS7UVgoDS8Qrf7SZI+i9If3e+WwP2eEXtUk3YT60e2GzhobBzcLpVvE9vqAm1Aqw5P4BA==} peerDependencies: '@babel/core': ^7.20.12 @@ -3571,11 +3577,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - babel-preset-solid@2.0.0-beta.10: - resolution: {integrity: sha512-lzGgPsh1fVtBJDl+UWLTCgimzPMda7X2Xzq7asCCOq/zHRwiF5vF3Eb3xj65dGyi7YpgVROTwJEpj+XiroKaww==} + babel-preset-solid@2.0.0-beta.12: + resolution: {integrity: sha512-Ap5ubTcdlNfX1OzhOc3xS6FpU4U7ooElgK4IR/GajIj8e6kuhKMi7XW4bUkmkg4O+Bxjl8eJ5/VIdWE7eF5KNA==} peerDependencies: '@babel/core': ^7.0.0 - solid-js: ^2.0.0-beta.10 + solid-js: ^2.0.0-beta.12 peerDependenciesMeta: solid-js: optional: true @@ -6680,6 +6686,9 @@ packages: solid-js@2.0.0-beta.10: resolution: {integrity: sha512-EAfV6b1SC4c3wEBAoX4dMy063uTb4nfL5uXnN8yse4InH7RTw1LoB0I9HAy+pj3/GHqQE2tYZurlZtqU4pGyog==} + solid-js@2.0.0-beta.12: + resolution: {integrity: sha512-UJC4gc0Dgbm6BTBFhUdrfIEXiQ/jaQuUGxYfZnEkywwD5FX16MhlM/e6bq2+94mhXUExYI9VJoGBh7CpOZ/1XA==} + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -9469,47 +9478,47 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} - '@solid-devtools/debugger@0.28.1(solid-js@2.0.0-beta.10)': + '@solid-devtools/debugger@0.28.1(solid-js@2.0.0-beta.12)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.10) - '@solid-primitives/bounds': 0.1.5(solid-js@2.0.0-beta.10) - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/keyboard': 1.3.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 - - '@solid-devtools/logger@0.9.11(solid-js@2.0.0-beta.10)': + '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.12) + '@solid-primitives/bounds': 0.1.5(solid-js@2.0.0-beta.12) + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/keyboard': 1.3.5(solid-js@2.0.0-beta.12) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 + + '@solid-devtools/logger@0.9.11(solid-js@2.0.0-beta.12)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/debugger': 0.28.1(solid-js@2.0.0-beta.10) - '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-devtools/debugger': 0.28.1(solid-js@2.0.0-beta.12) + '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-devtools/shared@0.20.0(solid-js@2.0.0-beta.10)': + '@solid-devtools/shared@0.20.0(solid-js@2.0.0-beta.12)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/media': 2.3.5(solid-js@2.0.0-beta.10) - '@solid-primitives/refs': 1.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/styles': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 - - '@solid-primitives/bounds@0.1.5(solid-js@2.0.0-beta.10)': - dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/resize-observer': 2.1.5(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/media': 2.3.5(solid-js@2.0.0-beta.12) + '@solid-primitives/refs': 1.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/styles': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 + + '@solid-primitives/bounds@0.1.5(solid-js@2.0.0-beta.12)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/resize-observer': 2.1.5(solid-js@2.0.0-beta.12) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 '@solid-primitives/composites@1.1.1(solid-js@1.9.7)': dependencies: @@ -9521,68 +9530,68 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solid-primitives/event-listener@2.4.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/event-listener@2.4.5(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/keyboard@1.3.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/keyboard@1.3.5(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/media@2.3.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/media@2.3.5(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 '@solid-primitives/refs@1.0.8(solid-js@1.9.7)': dependencies: '@solid-primitives/utils': 6.2.3(solid-js@1.9.7) solid-js: 1.9.7 - '@solid-primitives/refs@1.0.8(solid-js@2.0.0-beta.10)': + '@solid-primitives/refs@1.0.8(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/utils': 6.2.3(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.2.3(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/refs@1.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/refs@1.1.3(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/resize-observer@2.1.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/resize-observer@2.1.5(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.12) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/rootless@1.5.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/rootless@1.5.3(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/scheduled@1.5.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/scheduled@1.5.3(solid-js@2.0.0-beta.12)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - '@solid-primitives/static-store@0.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/static-store@0.1.3(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 - '@solid-primitives/styles@0.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/styles@0.1.3(solid-js@2.0.0-beta.12)': dependencies: - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.12) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.12) + solid-js: 2.0.0-beta.12 '@solid-primitives/throttle@1.2.0(solid-js@1.9.7)': dependencies: @@ -9596,17 +9605,17 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solid-primitives/utils@6.2.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/utils@6.2.3(solid-js@2.0.0-beta.12)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - '@solid-primitives/utils@6.4.0(solid-js@2.0.0-beta.10)': + '@solid-primitives/utils@6.4.0(solid-js@2.0.0-beta.12)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.10)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.12)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: @@ -9614,6 +9623,8 @@ snapshots: '@solidjs/signals@2.0.0-beta.10': {} + '@solidjs/signals@2.0.0-beta.12': {} + '@solidjs/start@1.1.4(solid-js@2.0.0-beta.10)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.0(vite@6.3.5(@types/node@22.15.31)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) @@ -9637,11 +9648,11 @@ snapshots: - supports-color - vite - '@solidjs/web@2.0.0-beta.10(solid-js@2.0.0-beta.10)': + '@solidjs/web@2.0.0-beta.12(solid-js@2.0.0-beta.12)': dependencies: seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 '@speed-highlight/core@1.2.7': {} @@ -9734,7 +9745,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/router-plugin@1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -9751,7 +9762,7 @@ snapshots: zod: 3.25.63 optionalDependencies: vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) - vite-plugin-solid: 2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + vite-plugin-solid: 2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: - supports-color @@ -9796,45 +9807,45 @@ snapshots: - supports-color - vite - '@tanstack/solid-router@1.169.1(solid-js@2.0.0-beta.10)': + '@tanstack/solid-router@1.169.1(solid-js@2.0.0-beta.12)': dependencies: - '@solid-devtools/logger': 0.9.11(solid-js@2.0.0-beta.10) - '@solid-primitives/refs': 1.0.8(solid-js@2.0.0-beta.10) - '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.10) + '@solid-devtools/logger': 0.9.11(solid-js@2.0.0-beta.12) + '@solid-primitives/refs': 1.0.8(solid-js@2.0.0-beta.12) + '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.12) '@tanstack/history': 1.161.6 '@tanstack/router-core': 1.169.1 isbot: 5.1.39 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - '@tanstack/solid-start-client@1.166.46(solid-js@2.0.0-beta.10)': + '@tanstack/solid-start-client@1.166.46(solid-js@2.0.0-beta.12)': dependencies: '@tanstack/router-core': 1.169.1 - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.12) '@tanstack/start-client-core': 1.168.1 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - '@tanstack/solid-start-server@1.166.50(solid-js@2.0.0-beta.10)': + '@tanstack/solid-start-server@1.166.50(solid-js@2.0.0-beta.12)': dependencies: - '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.10) + '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.12) '@tanstack/history': 1.161.6 '@tanstack/router-core': 1.169.1 - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.12) '@tanstack/start-client-core': 1.168.1 '@tanstack/start-server-core': 1.167.29 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 transitivePeerDependencies: - crossws - '@tanstack/solid-start@1.167.59(solid-js@2.0.0-beta.10)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/solid-start@1.167.59(solid-js@2.0.0-beta.12)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) - '@tanstack/solid-start-client': 1.166.46(solid-js@2.0.0-beta.10) - '@tanstack/solid-start-server': 1.166.50(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.12) + '@tanstack/solid-start-client': 1.166.46(solid-js@2.0.0-beta.12) + '@tanstack/solid-start-server': 1.166.50(solid-js@2.0.0-beta.12) '@tanstack/start-client-core': 1.168.1 - '@tanstack/start-plugin-core': 1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + '@tanstack/start-plugin-core': 1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@tanstack/start-server-core': 1.167.29 pathe: 2.0.3 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 optionalDependencies: vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) transitivePeerDependencies: @@ -9853,7 +9864,7 @@ snapshots: '@tanstack/start-fn-stubs@1.161.6': {} - '@tanstack/start-plugin-core@1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/start-plugin-core@1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.0 @@ -9861,7 +9872,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.169.1 '@tanstack/router-generator': 1.166.39 - '@tanstack/router-plugin': 1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + '@tanstack/router-plugin': 1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@tanstack/router-utils': 1.161.7 '@tanstack/start-client-core': 1.168.1 '@tanstack/start-server-core': 1.167.29 @@ -10499,7 +10510,7 @@ snapshots: parse5: 7.3.0 validate-html-nesting: 1.2.2 - babel-plugin-jsx-dom-expressions@0.50.0-next.6(@babel/core@7.29.0): + babel-plugin-jsx-dom-expressions@0.50.0-next.10(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.18.6 @@ -10549,12 +10560,12 @@ snapshots: '@babel/core': 7.29.0 babel-plugin-jsx-dom-expressions: 0.39.8(@babel/core@7.29.0) - babel-preset-solid@2.0.0-beta.10(@babel/core@7.29.0)(solid-js@2.0.0-beta.10): + babel-preset-solid@2.0.0-beta.12(@babel/core@7.29.0)(solid-js@2.0.0-beta.12): dependencies: '@babel/core': 7.29.0 - babel-plugin-jsx-dom-expressions: 0.50.0-next.6(@babel/core@7.29.0) + babel-plugin-jsx-dom-expressions: 0.50.0-next.10(@babel/core@7.29.0) optionalDependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 bail@2.0.2: {} @@ -13981,13 +13992,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@2.0.0-beta.10): + solid-dismiss@1.8.2(solid-js@2.0.0-beta.12): dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 - solid-icons@1.1.0(solid-js@2.0.0-beta.10): + solid-icons@1.1.0(solid-js@2.0.0-beta.12): dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-beta.12 solid-js@1.9.7: dependencies: @@ -14002,6 +14013,13 @@ snapshots: seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) + solid-js@2.0.0-beta.12: + dependencies: + '@solidjs/signals': 2.0.0-beta.12 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-refresh@0.6.3(solid-js@2.0.0-beta.10): dependencies: '@babel/generator': 7.27.5 @@ -14011,6 +14029,15 @@ snapshots: transitivePeerDependencies: - supports-color + solid-refresh@0.6.3(solid-js@2.0.0-beta.12): + dependencies: + '@babel/generator': 7.27.5 + '@babel/helper-module-imports': 7.28.6 + '@babel/types': 7.29.0 + solid-js: 2.0.0-beta.12 + transitivePeerDependencies: + - supports-color + solid-transition-group@0.2.3(solid-js@1.9.7): dependencies: '@solid-primitives/refs': 1.0.8(solid-js@1.9.7) @@ -14696,14 +14723,14 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): + vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.12)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): dependencies: '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 babel-preset-solid: 1.9.6(@babel/core@7.29.0) merge-anything: 5.1.7 - solid-js: 2.0.0-beta.10 - solid-refresh: 0.6.3(solid-js@2.0.0-beta.10) + solid-js: 2.0.0-beta.12 + solid-refresh: 0.6.3(solid-js@2.0.0-beta.12) vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vitefu: 1.0.6(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: From 8e949842fbb46fd461e2c782ff924e60057114f0 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 14 May 2026 19:49:31 -0400 Subject: [PATCH 3/4] Adjusted values to use undefined as per request --- packages/scroll/src/preventScroll.ts | 48 ++++++++++++++++++--------- packages/scroll/src/scrollPosition.ts | 8 ++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/scroll/src/preventScroll.ts b/packages/scroll/src/preventScroll.ts index 76d12b57a..d5b772201 100644 --- a/packages/scroll/src/preventScroll.ts +++ b/packages/scroll/src/preventScroll.ts @@ -12,13 +12,24 @@ import { createEffect, createSignal } from "solid-js"; import { isServer } from "@solidjs/web"; -import { contains, INTERNAL_OPTIONS, access, type MaybeAccessor } from "@solid-primitives/utils"; +import { access, type MaybeAccessor } from "@solid-primitives/utils"; + +function contains(wrapper: HTMLElement, target: HTMLElement): boolean { + if (wrapper.contains(target)) return true; + let current: HTMLElement | null = target; + while (current) { + if (current === wrapper) return true; + // @ts-expect-error: _$host is a SolidJS-internal property set on portal roots + current = current._$host ?? current.parentElement; + } + return false; +} type Axis = "x" | "y"; export type CreatePreventScrollProps = { /** Element that is allowed to scroll. Wheel/touch events inside it are passed through. *Default = `null`* */ - element?: MaybeAccessor; + element?: MaybeAccessor; /** Whether scroll prevention is active. *Default = `true`* */ enabled?: MaybeAccessor; /** Hide the `` scrollbar while active. *Default = `true`* */ @@ -36,7 +47,9 @@ export type CreatePreventScrollProps = { // ─── Module-level stack ─────────────────────────────────────────────────────── // Tracks active instances; only the topmost one installs wheel/touch handlers. -const [preventScrollStack, setPreventScrollStack] = createSignal([], INTERNAL_OPTIONS); +const [preventScrollStack, setPreventScrollStack] = createSignal([], { + ownedWrite: true, +}); const isActive = (id: string): boolean => { const stack = preventScrollStack(); @@ -129,7 +142,7 @@ function getScrollAtLocation( const directionFactor = axis === "x" && window.getComputedStyle(location).direction === "rtl" ? -1 : 1; - let currentElement: HTMLElement | null = location; + let currentElement: HTMLElement | undefined = location; let availableScroll = 0; let availableScrollTop = 0; let wrapperReached = false; @@ -158,9 +171,9 @@ function wouldScroll( target: HTMLElement, axis: Axis, delta: number, - wrapper: HTMLElement | null, + wrapper: HTMLElement | undefined, ): boolean { - const targetInWrapper = wrapper !== null && contains(wrapper, target); + const targetInWrapper = wrapper && contains(wrapper, target); const [availableScroll, availableScrollTop] = getScrollAtLocation( target, axis, @@ -188,8 +201,8 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void const id = String(_nextId++); let currentTouchStart: [number, number] = [0, 0]; - let currentTouchStartAxis: Axis | null = null; - let currentTouchStartDelta: number | null = null; + let currentTouchStartAxis: Axis | undefined; + let currentTouchStartDelta: number | undefined; // 1. Manage the active-instance stack. createEffect( @@ -210,7 +223,13 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void preventScrollbarShiftMode: access(props.preventScrollbarShiftMode) ?? "padding", restoreScrollPosition: access(props.restoreScrollPosition) ?? true, }), - ({ enabled, hideScrollbar, preventScrollbarShift, preventScrollbarShiftMode, restoreScrollPosition }) => { + ({ + enabled, + hideScrollbar, + preventScrollbarShift, + preventScrollbarShiftMode, + restoreScrollPosition, + }) => { if (!enabled || !hideScrollbar) return; const { body } = document; @@ -246,7 +265,7 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void () => ({ active: isActive(id), enabled: access(props.enabled) ?? true, - wrapper: access(props.element) ?? null, + wrapper: access(props.element) ?? undefined, allowPinchZoom: access(props.allowPinchZoom) ?? false, }), ({ active, enabled, wrapper, allowPinchZoom }) => { @@ -269,8 +288,8 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void const logTouchStart = (event: TouchEvent) => { const touch = event.changedTouches[0]; currentTouchStart = touch ? [touch.clientX, touch.clientY] : [0, 0]; - currentTouchStartAxis = null; - currentTouchStartDelta = null; + currentTouchStartAxis = undefined; + currentTouchStartDelta = undefined; }; const maybePreventTouch = (event: TouchEvent) => { @@ -280,7 +299,7 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void if (event.touches.length === 2) { shouldCancel = !allowPinchZoom; } else { - if (currentTouchStartAxis === null || currentTouchStartDelta === null) { + if (currentTouchStartAxis === undefined || currentTouchStartDelta === undefined) { const touch = event.changedTouches[0]; const curr: [number, number] = touch ? [touch.clientX, touch.clientY] : [0, 0]; const delta: [number, number] = [ @@ -301,8 +320,7 @@ export const createPreventScroll = (props: CreatePreventScrollProps = {}): void currentTouchStartDelta, wrapper, ); - shouldCancel = - wrapper && contains(wrapper, target) ? !wouldResultInScroll : true; + shouldCancel = wrapper && contains(wrapper, target) ? !wouldResultInScroll : true; } } diff --git a/packages/scroll/src/scrollPosition.ts b/packages/scroll/src/scrollPosition.ts index 055e02e36..b01716fca 100644 --- a/packages/scroll/src/scrollPosition.ts +++ b/packages/scroll/src/scrollPosition.ts @@ -1,15 +1,15 @@ +import { type Accessor, createSignal, onSettled, sharedConfig } from "solid-js"; +import { isServer } from "@solidjs/web"; import { createEventListener } from "@solid-primitives/event-listener"; import { createHydratableSingletonRoot } from "@solid-primitives/rootless"; import { createDerivedStaticStore } from "@solid-primitives/static-store"; -import { type Accessor, createSignal, onSettled, sharedConfig } from "solid-js"; -import { isServer } from "@solidjs/web"; -export function getScrollParent(node: Element | null): Element { +export function getScrollParent(node: Element | undefined): Element { if (isServer) { return {} as Element; } while (node && !isScrollable(node)) { - node = node.parentElement; + node = node.parentElement ?? undefined; } return node || document.scrollingElement || document.documentElement; From a5b9a8c21dbce0ee9b6454436d05859b6944f313 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 14 May 2026 19:53:14 -0400 Subject: [PATCH 4/4] Added test to check for reactive property --- packages/scroll/README.md | 10 ++++++++-- packages/scroll/test/index.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/scroll/README.md b/packages/scroll/README.md index 419314639..d3150460b 100644 --- a/packages/scroll/README.md +++ b/packages/scroll/README.md @@ -128,7 +128,7 @@ Adapted from [solid-prevent-scroll](https://github.com/corvudev/corvu/tree/main/ ### How to use it -```ts +```tsx import { createPreventScroll } from "@solid-primitives/scroll"; // Prevent all page scroll (no element specified) @@ -137,6 +137,12 @@ createPreventScroll(); // Prevent scroll outside a specific element createPreventScroll({ element: () => myElement }); +// Using a signal ref — preferred pattern for JSX refs +const [ref, setRef] = createSignal(); +createPreventScroll({ element: ref }); + +
; + // Reactive enabled toggle const [open, setOpen] = createSignal(false); createPreventScroll({ enabled: open }); @@ -146,7 +152,7 @@ createPreventScroll({ enabled: open }); | Prop | Type | Default | Description | |------|------|---------|-------------| -| `element` | `MaybeAccessor` | `null` | Allow scroll inside this element. Events outside it are cancelled. | +| `element` | `MaybeAccessor` | `undefined` | Allow scroll inside this element. Events outside it are cancelled. | | `enabled` | `MaybeAccessor` | `true` | Whether scroll prevention is active. | | `hideScrollbar` | `MaybeAccessor` | `true` | Hide the `` scrollbar while active. | | `preventScrollbarShift` | `MaybeAccessor` | `true` | Compensate for the hidden scrollbar width to avoid layout shift. | diff --git a/packages/scroll/test/index.test.ts b/packages/scroll/test/index.test.ts index 280573960..5e028e5bf 100644 --- a/packages/scroll/test/index.test.ts +++ b/packages/scroll/test/index.test.ts @@ -194,6 +194,32 @@ describe("createPreventScroll", () => { dispose(); })); + it("accepts a signal ref as element — wrapper updates reactively", () => + createRoot(dispose => { + const container = document.createElement("div"); + const [ref, setRef] = createSignal(undefined, { + ownedWrite: true, + }); + + const addSpy = vi.spyOn(document, "addEventListener"); + + createPreventScroll({ element: ref }); + flush(); + + const wheelCallsAfterInit = addSpy.mock.calls.filter(([e]) => e === "wheel").length; + expect(wheelCallsAfterInit).toBe(1); + + // Changing the signal causes the effect to re-run and reinstall listeners with the new wrapper + setRef(container); + flush(); + + const wheelCallsAfterUpdate = addSpy.mock.calls.filter(([e]) => e === "wheel").length; + expect(wheelCallsAfterUpdate).toBe(2); + + addSpy.mockRestore(); + dispose(); + })); + it("stacks multiple instances — only top one handles events", () => createRoot(dispose1 => { createPreventScroll();