overlayscrollbars-vue v0.5.0

This commit is contained in:
Rene Haas
2022-11-17 09:20:02 +01:00
parent c6232bd3c0
commit 836b6a2076
12 changed files with 199 additions and 65 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
}, },
"../../packages/overlayscrollbars-react/dist": { "../../packages/overlayscrollbars-react/dist": {
"name": "overlayscrollbars-react", "name": "overlayscrollbars-react",
"version": "0.4.0", "version": "0.5.0",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"overlayscrollbars": "^2.0.0", "overlayscrollbars": "^2.0.0",
+1
View File
@@ -7,6 +7,7 @@ function App() {
<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} className="App-logo" alt="React logo" width="333" height="333" /> <img src={logo} className="App-logo" alt="React logo" width="333" height="333" />
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
+1 -1
View File
@@ -22,7 +22,7 @@
}, },
"../../packages/overlayscrollbars-vue/dist": { "../../packages/overlayscrollbars-vue/dist": {
"name": "overlayscrollbars-vue", "name": "overlayscrollbars-vue",
"version": "0.4.0", "version": "0.5.0",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"overlayscrollbars": "^2.0.0", "overlayscrollbars": "^2.0.0",
+1 -1
View File
@@ -10,7 +10,7 @@ const options: PartialOptions = {
</script> </script>
<template> <template>
<OverlayScrollbarsComponent style="height: 222px; width: 222px" :options="options"> <OverlayScrollbarsComponent style="height: 222px; width: 222px" :options="options" defer>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="333" height="333" /> <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="333" height="333" />
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</template> </template>
@@ -1,5 +1,18 @@
# Changelog # 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 ## 0.4.0
Depends on `OverlayScrollbars` version `^2.0.0` and `Vue` version `^3.2.25`. Depends on `OverlayScrollbars` version `^2.0.0` and `Vue` version `^3.2.25`.
+11 -5
View File
@@ -57,18 +57,21 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
// ... // ...
<OverlayScrollbarsComponent> <OverlayScrollbarsComponent defer>
example content example content
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
``` ```
### Properties ### 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. - `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 +79,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
/> />
``` ```
@@ -116,7 +120,7 @@ import { useOverlayScrollbars } from "overlayscrollbars-vue";
const Component = { const Component = {
setup() { setup() {
const div = ref(null); const div = ref(null);
const reactiveParams = reactive({ options, events }); const reactiveParams = reactive({ options, events, defer });
const [initialize, instance] = useOverlayScrollbars(reactiveParams); const [initialize, instance] = useOverlayScrollbars(reactiveParams);
/** /**
@@ -132,6 +136,7 @@ const Component = {
* const [initialize, instance] = useOverlayScrollbars({ * const [initialize, instance] = useOverlayScrollbars({
* options, * options,
* events, * events,
* defer,
* }); * });
* *
*/ */
@@ -154,12 +159,13 @@ The composable is for advanced usage and lets you control the whole initializati
### 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, `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 ### Return
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "overlayscrollbars-vue", "name": "overlayscrollbars-vue",
"private": true, "private": true,
"version": "0.4.0", "version": "0.5.0",
"description": "OverlayScrollbars for Vue.", "description": "OverlayScrollbars for Vue.",
"author": "Rene Haas | KingSora", "author": "Rene Haas | KingSora",
"license": "MIT", "license": "MIT",
@@ -4,6 +4,7 @@ export interface OverlayScrollbarsComponentProps {
element?: string; element?: string;
options?: PartialOptions | false | null; options?: PartialOptions | false | null;
events?: EventListeners | false | null; events?: EventListeners | false | null;
defer?: boolean | IdleRequestOptions;
} }
export interface OverlayScrollbarsComponentRef { export interface OverlayScrollbarsComponentRef {
@@ -25,6 +25,7 @@ const props = defineProps({
}, },
options: { type: Object as PropType<OverlayScrollbarsComponentProps['options']> }, options: { type: Object as PropType<OverlayScrollbarsComponentProps['options']> },
events: { type: Object as PropType<OverlayScrollbarsComponentProps['events']> }, events: { type: Object as PropType<OverlayScrollbarsComponentProps['events']> },
defer: { type: [Boolean, Object] as PropType<OverlayScrollbarsComponentProps['defer']> },
}); });
const emits = defineEmits<{ const emits = defineEmits<{
(name: 'osInitialized', ...args: EventListenerArgs['initialized']): void; (name: 'osInitialized', ...args: EventListenerArgs['initialized']): void;
@@ -36,8 +37,8 @@ const emits = defineEmits<{
const elementRef = shallowRef<HTMLElement | null>(null); const elementRef = shallowRef<HTMLElement | null>(null);
const slotRef = shallowRef<HTMLElement | null>(null); const slotRef = shallowRef<HTMLElement | null>(null);
const combinedEvents = ref<EventListeners>(); const combinedEvents = ref<EventListeners>();
const { element, options, events } = toRefs(props); const { element, options, events, defer } = toRefs(props);
const [initialize, osInstance] = useOverlayScrollbars({ options, events: combinedEvents }); const [initialize, osInstance] = useOverlayScrollbars({ options, events: combinedEvents, defer });
const exposed: OverlayScrollbarsComponentRef = { const exposed: OverlayScrollbarsComponentRef = {
osInstance, osInstance,
getElement: () => elementRef.value, getElement: () => elementRef.value,
@@ -45,21 +46,20 @@ const exposed: OverlayScrollbarsComponentRef = {
defineExpose(exposed); defineExpose(exposed);
onUnmounted(() => osInstance()?.destroy());
watchPostEffect((onCleanup) => { watchPostEffect((onCleanup) => {
const { value: elm } = elementRef; const { value: elm } = elementRef;
const { value: slotElm } = slotRef; const { value: slotElm } = slotRef;
if (elm && slotElm) { if (elm && slotElm) {
const instance = initialize({ initialize({
target: elm, target: elm,
elements: { elements: {
viewport: slotElm, viewport: slotElm,
content: slotElm, content: slotElm,
}, },
}); });
onCleanup(() => instance.destroy());
onCleanup(() => osInstance()?.destroy());
} }
}); });
@@ -1,4 +1,4 @@
import { shallowRef, unref, watch } from 'vue'; import { onUnmounted, shallowRef, unref, watch } from 'vue';
import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbars } from 'overlayscrollbars';
import type { Ref, UnwrapRef } from 'vue'; import type { Ref, UnwrapRef } from 'vue';
import type { InitializationTarget } from 'overlayscrollbars'; import type { InitializationTarget } from 'overlayscrollbars';
@@ -7,6 +7,11 @@ import type {
OverlayScrollbarsComponentRef, OverlayScrollbarsComponentRef,
} from './OverlayScrollbarsComponent.types'; } from './OverlayScrollbarsComponent.types';
type Defer = [
requestDefer: (callback: () => any, options?: OverlayScrollbarsComponentProps['defer']) => void,
cancelDefer: () => void
];
export interface UseOverlayScrollbarsParams { export interface UseOverlayScrollbarsParams {
/** OverlayScrollbars options. */ /** OverlayScrollbars options. */
options?: options?:
@@ -16,16 +21,57 @@ export interface UseOverlayScrollbarsParams {
events?: events?:
| OverlayScrollbarsComponentProps['events'] | OverlayScrollbarsComponentProps['events']
| Ref<OverlayScrollbarsComponentProps['events']>; | Ref<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'] | Ref<OverlayScrollbarsComponentProps['defer']>;
} }
export type UseOverlayScrollbarsInitialization = ( export type UseOverlayScrollbarsInitialization = (target: InitializationTarget) => void;
target: InitializationTarget
) => OverlayScrollbars;
export type UseOverlayScrollbarsInstance = () => ReturnType< export type UseOverlayScrollbarsInstance = () => 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,
];
};
/** /**
* Composable for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough) * Composable for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough)
* @param params Parameters for customization. * @param params Parameters for customization.
@@ -39,7 +85,17 @@ export const useOverlayScrollbars = (
let instance: ReturnType<UseOverlayScrollbarsInstance> = null; let instance: ReturnType<UseOverlayScrollbarsInstance> = null;
let options: UnwrapRef<UseOverlayScrollbarsParams['options']>; let options: UnwrapRef<UseOverlayScrollbarsParams['options']>;
let events: UnwrapRef<UseOverlayScrollbarsParams['events']>; let events: UnwrapRef<UseOverlayScrollbarsParams['events']>;
let defer: UnwrapRef<UseOverlayScrollbarsParams['defer']>;
const paramsRef = shallowRef(params || {}); const paramsRef = shallowRef(params || {});
const [requestDefer, clearDefer] = createDefer();
watch(
() => unref(paramsRef.value?.defer),
(currDefer) => {
defer = currDefer;
},
{ deep: true, immediate: true }
);
watch( watch(
() => unref(paramsRef.value?.options), () => unref(paramsRef.value?.options),
@@ -69,14 +125,25 @@ export const useOverlayScrollbars = (
{ deep: true, immediate: true } { deep: true, immediate: true }
); );
onUnmounted(() => {
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,10 +1,19 @@
import { onMounted, ref, toRefs } from 'vue'; 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 { OverlayScrollbars } from 'overlayscrollbars';
import { fireEvent, render, screen, cleanup } from '@testing-library/vue'; import { fireEvent, render, screen, cleanup } from '@testing-library/vue';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { OverlayScrollbarsComponent } from '~/overlayscrollbars-vue'; import { OverlayScrollbarsComponent } from '~/overlayscrollbars-vue';
vi.useFakeTimers({
toFake: [
'requestAnimationFrame',
'cancelAnimationFrame',
'requestIdleCallback',
'cancelIdleCallback',
],
});
describe('OverlayScrollbarsComponent', () => { describe('OverlayScrollbarsComponent', () => {
afterEach(() => cleanup()); afterEach(() => cleanup());
@@ -117,6 +126,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', () => {
const osRef = ref(); const osRef = ref();
const { container } = render({ const { container } = render({
@@ -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 { describe, test, afterEach, expect, vitest } from 'vitest';
import { render, screen, cleanup } from '@testing-library/vue'; import { render, screen, cleanup } from '@testing-library/vue';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from '~/overlayscrollbars-vue'; import { useOverlayScrollbars } from '~/overlayscrollbars-vue';
import type { PartialOptions, EventListeners, OverlayScrollbars } from 'overlayscrollbars'; import type { PartialOptions, EventListeners } from 'overlayscrollbars';
describe('useOverlayScrollbars', () => { describe('useOverlayScrollbars', () => {
afterEach(() => cleanup()); afterEach(() => {
try {
cleanup();
} catch {}
});
test('re-initialization', () => { test('re-initialization', () => {
render({ const { unmount } = render({
setup() { setup() {
const instanceRef = ref<OverlayScrollbars | null>(null); const instanceRef = ref<OverlayScrollbars | null>(null);
const [initialize, instance] = useOverlayScrollbars(); const [initialize, instance] = useOverlayScrollbars();
@@ -18,12 +23,11 @@ describe('useOverlayScrollbars', () => {
<> <>
<button <button
onClick={(event) => { onClick={(event) => {
const osInstance = initialize(event.target as HTMLElement); initialize(event.target as HTMLElement);
if (instanceRef.value) { if (instanceRef.value) {
expect(toRaw(instanceRef.value)).toBe(osInstance);
expect(toRaw(instanceRef.value)).toBe(instance()); expect(toRaw(instanceRef.value)).toBe(instance());
} }
instanceRef.value = osInstance; instanceRef.value = instance();
expect(toRaw(instanceRef.value)).toBe(instance()); expect(toRaw(instanceRef.value)).toBe(instance());
}} }}
> >
@@ -42,40 +46,37 @@ describe('useOverlayScrollbars', () => {
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('reactive params', async () => { test('reactive params', async () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render({ const { unmount } = render({
setup() { setup() {
const div = ref<HTMLElement | null>(null); const div = ref<HTMLElement | null>(null);
const params = reactive<{ options?: PartialOptions; events?: EventListeners }>({}); const params = reactive<{ options?: PartialOptions; events?: EventListeners }>({});
const [initialize, instance] = useOverlayScrollbars(params); const [initialize, instance] = useOverlayScrollbars(params);
onMounted(() => { onMounted(() => {
osInstance = initialize({ target: div.value! }); initialize({ target: div.value! });
osInstance = instance();
}); });
watch(
() => params,
() => {
if (params.events!.updated) {
instance()?.update(true);
}
},
{ deep: true }
);
return () => ( return () => (
<> <>
<div ref={div} /> <div ref={div} />
<button <button
onClick={() => { onClick={() => {
params.options = {};
params.events = {}; params.events = {};
params.options!.paddingAbsolute = true;
params.events!.updated = onUpdated; params.events!.updated = onUpdated;
params.options = {};
params.options!.paddingAbsolute = true;
}} }}
> >
trigger trigger
@@ -92,25 +93,22 @@ describe('useOverlayScrollbars', () => {
expect(onUpdated).toHaveBeenCalledTimes(1); expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true); expect(osInstance!.options().paddingAbsolute).toBe(true);
unmount();
}); });
test('ref params', async () => { test('ref params', async () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render({ const { unmount } = render({
setup() { setup() {
const div = ref<HTMLElement | null>(null); const div = ref<HTMLElement | null>(null);
const params = ref<{ options?: PartialOptions; events?: EventListeners } | undefined>(); const params = ref<{ options?: PartialOptions; events?: EventListeners } | undefined>();
const [initialize, instance] = useOverlayScrollbars(params); const [initialize, instance] = useOverlayScrollbars(params);
onMounted(() => { onMounted(() => {
osInstance = initialize({ target: div.value! }); initialize({ target: div.value! });
}); osInstance = instance();
watchPostEffect(() => {
if (params.value?.events?.updated) {
instance()?.update(true);
}
}); });
return () => ( return () => (
@@ -139,29 +137,28 @@ describe('useOverlayScrollbars', () => {
expect(onUpdated).toHaveBeenCalledTimes(1); expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true); expect(osInstance!.options().paddingAbsolute).toBe(true);
unmount();
}); });
test('ref params fields', async () => { test('ref params fields', async () => {
let osInstance: OverlayScrollbars; let osInstance: OverlayScrollbars | null;
const onUpdated = vitest.fn(); const onUpdated = vitest.fn();
render({ const { unmount } = render({
setup() { setup() {
const div = ref<HTMLElement | null>(null); const div = ref<HTMLElement | null>(null);
const options = ref<PartialOptions | undefined>(); const options = ref<PartialOptions | undefined>();
const events = ref<EventListeners | undefined>(); const events = ref<EventListeners | undefined>();
const defer = ref<boolean | undefined>();
const [initialize, instance] = useOverlayScrollbars({ const [initialize, instance] = useOverlayScrollbars({
options, options,
events, events,
defer,
}); });
onMounted(() => { onMounted(() => {
osInstance = initialize({ target: div.value! }); initialize({ target: div.value! });
}); osInstance = instance();
watchPostEffect(() => {
if (events.value?.updated) {
instance()?.update(true);
}
}); });
return () => ( return () => (
@@ -169,10 +166,10 @@ describe('useOverlayScrollbars', () => {
<div ref={div} /> <div ref={div} />
<button <button
onClick={() => { onClick={() => {
options.value = {};
events.value = {}; events.value = {};
options.value.paddingAbsolute = true;
events.value.updated = onUpdated; events.value.updated = onUpdated;
options.value = {};
options.value.paddingAbsolute = true;
}} }}
> >
trigger trigger
@@ -189,5 +186,7 @@ describe('useOverlayScrollbars', () => {
expect(onUpdated).toHaveBeenCalledTimes(1); expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true); expect(osInstance!.options().paddingAbsolute).toBe(true);
unmount();
}); });
}); });