diff --git a/examples/react/package-lock.json b/examples/react/package-lock.json index ff52838..72940b4 100644 --- a/examples/react/package-lock.json +++ b/examples/react/package-lock.json @@ -24,7 +24,7 @@ }, "../../packages/overlayscrollbars-react/dist": { "name": "overlayscrollbars-react", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "peerDependencies": { "overlayscrollbars": "^2.0.0", diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx index 5522a93..5c2a9b6 100644 --- a/examples/react/src/App.tsx +++ b/examples/react/src/App.tsx @@ -7,6 +7,7 @@ function App() { React logo diff --git a/examples/vue/package-lock.json b/examples/vue/package-lock.json index b1166cb..5ffb1d6 100644 --- a/examples/vue/package-lock.json +++ b/examples/vue/package-lock.json @@ -22,7 +22,7 @@ }, "../../packages/overlayscrollbars-vue/dist": { "name": "overlayscrollbars-vue", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "peerDependencies": { "overlayscrollbars": "^2.0.0", diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue index 2e7b9a4..9635757 100644 --- a/examples/vue/src/App.vue +++ b/examples/vue/src/App.vue @@ -10,7 +10,7 @@ const options: PartialOptions = { diff --git a/packages/overlayscrollbars-vue/CHANGELOG.md b/packages/overlayscrollbars-vue/CHANGELOG.md index 4c1c6c5..f840ce8 100644 --- a/packages/overlayscrollbars-vue/CHANGELOG.md +++ b/packages/overlayscrollbars-vue/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.5.0 + +### 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 +- `useOverlayScrollbars` params accept now the `defer` key +- `useOverlayScrollbars` 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 `useOverlayScrollbars` composable isn't returning the instance anymore. Use the `instance` function of the `useOverlayScrollbars` composable instead. + ## 0.4.0 Depends on `OverlayScrollbars` version `^2.0.0` and `Vue` version `^3.2.25`. diff --git a/packages/overlayscrollbars-vue/README.md b/packages/overlayscrollbars-vue/README.md index 7da2f7f..aa6c825 100644 --- a/packages/overlayscrollbars-vue/README.md +++ b/packages/overlayscrollbars-vue/README.md @@ -57,18 +57,21 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; // ... - + example content ``` ### Properties -It has three optional properties: `element`, `options` and `events`. +It has 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 +79,7 @@ It has three optional properties: `element`, `options` and `events`. element="span" options={{ scrollbars: { autoHide: 'scroll' } }} events={{ scroll: () => { /* ... */ } }} + defer /> ``` @@ -116,7 +120,7 @@ import { useOverlayScrollbars } from "overlayscrollbars-vue"; const Component = { setup() { const div = ref(null); - const reactiveParams = reactive({ options, events }); + const reactiveParams = reactive({ options, events, defer }); const [initialize, instance] = useOverlayScrollbars(reactiveParams); /** @@ -132,6 +136,7 @@ const Component = { * const [initialize, instance] = useOverlayScrollbars({ * options, * events, + * defer, * }); * */ @@ -154,12 +159,13 @@ The composable is for advanced usage and lets you control the whole initializati ### 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, `reactive` or `ref` object. This also applies to the `options` and `events` fields. +> __Note__: The object can be a normal, `reactive` or `ref` object. This also applies to all fields. ### Return diff --git a/packages/overlayscrollbars-vue/package.json b/packages/overlayscrollbars-vue/package.json index ac64c6d..cf298fd 100644 --- a/packages/overlayscrollbars-vue/package.json +++ b/packages/overlayscrollbars-vue/package.json @@ -1,7 +1,7 @@ { "name": "overlayscrollbars-vue", "private": true, - "version": "0.4.0", + "version": "0.5.0", "description": "OverlayScrollbars for Vue.", "author": "Rene Haas | KingSora", "license": "MIT", diff --git a/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.types.ts b/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.types.ts index e4f14fd..3fe9a3d 100644 --- a/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.types.ts +++ b/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.types.ts @@ -4,6 +4,7 @@ export interface OverlayScrollbarsComponentProps { element?: string; options?: PartialOptions | false | null; events?: EventListeners | false | null; + defer?: boolean | IdleRequestOptions; } export interface OverlayScrollbarsComponentRef { diff --git a/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.vue b/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.vue index ec6a91d..51e495b 100644 --- a/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.vue +++ b/packages/overlayscrollbars-vue/src/OverlayScrollbarsComponent.vue @@ -25,6 +25,7 @@ const props = defineProps({ }, options: { type: Object as PropType }, events: { type: Object as PropType }, + defer: { type: [Boolean, Object] as PropType }, }); const emits = defineEmits<{ (name: 'osInitialized', ...args: EventListenerArgs['initialized']): void; @@ -36,8 +37,8 @@ const emits = defineEmits<{ const elementRef = shallowRef(null); const slotRef = shallowRef(null); const combinedEvents = ref(); -const { element, options, events } = toRefs(props); -const [initialize, osInstance] = useOverlayScrollbars({ options, events: combinedEvents }); +const { element, options, events, defer } = toRefs(props); +const [initialize, osInstance] = useOverlayScrollbars({ options, events: combinedEvents, defer }); const exposed: OverlayScrollbarsComponentRef = { osInstance, getElement: () => elementRef.value, @@ -45,21 +46,20 @@ const exposed: OverlayScrollbarsComponentRef = { defineExpose(exposed); -onUnmounted(() => osInstance()?.destroy()); - watchPostEffect((onCleanup) => { const { value: elm } = elementRef; const { value: slotElm } = slotRef; if (elm && slotElm) { - const instance = initialize({ + initialize({ target: elm, elements: { viewport: slotElm, content: slotElm, }, }); - onCleanup(() => instance.destroy()); + + onCleanup(() => osInstance()?.destroy()); } }); diff --git a/packages/overlayscrollbars-vue/src/useOverlayScrollbars.ts b/packages/overlayscrollbars-vue/src/useOverlayScrollbars.ts index 087d296..85ac17a 100644 --- a/packages/overlayscrollbars-vue/src/useOverlayScrollbars.ts +++ b/packages/overlayscrollbars-vue/src/useOverlayScrollbars.ts @@ -1,4 +1,4 @@ -import { shallowRef, unref, watch } from 'vue'; +import { onUnmounted, shallowRef, unref, watch } from 'vue'; import { OverlayScrollbars } from 'overlayscrollbars'; import type { Ref, UnwrapRef } from 'vue'; import type { InitializationTarget } from 'overlayscrollbars'; @@ -7,6 +7,11 @@ import type { OverlayScrollbarsComponentRef, } from './OverlayScrollbarsComponent.types'; +type Defer = [ + requestDefer: (callback: () => any, options?: OverlayScrollbarsComponentProps['defer']) => void, + cancelDefer: () => void +]; + export interface UseOverlayScrollbarsParams { /** OverlayScrollbars options. */ options?: @@ -16,16 +21,57 @@ export interface UseOverlayScrollbarsParams { events?: | OverlayScrollbarsComponentProps['events'] | Ref; + /** 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'] | Ref; } -export type UseOverlayScrollbarsInitialization = ( - target: InitializationTarget -) => OverlayScrollbars; +export type UseOverlayScrollbarsInitialization = (target: InitializationTarget) => void; export type UseOverlayScrollbarsInstance = () => 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, + ]; +}; + /** * Composable for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough) * @param params Parameters for customization. @@ -39,7 +85,17 @@ export const useOverlayScrollbars = ( let instance: ReturnType = null; let options: UnwrapRef; let events: UnwrapRef; + let defer: UnwrapRef; const paramsRef = shallowRef(params || {}); + const [requestDefer, clearDefer] = createDefer(); + + watch( + () => unref(paramsRef.value?.defer), + (currDefer) => { + defer = currDefer; + }, + { deep: true, immediate: true } + ); watch( () => unref(paramsRef.value?.options), @@ -69,14 +125,25 @@ export const useOverlayScrollbars = ( { deep: true, immediate: true } ); + onUnmounted(() => { + 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-vue/test/OverlayScrollbarsComponent.test.tsx b/packages/overlayscrollbars-vue/test/OverlayScrollbarsComponent.test.tsx index 77a9b3a..c356317 100644 --- a/packages/overlayscrollbars-vue/test/OverlayScrollbarsComponent.test.tsx +++ b/packages/overlayscrollbars-vue/test/OverlayScrollbarsComponent.test.tsx @@ -1,10 +1,19 @@ import { onMounted, ref, toRefs } from 'vue'; -import { describe, test, afterEach, expect, vitest } from 'vitest'; +import { describe, test, afterEach, expect, vitest, vi } from 'vitest'; import { OverlayScrollbars } from 'overlayscrollbars'; import { fireEvent, render, screen, cleanup } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { OverlayScrollbarsComponent } from '~/overlayscrollbars-vue'; +vi.useFakeTimers({ + toFake: [ + 'requestAnimationFrame', + 'cancelAnimationFrame', + 'requestIdleCallback', + 'cancelIdleCallback', + ], +}); + describe('OverlayScrollbarsComponent', () => { afterEach(() => cleanup()); @@ -117,6 +126,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', () => { const osRef = ref(); const { container } = render({ diff --git a/packages/overlayscrollbars-vue/test/useOverlayScrollbars.test.tsx b/packages/overlayscrollbars-vue/test/useOverlayScrollbars.test.tsx index 92ab45b..2f91639 100644 --- a/packages/overlayscrollbars-vue/test/useOverlayScrollbars.test.tsx +++ b/packages/overlayscrollbars-vue/test/useOverlayScrollbars.test.tsx @@ -1,15 +1,20 @@ -import { reactive, onMounted, ref, watch, toRaw, watchPostEffect } from 'vue'; +import { reactive, onMounted, ref, toRaw } from 'vue'; import { describe, test, afterEach, expect, vitest } from 'vitest'; import { render, screen, cleanup } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; +import { OverlayScrollbars } from 'overlayscrollbars'; import { useOverlayScrollbars } from '~/overlayscrollbars-vue'; -import type { PartialOptions, EventListeners, OverlayScrollbars } from 'overlayscrollbars'; +import type { PartialOptions, EventListeners } from 'overlayscrollbars'; describe('useOverlayScrollbars', () => { - afterEach(() => cleanup()); + afterEach(() => { + try { + cleanup(); + } catch {} + }); test('re-initialization', () => { - render({ + const { unmount } = render({ setup() { const instanceRef = ref(null); const [initialize, instance] = useOverlayScrollbars(); @@ -18,12 +23,11 @@ describe('useOverlayScrollbars', () => { <>