diff --git a/examples/solid/src/App.tsx b/examples/solid/src/App.tsx index 5e3d62e..e16d5be 100644 --- a/examples/solid/src/App.tsx +++ b/examples/solid/src/App.tsx @@ -8,6 +8,7 @@ const App: Component = () => { logo diff --git a/packages/overlayscrollbars-solid/CHANGELOG.md b/packages/overlayscrollbars-solid/CHANGELOG.md index a21d9a6..f93494f 100644 --- a/packages/overlayscrollbars-solid/CHANGELOG.md +++ b/packages/overlayscrollbars-solid/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.5.0 + +### Bug Fixes + +- Fixed SSR compatibility with `solid-start`. + +### Features + +Added the possibility to `defer` the initialization to a point in time when the browser is idle. (or to the next frame if `window.requestIdleCallback` is not supported) +- `OverlayScrollbarsComponent` accepts now the `defer` property +- `createOverlayScrollbars` params accept now the `defer` property +- `createOverlayScrollbars` will now always try to destroy the instance if the component unmounts. + +### Breaking Changes + +- Because initialization can be deferred now, the `initialize` function of the `createOverlayScrollbars` primitive isn't returning the instance anymore. Use the `instance` function of the `createOverlayScrollbars` primitive instead. + ## 0.4.0 The component was created. diff --git a/packages/overlayscrollbars-solid/README.md b/packages/overlayscrollbars-solid/README.md index 9694b11..306064b 100644 --- a/packages/overlayscrollbars-solid/README.md +++ b/packages/overlayscrollbars-solid/README.md @@ -57,18 +57,22 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-solid"; // ... - + example content ``` ### Properties -It has three optional properties: `element`, `options` and `events`. +The component accepts all properties of intrinsic JSX elements such as `div` and `span`. +Additionally it has custom optional properties: - `element`: accepts a `string` which represents the tag of the root element. - `options`: accepts an `object` which represents the OverlayScrollbars options. - `events`: accepts an `object` which represents the OverlayScrollbars events. +- `defer`: accepts an `boolean` or `object`. Defers the initialization to a point in time when the browser is idle. + +> __Note__: Its **highly recommended** to use the `defer` option whenever possible to defer the initialization to a browser's idle period. ```jsx // example usage @@ -76,6 +80,7 @@ It has three optional properties: `element`, `options` and `events`. element="span" options={{ scrollbars: { autoHide: 'scroll' } }} events={{ scroll: () => { /* ... */ } }} + defer /> ``` @@ -108,7 +113,7 @@ import { createOverlayScrollbars } from "overlayscrollbars-solid"; // example usage const Component = () => { let div; - const [params, setParams] = createStore({ options, events }); + const [params, setParams] = createStore({ options, events, defer }); const [initialize, instance] = createOverlayScrollbars(params); /** @@ -124,6 +129,7 @@ const Component = () => { * const [initialize, instance] = createOverlayScrollbars({ * options, * events, + * defer, * }); * */ @@ -132,10 +138,6 @@ const Component = () => { initialize({ target: div }); }); - onCleanup(() => { - instance().destroy(); - }); - return
} ``` @@ -145,12 +147,13 @@ The primitive is for advanced usage and lets you control the whole initializatio ### Parameters Parameters are optional and similar to the `OverlayScrollbarsComponent`. -Its an `object` with two optional properties: +Its an `object` with optional properties: - `options`: accepts an `object` which represents the OverlayScrollbars options. - `events`: accepts an `object` which represents the OverlayScrollbars events. +- `defer`: accepts an `boolean` or `object`. Defers the initialization to a point in time when the browser is idle. -> __Note__: The object can be a normal, `store` or `signal` object. This also applies to the `options` and `events` fields. +> __Note__: The object can be a normal, `store` or `signal` object. This also applies to all fields. ### Return diff --git a/packages/overlayscrollbars-solid/src/OverlayScrollbarsComponent.tsx b/packages/overlayscrollbars-solid/src/OverlayScrollbarsComponent.tsx index 01e9c80..8568b98 100644 --- a/packages/overlayscrollbars-solid/src/OverlayScrollbarsComponent.tsx +++ b/packages/overlayscrollbars-solid/src/OverlayScrollbarsComponent.tsx @@ -25,6 +25,8 @@ export type OverlayScrollbarsComponentProps, OverlayScrollbarsComponentRef>; }>; @@ -41,7 +43,7 @@ export const OverlayScrollbarsComponent = { const [finalProps, other] = splitProps( mergeProps({ element: 'div' }, props as OverlayScrollbarsComponentProps), - ['element', 'options', 'events', 'ref', 'children'] + ['element', 'options', 'events', 'defer', 'ref', 'children'] ); const [elementRef, setElementRef] = createSignal(); const [childrenRef, setChildrenRef] = createSignal(); @@ -52,7 +54,7 @@ export const OverlayScrollbarsComponent = { - osInstance.destroy(); + instance()?.destroy(); }); } }); @@ -75,10 +77,6 @@ export const OverlayScrollbarsComponent = { - instance()?.destroy(); - }); - return ( any, options?: OverlayScrollbarsComponentProps['defer']) => void, + cancelDefer: () => void +]; + export interface CreateOverlayScrollbarsParams { /** OverlayScrollbars options. */ options?: @@ -17,16 +22,59 @@ export interface CreateOverlayScrollbarsParams { events?: | OverlayScrollbarsComponentProps['events'] | Accessor; + /** Whether to defer the initialization to a point in time when the browser is idle. (or to the next frame if `window.requestIdleCallback` is not supported) */ + defer?: + | OverlayScrollbarsComponentProps['defer'] + | Accessor; } -export type CreateOverlayScrollbarsInitialization = ( - target: InitializationTarget -) => OverlayScrollbars; +export type CreateOverlayScrollbarsInitialization = (target: InitializationTarget) => void; export type CreateOverlayScrollbarsInstance = () => ReturnType< OverlayScrollbarsComponentRef['osInstance'] >; +const createDefer = (): Defer => { + /* c8 ignore start */ + if (typeof window === 'undefined') { + // mock ssr calls with "noop" + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => {}; + return [noop, noop]; + } + /* c8 ignore end */ + + let idleId: number; + let rafId: number; + const wnd = window; + const idleSupported = typeof wnd.requestIdleCallback === 'function'; + const rAF = wnd.requestAnimationFrame; + const cAF = wnd.cancelAnimationFrame; + const rIdle = idleSupported ? wnd.requestIdleCallback : rAF; + const cIdle = idleSupported ? wnd.cancelIdleCallback : cAF; + const clear = () => { + cIdle(idleId); + cAF(rafId); + }; + + return [ + (callback, options) => { + clear(); + idleId = rIdle( + idleSupported + ? () => { + clear(); + // inside idle its best practice to use rAF to change DOM for best performance + rafId = rAF(callback); + } + : callback, + typeof options === 'object' ? options : { timeout: 2233 } + ); + }, + clear, + ]; +}; + const isAccessor = (obj: any): obj is Accessor => typeof obj === 'function'; const unwrapAccessor = (obj: Accessor | T): T => (isAccessor(obj) ? obj() : obj); @@ -39,6 +87,12 @@ export const createOverlayScrollbars = ( let instance: OverlayScrollbars | null = null; let options: OverlayScrollbarsComponentProps['options']; let events: OverlayScrollbarsComponentProps['events']; + let defer: OverlayScrollbarsComponentProps['defer']; + const [requestDefer, clearDefer] = createDefer(); + + createRenderEffect(() => { + defer = unwrapAccessor(unwrapAccessor(params)?.defer); + }); createRenderEffect(() => { options = unwrapAccessor(unwrapAccessor(params)?.options); @@ -56,14 +110,25 @@ export const createOverlayScrollbars = ( } }); + onCleanup(() => { + clearDefer(); + instance?.destroy(); + }); + return [ - (target: InitializationTarget): OverlayScrollbars => { - // if already initialized return the current instance + (target) => { + // if already initialized do nothing if (OverlayScrollbars.valid(instance)) { return instance; } - return (instance = OverlayScrollbars(target, options || {}, events || {})); + const init = () => (instance = OverlayScrollbars(target, options || {}, events || {})); + + if (defer) { + requestDefer(init, defer); + } else { + init(); + } }, () => instance, ]; diff --git a/packages/overlayscrollbars-solid/test/OverlayScrollbarsComponent.test.tsx b/packages/overlayscrollbars-solid/test/OverlayScrollbarsComponent.test.tsx index 587c9cb..8df7242 100644 --- a/packages/overlayscrollbars-solid/test/OverlayScrollbarsComponent.test.tsx +++ b/packages/overlayscrollbars-solid/test/OverlayScrollbarsComponent.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, afterEach, expect, vitest } from 'vitest'; +import { describe, test, afterEach, expect, vitest, vi } from 'vitest'; import { createSignal, createEffect } from 'solid-js'; import { render, screen, cleanup, fireEvent } from 'solid-testing-library'; import userEvent from '@testing-library/user-event'; @@ -6,6 +6,15 @@ import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbarsComponent } from '~/overlayscrollbars-solid'; import type { OverlayScrollbarsComponentRef } from '~/overlayscrollbars-solid'; +vi.useFakeTimers({ + toFake: [ + 'requestAnimationFrame', + 'cancelAnimationFrame', + 'requestIdleCallback', + 'cancelIdleCallback', + ], +}); + const createTestComponent = (props: any = {}) => () => { @@ -200,6 +209,44 @@ describe('OverlayScrollbarsComponent', () => { }); }); + describe('deferred initialization', () => { + test('basic defer', () => { + const { container } = render(() => ); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeUndefined(); + + vi.advanceTimersByTime(2000); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeDefined(); + }); + + test('options defer', () => { + const { container } = render(() => ); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeUndefined(); + + vi.advanceTimersByTime(2000); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeDefined(); + }); + + test('defer with unsupported Idle', () => { + const original = window.requestIdleCallback; + // @ts-ignore + window.requestIdleCallback = undefined; + + const { container } = render(() => ); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeUndefined(); + + vi.advanceTimersByTime(2000); + + expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeDefined(); + + window.requestIdleCallback = original; + }); + }); + test('ref', () => { let osRef: OverlayScrollbarsComponentRef | undefined; const { container } = render( diff --git a/packages/overlayscrollbars-solid/test/createOverlayScrollbars.test.tsx b/packages/overlayscrollbars-solid/test/createOverlayScrollbars.test.tsx index 65be463..7e9c8ce 100644 --- a/packages/overlayscrollbars-solid/test/createOverlayScrollbars.test.tsx +++ b/packages/overlayscrollbars-solid/test/createOverlayScrollbars.test.tsx @@ -3,8 +3,9 @@ import { createSignal, createEffect, onMount } from 'solid-js'; import { createStore } from 'solid-js/store'; import { render, screen, cleanup } from 'solid-testing-library'; import userEvent from '@testing-library/user-event'; +import { OverlayScrollbars } from 'overlayscrollbars'; import { createOverlayScrollbars } from '~/overlayscrollbars-solid'; -import type { OverlayScrollbars, PartialOptions, EventListeners } from 'overlayscrollbars'; +import type { PartialOptions, EventListeners } from 'overlayscrollbars'; describe('OverlayScrollbarsComponent', () => { afterEach(() => cleanup()); @@ -17,12 +18,11 @@ describe('OverlayScrollbarsComponent', () => { <>