add defer option to overlayscrollbars-solid

This commit is contained in:
Rene Haas
2022-11-30 19:05:11 +01:00
parent 1976ccaebb
commit eba650544a
7 changed files with 175 additions and 35 deletions
+1
View File
@@ -8,6 +8,7 @@ const App: Component = () => {
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
style={{ width: '222px', height: '222px' }} style={{ width: '222px', height: '222px' }}
options={{ scrollbars: { theme: 'os-theme-light' } }} options={{ scrollbars: { theme: 'os-theme-light' } }}
defer
> >
<img src={logo} class={styles.logo} alt="logo" width="333" height="333" /> <img src={logo} class={styles.logo} alt="logo" width="333" height="333" />
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
@@ -1,5 +1,22 @@
# Changelog # 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 ## 0.4.0
The component was created. The component was created.
+12 -9
View File
@@ -57,18 +57,22 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-solid";
// ... // ...
<OverlayScrollbarsComponent> <OverlayScrollbarsComponent defer>
example content example content
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
``` ```
### Properties ### 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. - `element`: accepts a `string` which represents the tag of the root element.
- `options`: accepts an `object` which represents the OverlayScrollbars options. - `options`: accepts an `object` which represents the OverlayScrollbars options.
- `events`: accepts an `object` which represents the OverlayScrollbars events. - `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 ```jsx
// example usage // example usage
@@ -76,6 +80,7 @@ It has three optional properties: `element`, `options` and `events`.
element="span" element="span"
options={{ scrollbars: { autoHide: 'scroll' } }} options={{ scrollbars: { autoHide: 'scroll' } }}
events={{ scroll: () => { /* ... */ } }} events={{ scroll: () => { /* ... */ } }}
defer
/> />
``` ```
@@ -108,7 +113,7 @@ import { createOverlayScrollbars } from "overlayscrollbars-solid";
// example usage // example usage
const Component = () => { const Component = () => {
let div; let div;
const [params, setParams] = createStore({ options, events }); const [params, setParams] = createStore({ options, events, defer });
const [initialize, instance] = createOverlayScrollbars(params); const [initialize, instance] = createOverlayScrollbars(params);
/** /**
@@ -124,6 +129,7 @@ const Component = () => {
* const [initialize, instance] = createOverlayScrollbars({ * const [initialize, instance] = createOverlayScrollbars({
* options, * options,
* events, * events,
* defer,
* }); * });
* *
*/ */
@@ -132,10 +138,6 @@ const Component = () => {
initialize({ target: div }); initialize({ target: div });
}); });
onCleanup(() => {
instance().destroy();
});
return <div ref={div} /> return <div ref={div} />
} }
``` ```
@@ -145,12 +147,13 @@ The primitive is for advanced usage and lets you control the whole initializatio
### Parameters ### Parameters
Parameters are optional and similar to the `OverlayScrollbarsComponent`. 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. - `options`: accepts an `object` which represents the OverlayScrollbars options.
- `events`: accepts an `object` which represents the OverlayScrollbars events. - `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 ### Return
@@ -25,6 +25,8 @@ export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElement
options?: PartialOptions | false | null; options?: PartialOptions | false | null;
/** OverlayScrollbars events. */ /** OverlayScrollbars events. */
events?: EventListeners | false | null; events?: EventListeners | false | null;
/** 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?: boolean | IdleRequestOptions;
/** OverlayScrollbarsComponent ref. */ /** OverlayScrollbarsComponent ref. */
ref?: Exclude<Ref<OverlayScrollbarsComponentRef>, OverlayScrollbarsComponentRef>; ref?: Exclude<Ref<OverlayScrollbarsComponentRef>, OverlayScrollbarsComponentRef>;
}>; }>;
@@ -41,7 +43,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
) => { ) => {
const [finalProps, other] = splitProps( const [finalProps, other] = splitProps(
mergeProps({ element: 'div' }, props as OverlayScrollbarsComponentProps), mergeProps({ element: 'div' }, props as OverlayScrollbarsComponentProps),
['element', 'options', 'events', 'ref', 'children'] ['element', 'options', 'events', 'defer', 'ref', 'children']
); );
const [elementRef, setElementRef] = createSignal<HTMLDivElement | undefined>(); const [elementRef, setElementRef] = createSignal<HTMLDivElement | undefined>();
const [childrenRef, setChildrenRef] = createSignal<HTMLDivElement | undefined>(); const [childrenRef, setChildrenRef] = createSignal<HTMLDivElement | undefined>();
@@ -52,7 +54,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
const currChildrenElement = childrenRef(); const currChildrenElement = childrenRef();
if (currElement && currChildrenElement) { if (currElement && currChildrenElement) {
const osInstance = initialize({ initialize({
target: currElement, target: currElement,
elements: { elements: {
viewport: currChildrenElement, viewport: currChildrenElement,
@@ -61,7 +63,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
}); });
onCleanup(() => { onCleanup(() => {
osInstance.destroy(); instance()?.destroy();
}); });
} }
}); });
@@ -75,10 +77,6 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
}); });
}); });
onCleanup(() => {
instance()?.destroy();
});
return ( return (
<Dynamic <Dynamic
component={finalProps.element} component={finalProps.element}
@@ -1,4 +1,4 @@
import { createRenderEffect } from 'solid-js'; import { createRenderEffect, onCleanup } from 'solid-js';
import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbars } from 'overlayscrollbars';
import type { Accessor } from 'solid-js'; import type { Accessor } from 'solid-js';
import type { Store } from 'solid-js/store'; import type { Store } from 'solid-js/store';
@@ -8,6 +8,11 @@ import type {
OverlayScrollbarsComponentRef, OverlayScrollbarsComponentRef,
} from './OverlayScrollbarsComponent'; } from './OverlayScrollbarsComponent';
type Defer = [
requestDefer: (callback: () => any, options?: OverlayScrollbarsComponentProps['defer']) => void,
cancelDefer: () => void
];
export interface CreateOverlayScrollbarsParams { export interface CreateOverlayScrollbarsParams {
/** OverlayScrollbars options. */ /** OverlayScrollbars options. */
options?: options?:
@@ -17,16 +22,59 @@ export interface CreateOverlayScrollbarsParams {
events?: events?:
| OverlayScrollbarsComponentProps['events'] | OverlayScrollbarsComponentProps['events']
| Accessor<OverlayScrollbarsComponentProps['events']>; | Accessor<OverlayScrollbarsComponentProps['events']>;
/** 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<OverlayScrollbarsComponentProps['defer']>;
} }
export type CreateOverlayScrollbarsInitialization = ( export type CreateOverlayScrollbarsInitialization = (target: InitializationTarget) => void;
target: InitializationTarget
) => OverlayScrollbars;
export type CreateOverlayScrollbarsInstance = () => ReturnType< export type CreateOverlayScrollbarsInstance = () => ReturnType<
OverlayScrollbarsComponentRef['osInstance'] 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<any> => typeof obj === 'function'; const isAccessor = (obj: any): obj is Accessor<any> => typeof obj === 'function';
const unwrapAccessor = <T>(obj: Accessor<T> | T): T => (isAccessor(obj) ? obj() : obj); const unwrapAccessor = <T>(obj: Accessor<T> | T): T => (isAccessor(obj) ? obj() : obj);
@@ -39,6 +87,12 @@ export const createOverlayScrollbars = (
let instance: OverlayScrollbars | null = null; let instance: OverlayScrollbars | null = null;
let options: OverlayScrollbarsComponentProps['options']; let options: OverlayScrollbarsComponentProps['options'];
let events: OverlayScrollbarsComponentProps['events']; let events: OverlayScrollbarsComponentProps['events'];
let defer: OverlayScrollbarsComponentProps['defer'];
const [requestDefer, clearDefer] = createDefer();
createRenderEffect(() => {
defer = unwrapAccessor(unwrapAccessor(params)?.defer);
});
createRenderEffect(() => { createRenderEffect(() => {
options = unwrapAccessor(unwrapAccessor(params)?.options); options = unwrapAccessor(unwrapAccessor(params)?.options);
@@ -56,14 +110,25 @@ export const createOverlayScrollbars = (
} }
}); });
onCleanup(() => {
clearDefer();
instance?.destroy();
});
return [ return [
(target: InitializationTarget): OverlayScrollbars => { (target) => {
// if already initialized return the current instance // if already initialized do nothing
if (OverlayScrollbars.valid(instance)) { if (OverlayScrollbars.valid(instance)) {
return instance; return instance;
} }
return (instance = OverlayScrollbars(target, options || {}, events || {})); const init = () => (instance = OverlayScrollbars(target, options || {}, events || {}));
if (defer) {
requestDefer(init, defer);
} else {
init();
}
}, },
() => instance, () => instance,
]; ];
@@ -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 { createSignal, createEffect } from 'solid-js';
import { render, screen, cleanup, fireEvent } from 'solid-testing-library'; import { render, screen, cleanup, fireEvent } from 'solid-testing-library';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
@@ -6,6 +6,15 @@ import { OverlayScrollbars } from 'overlayscrollbars';
import { OverlayScrollbarsComponent } from '~/overlayscrollbars-solid'; import { OverlayScrollbarsComponent } from '~/overlayscrollbars-solid';
import type { OverlayScrollbarsComponentRef } from '~/overlayscrollbars-solid'; import type { OverlayScrollbarsComponentRef } from '~/overlayscrollbars-solid';
vi.useFakeTimers({
toFake: [
'requestAnimationFrame',
'cancelAnimationFrame',
'requestIdleCallback',
'cancelIdleCallback',
],
});
const createTestComponent = const createTestComponent =
(props: any = {}) => (props: any = {}) =>
() => { () => {
@@ -200,6 +209,44 @@ describe('OverlayScrollbarsComponent', () => {
}); });
}); });
describe('deferred initialization', () => {
test('basic defer', () => {
const { container } = render(() => <OverlayScrollbarsComponent defer />);
expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeUndefined();
vi.advanceTimersByTime(2000);
expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeDefined();
});
test('options defer', () => {
const { container } = render(() => <OverlayScrollbarsComponent defer={{ timeout: 0 }} />);
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(() => <OverlayScrollbarsComponent defer />);
expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeUndefined();
vi.advanceTimersByTime(2000);
expect(OverlayScrollbars(container.firstElementChild! as HTMLElement)).toBeDefined();
window.requestIdleCallback = original;
});
});
test('ref', () => { test('ref', () => {
let osRef: OverlayScrollbarsComponentRef | undefined; let osRef: OverlayScrollbarsComponentRef | undefined;
const { container } = render( const { container } = render(
@@ -3,8 +3,9 @@ import { createSignal, createEffect, onMount } from 'solid-js';
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { render, screen, cleanup } from 'solid-testing-library'; import { render, screen, cleanup } from 'solid-testing-library';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { OverlayScrollbars } from 'overlayscrollbars';
import { createOverlayScrollbars } from '~/overlayscrollbars-solid'; import { createOverlayScrollbars } from '~/overlayscrollbars-solid';
import type { OverlayScrollbars, PartialOptions, EventListeners } from 'overlayscrollbars'; import type { PartialOptions, EventListeners } from 'overlayscrollbars';
describe('OverlayScrollbarsComponent', () => { describe('OverlayScrollbarsComponent', () => {
afterEach(() => cleanup()); afterEach(() => cleanup());
@@ -17,12 +18,11 @@ describe('OverlayScrollbarsComponent', () => {
<> <>
<button <button
onClick={(event) => { onClick={(event) => {
const osInstance = initialize(event.target as HTMLElement); initialize(event.target as HTMLElement);
if (instanceRef) { if (instanceRef) {
expect(instanceRef).toBe(osInstance);
expect(instanceRef).toBe(instance()); expect(instanceRef).toBe(instance());
} }
instanceRef = osInstance; instanceRef = instance();
expect(instanceRef).toBe(instance()); expect(instanceRef).toBe(instance());
}} }}
> >
@@ -32,7 +32,7 @@ describe('OverlayScrollbarsComponent', () => {
); );
}; };
render(Test); const { unmount } = render(Test);
const initializeBtn = screen.getByRole('button'); const initializeBtn = screen.getByRole('button');
userEvent.click(initializeBtn); userEvent.click(initializeBtn);
@@ -42,10 +42,16 @@ describe('OverlayScrollbarsComponent', () => {
userEvent.click(initializeBtn); userEvent.click(initializeBtn);
expect(snapshot).toBe(initializeBtn.innerHTML); expect(snapshot).toBe(initializeBtn.innerHTML);
expect(OverlayScrollbars(initializeBtn)).toBeDefined();
unmount();
expect(OverlayScrollbars(initializeBtn)).toBeUndefined();
}); });
test('params store', () => { test('params store', () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render(() => { render(() => {
let div: HTMLDivElement; let div: HTMLDivElement;
@@ -56,7 +62,8 @@ describe('OverlayScrollbarsComponent', () => {
const [initialize, instance] = createOverlayScrollbars(params); const [initialize, instance] = createOverlayScrollbars(params);
onMount(() => { onMount(() => {
osInstance = initialize({ target: div! }); initialize({ target: div! });
osInstance = instance();
}); });
createEffect(() => { createEffect(() => {
@@ -92,7 +99,7 @@ describe('OverlayScrollbarsComponent', () => {
}); });
test('params signal', () => { test('params signal', () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render(() => { render(() => {
let div: HTMLDivElement; let div: HTMLDivElement;
@@ -103,7 +110,8 @@ describe('OverlayScrollbarsComponent', () => {
const [initialize, instance] = createOverlayScrollbars(params); const [initialize, instance] = createOverlayScrollbars(params);
onMount(() => { onMount(() => {
osInstance = initialize({ target: div! }); initialize({ target: div! });
osInstance = instance();
}); });
createEffect(() => { createEffect(() => {
@@ -139,7 +147,7 @@ describe('OverlayScrollbarsComponent', () => {
}); });
test('params fields signal', async () => { test('params fields signal', async () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render(() => { render(() => {
let div: HTMLDivElement; let div: HTMLDivElement;
@@ -151,7 +159,8 @@ describe('OverlayScrollbarsComponent', () => {
}); });
onMount(() => { onMount(() => {
osInstance = initialize({ target: div! }); initialize({ target: div! });
osInstance = instance();
}); });
createEffect(() => { createEffect(() => {