mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-17 04:39:40 +03:00
add defer option to overlayscrollbars-solid
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -57,18 +57,22 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-solid";
|
||||
|
||||
// ...
|
||||
|
||||
<OverlayScrollbarsComponent>
|
||||
<OverlayScrollbarsComponent defer>
|
||||
example content
|
||||
</OverlayScrollbarsComponent>
|
||||
```
|
||||
|
||||
### 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 <div ref={div} />
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElement
|
||||
options?: PartialOptions | false | null;
|
||||
/** OverlayScrollbars events. */
|
||||
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. */
|
||||
ref?: Exclude<Ref<OverlayScrollbarsComponentRef>, OverlayScrollbarsComponentRef>;
|
||||
}>;
|
||||
@@ -41,7 +43,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
|
||||
) => {
|
||||
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<HTMLDivElement | undefined>();
|
||||
const [childrenRef, setChildrenRef] = createSignal<HTMLDivElement | undefined>();
|
||||
@@ -52,7 +54,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
|
||||
const currChildrenElement = childrenRef();
|
||||
|
||||
if (currElement && currChildrenElement) {
|
||||
const osInstance = initialize({
|
||||
initialize({
|
||||
target: currElement,
|
||||
elements: {
|
||||
viewport: currChildrenElement,
|
||||
@@ -61,7 +63,7 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
osInstance.destroy();
|
||||
instance()?.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -75,10 +77,6 @@ export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements
|
||||
});
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
instance()?.destroy();
|
||||
});
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
component={finalProps.element}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRenderEffect } from 'solid-js';
|
||||
import { createRenderEffect, onCleanup } from 'solid-js';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import type { Accessor } from 'solid-js';
|
||||
import type { Store } from 'solid-js/store';
|
||||
@@ -8,6 +8,11 @@ import type {
|
||||
OverlayScrollbarsComponentRef,
|
||||
} from './OverlayScrollbarsComponent';
|
||||
|
||||
type Defer = [
|
||||
requestDefer: (callback: () => any, options?: OverlayScrollbarsComponentProps['defer']) => void,
|
||||
cancelDefer: () => void
|
||||
];
|
||||
|
||||
export interface CreateOverlayScrollbarsParams {
|
||||
/** OverlayScrollbars options. */
|
||||
options?:
|
||||
@@ -17,16 +22,59 @@ export interface CreateOverlayScrollbarsParams {
|
||||
events?:
|
||||
| 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 = (
|
||||
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<any> => typeof obj === 'function';
|
||||
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 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,
|
||||
];
|
||||
|
||||
@@ -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(() => <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', () => {
|
||||
let osRef: OverlayScrollbarsComponentRef | undefined;
|
||||
const { container } = render(
|
||||
|
||||
@@ -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', () => {
|
||||
<>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
const osInstance = initialize(event.target as HTMLElement);
|
||||
initialize(event.target as HTMLElement);
|
||||
if (instanceRef) {
|
||||
expect(instanceRef).toBe(osInstance);
|
||||
expect(instanceRef).toBe(instance());
|
||||
}
|
||||
instanceRef = osInstance;
|
||||
instanceRef = instance();
|
||||
expect(instanceRef).toBe(instance());
|
||||
}}
|
||||
>
|
||||
@@ -32,7 +32,7 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
);
|
||||
};
|
||||
|
||||
render(Test);
|
||||
const { unmount } = render(Test);
|
||||
|
||||
const initializeBtn = screen.getByRole('button');
|
||||
userEvent.click(initializeBtn);
|
||||
@@ -42,10 +42,16 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
userEvent.click(initializeBtn);
|
||||
|
||||
expect(snapshot).toBe(initializeBtn.innerHTML);
|
||||
|
||||
expect(OverlayScrollbars(initializeBtn)).toBeDefined();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(OverlayScrollbars(initializeBtn)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('params store', () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
let osInstance: OverlayScrollbars | null;
|
||||
const onUpdated = vitest.fn();
|
||||
render(() => {
|
||||
let div: HTMLDivElement;
|
||||
@@ -56,7 +62,8 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
const [initialize, instance] = createOverlayScrollbars(params);
|
||||
|
||||
onMount(() => {
|
||||
osInstance = initialize({ target: div! });
|
||||
initialize({ target: div! });
|
||||
osInstance = instance();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
@@ -92,7 +99,7 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
});
|
||||
|
||||
test('params signal', () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
let osInstance: OverlayScrollbars | null;
|
||||
const onUpdated = vitest.fn();
|
||||
render(() => {
|
||||
let div: HTMLDivElement;
|
||||
@@ -103,7 +110,8 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
const [initialize, instance] = createOverlayScrollbars(params);
|
||||
|
||||
onMount(() => {
|
||||
osInstance = initialize({ target: div! });
|
||||
initialize({ target: div! });
|
||||
osInstance = instance();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
@@ -139,7 +147,7 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
});
|
||||
|
||||
test('params fields signal', async () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
let osInstance: OverlayScrollbars | null;
|
||||
const onUpdated = vitest.fn();
|
||||
render(() => {
|
||||
let div: HTMLDivElement;
|
||||
@@ -151,7 +159,8 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
osInstance = initialize({ target: div! });
|
||||
initialize({ target: div! });
|
||||
osInstance = instance();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user