mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-17 03:09:39 +03:00
improve api, readmes, overlayscrollbars-react and finish overlayscrollbars-vue
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Rene Haas
|
||||
Copyright (c) 2022 Rene Haas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Generated
+7
-1521
File diff suppressed because it is too large
Load Diff
Generated
+725
-10
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Rene Haas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -63,7 +63,7 @@ import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
The main entry point is the `OverlayScrollbarsComponent` which can be used in your application as a component:
|
||||
|
||||
```js
|
||||
```jsx
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
// ...
|
||||
@@ -75,14 +75,14 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
### Properties
|
||||
|
||||
The component accepts all properties which intrinsic JSX elements such as `div` and `span` accept.
|
||||
The component accepts all properties of intrinsic JSX elements such as `div` and `span`.
|
||||
Additionally it has three optional properties: `element`, `options` and `events`.
|
||||
|
||||
- `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.
|
||||
|
||||
None of these properties has to be memoized.
|
||||
> __Note__: None of these properties has to be memoized.
|
||||
|
||||
```jsx
|
||||
// example usage
|
||||
@@ -105,7 +105,7 @@ The ref object has two properties:
|
||||
|
||||
In case the `OverlayScrollbarsComponent` is not enough, you can also use the `useOverlayScrollbars` hook:
|
||||
|
||||
```js
|
||||
```jsx
|
||||
import { useOverlayScrollbars } from "overlayscrollbars-react";
|
||||
|
||||
// example usage
|
||||
@@ -132,7 +132,6 @@ Its an `object` with two optional properties:
|
||||
- `options`: accepts an `object` which represents the OverlayScrollbars options.
|
||||
- `events`: accepts an `object` which represents the OverlayScrollbars events.
|
||||
|
||||
|
||||
### Return
|
||||
|
||||
The `useOverlayScrollbars` hook returns a `tuple` with two values:
|
||||
@@ -140,7 +139,7 @@ The `useOverlayScrollbars` hook returns a `tuple` with two values:
|
||||
- The first value is the `initialization` function, it takes one argument which is the `InitializationTarget` and returns the OverlayScrollbars instance.
|
||||
- The second value is a function which returns the current OverlayScrollbars instance or `null` if not initialized.
|
||||
|
||||
The identity of both functions is stable and won't change, thus they can safely be used in any dependency array.
|
||||
> __Note__: The identity of both functions is stable and won't change, thus they can safely be used in any dependency array.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
"scrollbars",
|
||||
"scroll"
|
||||
],
|
||||
"main": "./dist/overlayscrollbars-react.umd.js",
|
||||
"module": "./dist/overlayscrollbars-react.es.js",
|
||||
"types": "./dist/overlayscrollbars-react.d.ts",
|
||||
"main": "./src/overlayscrollbars-react.ts",
|
||||
"module": "./src/overlayscrollbars-react.ts",
|
||||
"types": "./src/overlayscrollbars-react.ts",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"overlayscrollbars": "^2.0.0"
|
||||
|
||||
@@ -9,9 +9,9 @@ export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElement
|
||||
/** Tag of the root element. */
|
||||
element?: T;
|
||||
/** OverlayScrollbars options. */
|
||||
options?: PartialOptions;
|
||||
options?: PartialOptions | false | null;
|
||||
/** OverlayScrollbars events. */
|
||||
events?: EventListeners;
|
||||
events?: EventListeners | false | null;
|
||||
};
|
||||
|
||||
export interface OverlayScrollbarsComponentRef<T extends keyof JSX.IntrinsicElements = 'div'> {
|
||||
@@ -27,17 +27,16 @@ const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements>(
|
||||
) => {
|
||||
const { element = 'div', options, events, children, ...other } = props;
|
||||
const Tag = element;
|
||||
|
||||
const [initialize, instance] = useOverlayScrollbars({ options, events });
|
||||
const elementRef = useRef<ElementRef<T>>(null);
|
||||
const childrenRef = useRef<HTMLDivElement>(null);
|
||||
const [initialize, instance] = useOverlayScrollbars({ options, events });
|
||||
|
||||
useEffect(() => {
|
||||
const { current: targetElm } = elementRef;
|
||||
const { current: elm } = elementRef;
|
||||
const { current: childrenElm } = childrenRef;
|
||||
if (targetElm && childrenElm) {
|
||||
if (elm && childrenElm) {
|
||||
const osInstance = initialize({
|
||||
target: targetElm as any,
|
||||
target: elm as any,
|
||||
elements: {
|
||||
viewport: childrenElm,
|
||||
content: childrenElm,
|
||||
@@ -46,7 +45,7 @@ const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements>(
|
||||
|
||||
return () => osInstance.destroy();
|
||||
}
|
||||
}, [initialize]);
|
||||
}, [initialize, element]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import type { PartialOptions, InitializationTarget, EventListeners } from 'overlayscrollbars';
|
||||
import type { InitializationTarget } from 'overlayscrollbars';
|
||||
import type {
|
||||
OverlayScrollbarsComponentProps,
|
||||
OverlayScrollbarsComponentRef,
|
||||
} from './OverlayScrollbarsComponent';
|
||||
|
||||
export interface UseOverlayScrollbarsParams {
|
||||
/** OverlayScrollbars options. */
|
||||
options?: PartialOptions;
|
||||
options?: OverlayScrollbarsComponentProps['options'];
|
||||
/** OverlayScrollbars events. */
|
||||
events?: EventListeners;
|
||||
events?: OverlayScrollbarsComponentProps['events'];
|
||||
}
|
||||
|
||||
export type UseOverlayScrollbarsInitialization = (
|
||||
target: InitializationTarget
|
||||
) => OverlayScrollbars;
|
||||
|
||||
export type UseOverlayScrollbarsInstance = () => OverlayScrollbars | null;
|
||||
export type UseOverlayScrollbarsInstance = () => ReturnType<
|
||||
OverlayScrollbarsComponentRef['instance']
|
||||
>;
|
||||
|
||||
/**
|
||||
* Hook for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough)
|
||||
@@ -26,24 +32,21 @@ export const useOverlayScrollbars = (
|
||||
params?: UseOverlayScrollbarsParams
|
||||
): [UseOverlayScrollbarsInitialization, UseOverlayScrollbarsInstance] => {
|
||||
const { options, events } = params || {};
|
||||
const osInstanceRef = useRef<OverlayScrollbars | null>(null);
|
||||
const offInitialEventsRef = useRef<(() => void) | void>();
|
||||
const optionsRef = useRef<PartialOptions>();
|
||||
const eventsRef = useRef<EventListeners>();
|
||||
const osInstanceRef = useRef<ReturnType<UseOverlayScrollbarsInstance>>(null);
|
||||
const optionsRef = useRef(options);
|
||||
const eventsRef = useRef(events);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: instance } = osInstanceRef;
|
||||
if (OverlayScrollbars.valid(instance) && options) {
|
||||
instance.options(options, true);
|
||||
if (OverlayScrollbars.valid(instance)) {
|
||||
instance.options(options || {}, true);
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: instance } = osInstanceRef;
|
||||
const { current: offInitialEvents } = offInitialEventsRef;
|
||||
if (OverlayScrollbars.valid(instance) && events) {
|
||||
offInitialEvents && (offInitialEventsRef.current = offInitialEvents()); // once called assign it to undefined so its not called again
|
||||
return instance.on(events);
|
||||
if (OverlayScrollbars.valid(instance)) {
|
||||
instance.on(events || {}, true);
|
||||
}
|
||||
}, [events]);
|
||||
|
||||
@@ -67,8 +70,6 @@ export const useOverlayScrollbars = (
|
||||
currEvents
|
||||
));
|
||||
|
||||
offInitialEventsRef.current = osInstance.on(currEvents);
|
||||
|
||||
return osInstance;
|
||||
},
|
||||
() => osInstanceRef.current,
|
||||
|
||||
@@ -9,29 +9,46 @@ import type { OverlayScrollbarsComponentRef } from '~/overlayscrollbars-react';
|
||||
|
||||
describe('OverlayScrollbarsComponent', () => {
|
||||
describe('correct rendering', () => {
|
||||
test('correct root element', () => {
|
||||
test('correct root element with instance', () => {
|
||||
const elementA = 'code';
|
||||
const elementB = 'span';
|
||||
let osInstance;
|
||||
const { container, rerender } = render(<OverlayScrollbarsComponent />);
|
||||
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
expect(container.querySelector('div')).toBe(container.firstElementChild); // default is div
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false);
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
|
||||
rerender(<OverlayScrollbarsComponent element={elementA} />);
|
||||
expect(container.querySelector(elementA)).toBe(container.firstElementChild);
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
|
||||
rerender(<OverlayScrollbarsComponent element={elementB} />);
|
||||
expect(container.querySelector(elementB)).toBe(container.firstElementChild);
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
});
|
||||
|
||||
test('children', () => {
|
||||
render(
|
||||
const { container } = render(
|
||||
<OverlayScrollbarsComponent>
|
||||
hello <span>react</span>
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
expect(screen.getByText(/hello/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/react/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/react/).parentElement).not.toBe(container.firstElementChild);
|
||||
});
|
||||
|
||||
test('dynamic children', async () => {
|
||||
@@ -112,26 +129,61 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
const instance = ref.current!.instance()!;
|
||||
|
||||
const opts = ref.current!.instance()!.options();
|
||||
const opts = instance.options();
|
||||
expect(opts.paddingAbsolute).toBe(true);
|
||||
expect(opts.overflow.y).toBe('hidden');
|
||||
|
||||
rerender(<OverlayScrollbarsComponent options={{ overflow: { x: 'hidden' } }} ref={ref} />);
|
||||
|
||||
const newOpts = ref.current!.instance()!.options()!;
|
||||
const newOpts = instance.options();
|
||||
expect(newOpts.paddingAbsolute).toBe(false); //switches back to default because its not specified in the new options
|
||||
expect(newOpts.overflow.y).toBe('scroll'); //switches back to default because its not specified in the new options
|
||||
expect(newOpts.overflow.x).toBe('hidden');
|
||||
expect(newOpts.overflow.y).toBe('scroll'); //switches back to default because its not specified in the new options
|
||||
|
||||
// instance didn't change
|
||||
expect(instance).toBe(ref.current!.instance());
|
||||
|
||||
rerender(
|
||||
<OverlayScrollbarsComponent
|
||||
element="span"
|
||||
options={{ overflow: { x: 'hidden', y: 'hidden' } }}
|
||||
ref={ref as any as RefObject<OverlayScrollbarsComponentRef<'span'>>}
|
||||
/>
|
||||
);
|
||||
|
||||
const newElementInstance = ref.current!.instance()!;
|
||||
const newElementNewOpts = newElementInstance.options();
|
||||
expect(newElementInstance).not.toBe(instance);
|
||||
expect(newElementNewOpts.paddingAbsolute).toBe(false);
|
||||
expect(newElementNewOpts.overflow.x).toBe('hidden');
|
||||
expect(newElementNewOpts.overflow.y).toBe('hidden');
|
||||
|
||||
// reset options with `undefined`, `null`, `false` or `{}`
|
||||
rerender(
|
||||
<OverlayScrollbarsComponent
|
||||
element="span"
|
||||
options={undefined}
|
||||
ref={ref as any as RefObject<OverlayScrollbarsComponentRef<'span'>>}
|
||||
/>
|
||||
);
|
||||
|
||||
const resetOpts = newElementInstance.options();
|
||||
expect(newElementInstance).toBe(ref.current!.instance());
|
||||
expect(resetOpts.paddingAbsolute).toBe(false);
|
||||
expect(resetOpts.overflow.x).toBe('scroll');
|
||||
expect(resetOpts.overflow.y).toBe('scroll');
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
const ref: RefObject<OverlayScrollbarsComponentRef> = { current: null };
|
||||
const onUpdatedInitial = vitest.fn();
|
||||
const onUpdated = vitest.fn();
|
||||
const ref: RefObject<OverlayScrollbarsComponentRef> = { current: null };
|
||||
const { rerender } = render(
|
||||
<OverlayScrollbarsComponent events={{ updated: onUpdatedInitial }} ref={ref} />
|
||||
);
|
||||
const instance = ref.current!.instance()!;
|
||||
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -139,7 +191,7 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -147,60 +199,44 @@ describe('OverlayScrollbarsComponent', () => {
|
||||
<OverlayScrollbarsComponent events={{ updated: [onUpdated, onUpdatedInitial] }} ref={ref} />
|
||||
);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(2);
|
||||
|
||||
// unregister works with `[]`, `null` or `undefined`
|
||||
rerender(<OverlayScrollbarsComponent events={{ updated: null }} ref={ref} />);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
const ref: RefObject<OverlayScrollbarsComponentRef> = { current: null };
|
||||
const onUpdatedInitial = vitest.fn();
|
||||
const onUpdated = vitest.fn();
|
||||
const { rerender } = render(
|
||||
<OverlayScrollbarsComponent events={{ updated: onUpdatedInitial }} ref={ref} />
|
||||
);
|
||||
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<OverlayScrollbarsComponent events={{ updated: onUpdated }} ref={ref} />);
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<OverlayScrollbarsComponent events={{ updated: onUpdatedInitial }} ref={ref} />);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<OverlayScrollbarsComponent events={{ updated: onUpdated }} ref={ref} />);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(2);
|
||||
// instance didn't change
|
||||
expect(instance).toBe(ref.current!.instance());
|
||||
|
||||
rerender(
|
||||
<OverlayScrollbarsComponent events={{ updated: [onUpdated, onUpdatedInitial] }} ref={ref} />
|
||||
<OverlayScrollbarsComponent
|
||||
element="span"
|
||||
events={{ updated: [onUpdated, onUpdatedInitial] }}
|
||||
ref={ref as any as RefObject<OverlayScrollbarsComponentRef<'span'>>}
|
||||
/>
|
||||
);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
const newElementInstance = ref.current!.instance()!;
|
||||
expect(newElementInstance).not.toBe(instance);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(3);
|
||||
|
||||
// unregister works with `[]`, `null` or `undefined`
|
||||
rerender(<OverlayScrollbarsComponent events={{ updated: null }} ref={ref} />);
|
||||
// reset events with `undefined`, `null`, `false` or `{}`
|
||||
rerender(
|
||||
<OverlayScrollbarsComponent
|
||||
element="span"
|
||||
events={undefined}
|
||||
ref={ref as any as RefObject<OverlayScrollbarsComponentRef<'span'>>}
|
||||
/>
|
||||
);
|
||||
|
||||
ref.current!.instance()!.update(true);
|
||||
newElementInstance.update(true);
|
||||
expect(newElementInstance).toBe(ref.current!.instance());
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["jest", "@testing-library/jest-dom"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
import { esbuildResolve } from 'rollup-plugin-esbuild-resolve';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import rollupPluginPackageJson from '@~local/rollup/plugin/packageJson';
|
||||
import rollupPluginCopy from '@~local/rollup/plugin/copy';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Rene Haas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,46 +1,149 @@
|
||||
# vue-project
|
||||
<div align="center">
|
||||
<a href="https://kingsora.github.io/OverlayScrollbars">
|
||||
<img src="https://raw.githubusercontent.com/KingSora/OverlayScrollbars/master/logo/logo.png" width="160" height="160" alt="OverlayScrollbars">
|
||||
</a>
|
||||
<a href="https://vuejs.org/">
|
||||
<img src="https://raw.githubusercontent.com/KingSora/OverlayScrollbars/master/packages/overlayscrollbars-vue/logo.svg" width="160" height="160" alt="Vue">
|
||||
</a>
|
||||
</div>
|
||||
<h6 align="center">
|
||||
<a href="https://github.com/KingSora/OverlayScrollbars">
|
||||
<img src="https://img.shields.io/badge/OverlayScrollbars-%5E2.0.0-338EFF?style=flat-square" alt="OverlayScrollbars">
|
||||
</a>
|
||||
<a href="https://github.com/vuejs/vue">
|
||||
<img src="https://img.shields.io/badge/Vue-%5E3.0.0-41B883?style=flat-square&logo=vue.js" alt="Vue">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/overlayscrollbars-vue">
|
||||
<img src="https://img.shields.io/npm/dt/overlayscrollbars-vue.svg?style=flat-square" alt="Downloads">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/overlayscrollbars">
|
||||
<img src="https://img.shields.io/npm/v/overlayscrollbars-vue.svg?style=flat-square" alt="Version">
|
||||
</a>
|
||||
<a href="https://github.com/KingSora/OverlayScrollbars/blob/master/packages/overlayscrollbars-vue/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/kingsora/overlayscrollbars.svg?style=flat-square" alt="License">
|
||||
</a>
|
||||
</h6>
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
# OverlayScrollbars for Vue
|
||||
|
||||
## Recommended IDE Setup
|
||||
This is the official OverlayScrollbars Vue wrapper.
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm install overlayscrollbars-vue
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
## Peer Dependencies
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
OverlayScrollbars for Vue has the following **peer dependencies**:
|
||||
|
||||
- The vanilla JavaScript library: [overlayscrollbars](https://www.npmjs.com/package/overlayscrollbars)
|
||||
|
||||
```
|
||||
npm install overlayscrollbars
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
- The Vue framework: [vue](https://www.npmjs.com/package/vue)
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
npm install vue
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
The first step is to import the CSS file into your app:
|
||||
```ts
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
```
|
||||
|
||||
> __Note__: In older node versions use `'overlayscrollbars/styles/overlayscrollbars.css'` as the import path for the CSS file.
|
||||
|
||||
## Component
|
||||
|
||||
The main entry point is the `OverlayScrollbarsComponent` which can be used in your application as a component:
|
||||
|
||||
```jsx
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
// ...
|
||||
|
||||
<OverlayScrollbarsComponent>
|
||||
example content
|
||||
</OverlayScrollbarsComponent>
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
It has three optional properties: `element`, `options` and `events`.
|
||||
|
||||
- `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.
|
||||
|
||||
```jsx
|
||||
// example usage
|
||||
<OverlayScrollbarsComponent
|
||||
element="span"
|
||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||
events={{ scroll: () => { /* ... */ } }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Ref
|
||||
|
||||
The `ref` of the `OverlayScrollbarsComponent` will give you an object with which you can access the OverlayScrollbars `instance` and the root `element` of the component.
|
||||
The ref object has two properties:
|
||||
|
||||
- `instance`: a function which returns the OverlayScrollbars instance.
|
||||
- `element`: a function which returns the root element.
|
||||
|
||||
## Composable
|
||||
|
||||
In case the `OverlayScrollbarsComponent` is not enough, you can also use the `useOverlayScrollbars` composable:
|
||||
|
||||
```jsx
|
||||
import { useOverlayScrollbars } from "overlayscrollbars-vue";
|
||||
|
||||
// example usage
|
||||
const Component = {
|
||||
setup() {
|
||||
const params = reactive({});
|
||||
const [initialize, instance] = useOverlayScrollbars(params);
|
||||
const div = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
initialize({ target: div.value });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
instance().destroy();
|
||||
});
|
||||
|
||||
return () => <div ref={div} />
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The composable is for advanced usage and lets you control the whole initialization process. This is useful if you want to integrate it with other plugins.
|
||||
|
||||
### Parameters
|
||||
|
||||
Parameters are optional and similar to the `OverlayScrollbarsComponent`.
|
||||
Its an `object` with two optional properties:
|
||||
|
||||
- `options`: accepts an `object` which represents the OverlayScrollbars options.
|
||||
- `events`: accepts an `object` which represents the OverlayScrollbars events.
|
||||
|
||||
> __Note__: The object can be a normal, `reactive` or `ref` object. This also applies to the `options` and `events` fields.
|
||||
|
||||
### Return
|
||||
|
||||
The `useOverlayScrollbars` composable returns a `tuple` with two values:
|
||||
|
||||
- The first value is the `initialization` function, it takes one argument which is the `InitializationTarget` and returns the OverlayScrollbars instance.
|
||||
- The second value is a function which returns the current OverlayScrollbars instance or `null` if not initialized.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1247.05px" height="1079.989px" viewBox="336.436 0.005 1247.05 1079.989" enable-background="new 336.436 0.005 1247.05 1079.989" xml:space="preserve">
|
||||
<g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)">
|
||||
<g transform="translate(178.06 235.01)">
|
||||
<path fill="#41B883" d="M707.163-0.003l-108-187.062l-108,187.062H131.508l467.655-810.012L1066.819-0.003H707.163z"/>
|
||||
</g>
|
||||
<g transform="translate(178.06 235.01)">
|
||||
<path fill="#34495E" d="M707.163-0.003l-108-187.062l-108,187.062H318.57l280.593-485.998L879.757-0.003H707.163z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
@@ -2,24 +2,45 @@
|
||||
"name": "overlayscrollbars-vue",
|
||||
"private": true,
|
||||
"version": "0.4.0",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"README.md"
|
||||
"description": "OverlayScrollbars for Vue.",
|
||||
"author": "Rene Haas | KingSora",
|
||||
"license": "MIT",
|
||||
"homepage": "https://kingsora.github.io/OverlayScrollbars",
|
||||
"bugs": "https://github.com/KingSora/OverlayScrollbars/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/KingSora/OverlayScrollbars.git",
|
||||
"directory": "packages/overlayscrollbars-vue"
|
||||
},
|
||||
"keywords": [
|
||||
"overlayscrollbars",
|
||||
"vue",
|
||||
"vue2",
|
||||
"vue3",
|
||||
"component",
|
||||
"composable",
|
||||
"composition",
|
||||
"styleable",
|
||||
"scrollbar",
|
||||
"scrollbars",
|
||||
"scroll"
|
||||
],
|
||||
"main": "./dist/overlayscrollbars-vue.umd.js",
|
||||
"module": "./dist/overlayscrollbars-vue.es.js",
|
||||
"types": "./dist/overlayscrollbars-vue.d.ts",
|
||||
"main": "./src/overlayscrollbars-vue.ts",
|
||||
"module": "./src/overlayscrollbars-vue.ts",
|
||||
"types": "./src/overlayscrollbars-vue.ts",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.25"
|
||||
"vue": "^3.2.25",
|
||||
"overlayscrollbars": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^16.11.45",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.1.0",
|
||||
"@vue/test-utils": "^2.1.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"jsdom": "^20.0.0",
|
||||
"overlayscrollbars": "file:./../overlayscrollbars/dist",
|
||||
"terser": "^5.14.2",
|
||||
"typescript": "~4.7.4",
|
||||
"vue": "^3.2.25",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { OverlayScrollbars, PartialOptions, EventListeners } from 'overlayscrollbars';
|
||||
|
||||
export interface OverlayScrollbarsComponentProps {
|
||||
element?: string;
|
||||
options?: PartialOptions | false | null;
|
||||
events?: EventListeners | false | null;
|
||||
}
|
||||
|
||||
export interface OverlayScrollbarsComponentRef {
|
||||
/** Returns the OverlayScrollbars instance or null if not initialized. */
|
||||
instance(): OverlayScrollbars | null;
|
||||
/** Returns the root element. */
|
||||
element(): HTMLElement | null;
|
||||
}
|
||||
@@ -1,16 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string;
|
||||
}>();
|
||||
<script lang="ts">
|
||||
import { defineComponent, watchPostEffect, onBeforeUnmount, shallowRef, toRef } from 'vue';
|
||||
import { useOverlayScrollbars } from './useOverlayScrollbars';
|
||||
import type {
|
||||
OverlayScrollbarsComponentProps,
|
||||
OverlayScrollbarsComponentRef,
|
||||
} from './OverlayScrollbarsComponent.types';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OverlayScrollbars',
|
||||
props: {
|
||||
element: {
|
||||
type: String as PropType<OverlayScrollbarsComponentProps['element']>,
|
||||
default: 'div',
|
||||
},
|
||||
options: { type: Object as PropType<OverlayScrollbarsComponentProps['options']> },
|
||||
events: { type: Object as PropType<OverlayScrollbarsComponentProps['events']> },
|
||||
},
|
||||
setup(props, { expose }) {
|
||||
const elementRef = shallowRef<HTMLElement | null>(null);
|
||||
const slotRef = shallowRef<HTMLElement | null>(null);
|
||||
const [initialize, instance] = useOverlayScrollbars(props);
|
||||
|
||||
const exposed: OverlayScrollbarsComponentRef = {
|
||||
instance,
|
||||
element: () => elementRef.value,
|
||||
};
|
||||
|
||||
expose(exposed);
|
||||
|
||||
watchPostEffect((onCleanup) => {
|
||||
const { value: elm } = elementRef;
|
||||
const { value: slotElm } = slotRef;
|
||||
|
||||
if (elm && slotElm) {
|
||||
const osInstance = initialize({
|
||||
target: elm,
|
||||
elements: {
|
||||
viewport: slotElm,
|
||||
content: slotElm,
|
||||
},
|
||||
});
|
||||
onCleanup(() => osInstance.destroy());
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => instance()?.destroy());
|
||||
|
||||
return { elementRef, slotRef, element: toRef(props, 'element') };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ msg }}</h1>
|
||||
<h3>
|
||||
Works!
|
||||
<a target="_blank" href="https://vitejs.dev/">Vite</a> +
|
||||
<a target="_blank" href="https://v2.vuejs.org/">Vue 2</a>.
|
||||
</h3>
|
||||
</div>
|
||||
<component :is="element" ref="elementRef" data-overlayscrollbars="">
|
||||
<div ref="slotRef">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as OverlayScrollbarsComponent } from './OverlayScrollbarsComponent.vue';
|
||||
export * from './useOverlayScrollbars';
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { shallowRef, unref, watch } from 'vue';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import type { Ref } from 'vue';
|
||||
import type { InitializationTarget } from 'overlayscrollbars';
|
||||
import type {
|
||||
OverlayScrollbarsComponentProps,
|
||||
OverlayScrollbarsComponentRef,
|
||||
} from './OverlayScrollbarsComponent.types';
|
||||
|
||||
type Options = OverlayScrollbarsComponentProps['options'];
|
||||
type Events = OverlayScrollbarsComponentProps['events'];
|
||||
|
||||
export interface UseOverlayScrollbarsParams {
|
||||
/** OverlayScrollbars options. */
|
||||
options?: Options | Ref<Options | undefined>;
|
||||
/** OverlayScrollbars events. */
|
||||
events?: Events | Ref<Events | undefined>;
|
||||
}
|
||||
|
||||
export type UseOverlayScrollbarsInitialization = (
|
||||
target: InitializationTarget
|
||||
) => OverlayScrollbars;
|
||||
|
||||
export type UseOverlayScrollbarsInstance = () => ReturnType<
|
||||
OverlayScrollbarsComponentRef['instance']
|
||||
>;
|
||||
|
||||
/**
|
||||
* Composable for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough)
|
||||
* @param params Parameters for customization.
|
||||
* @returns A tuple with two values:
|
||||
* The first value is the initialization function, it takes one argument which is the `InitializationTarget` and returns the OverlayScrollbars instance.
|
||||
* The second value is a function which returns the current OverlayScrollbars instance or `null` if not initialized.
|
||||
*/
|
||||
export const useOverlayScrollbars = (
|
||||
params?: UseOverlayScrollbarsParams | Ref<UseOverlayScrollbarsParams>
|
||||
): [UseOverlayScrollbarsInitialization, UseOverlayScrollbarsInstance] => {
|
||||
const paramsRef = shallowRef(params || {});
|
||||
const variables = shallowRef<{
|
||||
instance: ReturnType<UseOverlayScrollbarsInstance>;
|
||||
options?: Options;
|
||||
events?: Events;
|
||||
}>({
|
||||
instance: null,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(paramsRef.value.options),
|
||||
() => {
|
||||
const {
|
||||
value: { options: rawOptions },
|
||||
} = paramsRef;
|
||||
const {
|
||||
value: { instance },
|
||||
} = variables;
|
||||
const options = unref(rawOptions);
|
||||
|
||||
variables.value.options = options;
|
||||
|
||||
if (OverlayScrollbars.valid(instance)) {
|
||||
instance.options(options || {}, true);
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => unref(paramsRef.value.events),
|
||||
() => {
|
||||
const {
|
||||
value: { events: rawEvents },
|
||||
} = paramsRef;
|
||||
const {
|
||||
value: { instance },
|
||||
} = variables;
|
||||
const events = unref(rawEvents);
|
||||
|
||||
variables.value.events = events;
|
||||
|
||||
if (OverlayScrollbars.valid(instance)) {
|
||||
instance.on(events || {}, true);
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
return [
|
||||
(target: InitializationTarget): OverlayScrollbars => {
|
||||
// if already initialized return the current instance
|
||||
const {
|
||||
value: { instance, options, events },
|
||||
} = variables;
|
||||
if (OverlayScrollbars.valid(instance)) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
const currOptions = options || {};
|
||||
const currEvents = events || {};
|
||||
const osInstance = (variables.value.instance = OverlayScrollbars(
|
||||
target,
|
||||
currOptions,
|
||||
currEvents
|
||||
));
|
||||
|
||||
return osInstance;
|
||||
},
|
||||
() => variables.value.instance,
|
||||
];
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { OverlayScrollbarsComponent } from '~/overlayscrollbars-vue';
|
||||
|
||||
describe('OverlayScrollbarsComponent', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(OverlayScrollbarsComponent, { propsData: { msg: 'Hello Vitest' } });
|
||||
expect(wrapper.text()).toContain('Hello Vitest');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import { onMounted, ref, toRefs } from 'vue';
|
||||
import { describe, test, expect, vitest } from 'vitest';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import { render, screen } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { OverlayScrollbarsComponent } from '~/overlayscrollbars-vue';
|
||||
|
||||
describe('OverlayScrollbarsComponent', () => {
|
||||
describe('correct rendering', () => {
|
||||
test('correct root element with instance', async () => {
|
||||
const elementA = 'code';
|
||||
const elementB = 'span';
|
||||
let osInstance;
|
||||
const { container, rerender } = render(OverlayScrollbarsComponent);
|
||||
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
expect(container.querySelector('div')).toBe(container.firstElementChild); // default is div
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false);
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
|
||||
await rerender({ element: elementA });
|
||||
expect(container.querySelector(elementA)).toBe(container.firstElementChild);
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
|
||||
await rerender({ element: elementB });
|
||||
expect(container.querySelector(elementB)).toBe(container.firstElementChild);
|
||||
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed
|
||||
osInstance = OverlayScrollbars(container.firstElementChild as HTMLElement);
|
||||
expect(osInstance).toBeDefined();
|
||||
expect(OverlayScrollbars.valid(osInstance)).toBe(true);
|
||||
});
|
||||
|
||||
test('children', () => {
|
||||
const { container } = render(OverlayScrollbarsComponent, {
|
||||
slots: { default: 'hello <span>vue</span>' },
|
||||
});
|
||||
expect(screen.getByText(/hello/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/vue/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/vue/).parentElement).not.toBe(container.firstElementChild);
|
||||
});
|
||||
|
||||
test('dynamic children', async () => {
|
||||
render(() => {
|
||||
const elements = ref(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayScrollbarsComponent>
|
||||
{elements.value === 0 ? 'empty' : null}
|
||||
{[...Array(elements.value).keys()].map((i) => (
|
||||
<span key={i}>{i}</span>
|
||||
))}
|
||||
</OverlayScrollbarsComponent>
|
||||
<button onClick={() => (elements.value += 1)}>add</button>
|
||||
<button onClick={() => (elements.value -= 1)}>remove</button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
const addBtn = screen.getByText('add');
|
||||
const removeBtn = screen.getByText('remove');
|
||||
const initialElement = screen.getByText('0');
|
||||
expect(initialElement).toBeInTheDocument();
|
||||
|
||||
const initialElementParent = initialElement.parentElement;
|
||||
expect(initialElementParent).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(addBtn);
|
||||
expect((await screen.findByText('1')).parentElement).toBe(initialElementParent);
|
||||
|
||||
await userEvent.click(removeBtn);
|
||||
await userEvent.click(removeBtn);
|
||||
expect(await screen.findByText('empty')).toBe(initialElementParent);
|
||||
});
|
||||
|
||||
test('className', async () => {
|
||||
const { container, rerender } = render(OverlayScrollbarsComponent, {
|
||||
props: {
|
||||
class: 'overlay scrollbars',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.firstElementChild).toHaveClass('overlay', 'scrollbars');
|
||||
|
||||
await rerender({ class: 'overlay scrollbars vue' });
|
||||
|
||||
expect(container.firstElementChild).toHaveClass('overlay', 'scrollbars', 'vue');
|
||||
});
|
||||
|
||||
test('style', async () => {
|
||||
const { container, rerender } = render(OverlayScrollbarsComponent, {
|
||||
props: {
|
||||
style: { width: '22px' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.firstElementChild).toHaveStyle({ width: '22px' });
|
||||
|
||||
await rerender({ style: { height: '33px' } });
|
||||
|
||||
expect(container.firstElementChild).toHaveStyle({ height: '33px' });
|
||||
});
|
||||
});
|
||||
|
||||
test('ref', () => {
|
||||
const osRef = ref();
|
||||
const { container } = render({
|
||||
setup() {
|
||||
const componentRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
osRef.value = componentRef.value;
|
||||
});
|
||||
|
||||
return () => <OverlayScrollbarsComponent ref={componentRef} />;
|
||||
},
|
||||
});
|
||||
|
||||
const { instance, element } = osRef.value!;
|
||||
expect(instance).toBeTypeOf('function');
|
||||
expect(element).toBeTypeOf('function');
|
||||
expect(OverlayScrollbars.valid(instance())).toBe(true);
|
||||
expect(element()).toBe(container.firstElementChild);
|
||||
});
|
||||
|
||||
test('options', async () => {
|
||||
const osRef = ref();
|
||||
const { rerender } = render(
|
||||
{
|
||||
setup(props: any) {
|
||||
const { options } = toRefs(props);
|
||||
const componentRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
osRef.value = componentRef.value;
|
||||
});
|
||||
|
||||
return () => <OverlayScrollbarsComponent options={options} ref={componentRef} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
options: { paddingAbsolute: true, overflow: { y: 'hidden' } },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const instance = osRef.value!.instance()!;
|
||||
|
||||
const opts = instance.options();
|
||||
expect(opts.paddingAbsolute).toBe(true);
|
||||
expect(opts.overflow.y).toBe('hidden');
|
||||
|
||||
await rerender({ options: { overflow: { x: 'hidden' } } });
|
||||
|
||||
const newOpts = instance.options();
|
||||
expect(newOpts.paddingAbsolute).toBe(false); //switches back to default because its not specified in the new options
|
||||
expect(newOpts.overflow.x).toBe('hidden');
|
||||
expect(newOpts.overflow.y).toBe('scroll'); //switches back to default because its not specified in the new options
|
||||
|
||||
// instance didn't change
|
||||
expect(instance).toBe(osRef.value!.instance());
|
||||
|
||||
await rerender({ element: 'span', options: { overflow: { x: 'hidden', y: 'hidden' } } });
|
||||
|
||||
const newElementInstance = osRef.value!.instance()!;
|
||||
const newElementNewOpts = newElementInstance.options();
|
||||
expect(newElementInstance).not.toBe(instance);
|
||||
expect(newElementNewOpts.paddingAbsolute).toBe(false);
|
||||
expect(newElementNewOpts.overflow.x).toBe('hidden');
|
||||
expect(newElementNewOpts.overflow.y).toBe('hidden');
|
||||
|
||||
// reset options with `undefined`, `null`, `false` or `{}`
|
||||
await rerender({ options: undefined });
|
||||
|
||||
const clearedOpts = newElementInstance.options();
|
||||
expect(osRef.value!.instance()).toBe(newElementInstance);
|
||||
expect(clearedOpts.paddingAbsolute).toBe(false);
|
||||
expect(clearedOpts.overflow.x).toBe('scroll');
|
||||
expect(clearedOpts.overflow.y).toBe('scroll');
|
||||
});
|
||||
|
||||
test('events', async () => {
|
||||
const onUpdatedInitial = vitest.fn();
|
||||
const onUpdated = vitest.fn();
|
||||
const osRef = ref();
|
||||
const { rerender } = render(
|
||||
{
|
||||
setup(props: any) {
|
||||
const { events } = toRefs(props);
|
||||
const componentRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
osRef.value = componentRef.value;
|
||||
});
|
||||
|
||||
return () => <OverlayScrollbarsComponent options={events} ref={componentRef} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
events: { updated: onUpdatedInitial },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const instance = osRef.value!.instance()!;
|
||||
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
|
||||
await rerender({ events: { updated: onUpdated } });
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
|
||||
await rerender({ events: { updated: [onUpdated, onUpdatedInitial] } });
|
||||
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(2);
|
||||
|
||||
// unregister works with `[]`, `null` or `undefined`
|
||||
await rerender({ events: { updated: null } });
|
||||
|
||||
instance.update(true);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(2);
|
||||
|
||||
// instance didn't change
|
||||
expect(instance).toBe(osRef.value!.instance());
|
||||
|
||||
await rerender({ element: 'span', events: { updated: [onUpdated, onUpdatedInitial] } });
|
||||
|
||||
const newElementInstance = osRef.value!.instance()!;
|
||||
expect(newElementInstance).not.toBe(instance);
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(3);
|
||||
|
||||
// reset events with `undefined`, `null`, `false` or `{}`
|
||||
await rerender({ events: undefined });
|
||||
|
||||
newElementInstance.update(true);
|
||||
expect(newElementInstance).toBe(osRef.value!.instance());
|
||||
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
|
||||
expect(onUpdated).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import { reactive, onMounted, ref, watch, toRaw, watchPostEffect } from 'vue';
|
||||
import { describe, test, expect, vitest } from 'vitest';
|
||||
import { render, screen } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useOverlayScrollbars } from '~/overlayscrollbars-vue';
|
||||
import type { PartialOptions, EventListeners, OverlayScrollbars } from 'overlayscrollbars';
|
||||
|
||||
describe('useOverlayScrollbars', () => {
|
||||
test('re-initialization', () => {
|
||||
const { unmount } = render({
|
||||
setup() {
|
||||
const instanceRef = ref<OverlayScrollbars | null>(null);
|
||||
const [initialize, instance] = useOverlayScrollbars();
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
const osInstance = initialize(event.target as HTMLElement);
|
||||
if (instanceRef.value) {
|
||||
expect(toRaw(instanceRef.value)).toBe(osInstance);
|
||||
expect(toRaw(instanceRef.value)).toBe(instance());
|
||||
}
|
||||
instanceRef.value = osInstance;
|
||||
expect(toRaw(instanceRef.value)).toBe(instance());
|
||||
}}
|
||||
>
|
||||
initialize
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const initializeBtn = screen.getByRole('button');
|
||||
userEvent.click(initializeBtn);
|
||||
// taking snapshot here wouldn't be equal because of "tabindex" attribute of the viewport element
|
||||
userEvent.click(initializeBtn);
|
||||
const snapshot = initializeBtn.innerHTML;
|
||||
userEvent.click(initializeBtn);
|
||||
|
||||
expect(snapshot).toBe(initializeBtn.innerHTML);
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('reactive params', async () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
const onUpdated = vitest.fn();
|
||||
const { unmount } = render({
|
||||
setup() {
|
||||
const div = ref<HTMLElement | null>(null);
|
||||
const params = reactive<{ options?: PartialOptions; events?: EventListeners }>({});
|
||||
const [initialize, instance] = useOverlayScrollbars(params);
|
||||
|
||||
onMounted(() => {
|
||||
osInstance = initialize({ target: div.value! });
|
||||
});
|
||||
|
||||
watch(
|
||||
() => params,
|
||||
() => {
|
||||
if (params.events!.updated) {
|
||||
instance()?.update(true);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div ref={div} />
|
||||
<button
|
||||
onClick={() => {
|
||||
params.options = {};
|
||||
params.events = {};
|
||||
params.options!.paddingAbsolute = true;
|
||||
params.events!.updated = onUpdated;
|
||||
}}
|
||||
>
|
||||
trigger
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
const triggerBtn = screen.getByRole('button');
|
||||
await userEvent.click(triggerBtn);
|
||||
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(osInstance!.options().paddingAbsolute).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('ref params', async () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
const onUpdated = vitest.fn();
|
||||
const { unmount } = render({
|
||||
setup() {
|
||||
const div = ref<HTMLElement | null>(null);
|
||||
const params = ref<{ options?: PartialOptions; events?: EventListeners }>({});
|
||||
const [initialize, instance] = useOverlayScrollbars(params);
|
||||
|
||||
onMounted(() => {
|
||||
osInstance = initialize({ target: div.value! });
|
||||
});
|
||||
|
||||
watchPostEffect(() => {
|
||||
if (params.value.events?.updated) {
|
||||
instance()?.update(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div ref={div} />
|
||||
<button
|
||||
onClick={() => {
|
||||
params.value.options = {};
|
||||
params.value.events = {};
|
||||
params.value.options.paddingAbsolute = true;
|
||||
params.value.events.updated = onUpdated;
|
||||
}}
|
||||
>
|
||||
trigger
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
const triggerBtn = screen.getByRole('button');
|
||||
await userEvent.click(triggerBtn);
|
||||
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(osInstance!.options().paddingAbsolute).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('ref params fields', async () => {
|
||||
let osInstance: OverlayScrollbars;
|
||||
const onUpdated = vitest.fn();
|
||||
const { unmount } = render({
|
||||
setup() {
|
||||
const div = ref<HTMLElement | null>(null);
|
||||
const options = ref<PartialOptions | undefined>();
|
||||
const events = ref<EventListeners | undefined>();
|
||||
const [initialize, instance] = useOverlayScrollbars({
|
||||
options,
|
||||
events,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
osInstance = initialize({ target: div.value! });
|
||||
});
|
||||
|
||||
watchPostEffect(() => {
|
||||
if (events.value?.updated) {
|
||||
instance()?.update(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div ref={div} />
|
||||
<button
|
||||
onClick={() => {
|
||||
options.value = {};
|
||||
events.value = {};
|
||||
options.value.paddingAbsolute = true;
|
||||
events.value.updated = onUpdated;
|
||||
}}
|
||||
>
|
||||
trigger
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(onUpdated).not.toHaveBeenCalled();
|
||||
|
||||
const triggerBtn = screen.getByRole('button');
|
||||
await userEvent.click(triggerBtn);
|
||||
|
||||
expect(onUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(osInstance!.options().paddingAbsolute).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"jsx": "preserve",
|
||||
"types": ["jest", "@testing-library/jest-dom"]
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src/",
|
||||
"outDir": "dist",
|
||||
"outDir": "./dist/types",
|
||||
"declaration": true,
|
||||
"types": []
|
||||
},
|
||||
|
||||
@@ -2,6 +2,9 @@ import { resolve } from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
import { esbuildResolve } from 'rollup-plugin-esbuild-resolve';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import rollupPluginPackageJson from '@~local/rollup/plugin/packageJson';
|
||||
import rollupPluginCopy from '@~local/rollup/plugin/copy';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -13,13 +16,52 @@ export default defineConfig({
|
||||
fileName: (format) => `overlayscrollbars-vue.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue'],
|
||||
external: ['vue', 'overlayscrollbars'],
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
overlayscrollbars: 'OverlayScrollbarsGlobal',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
rollupPluginCopy({ paths: ['README.md'] }),
|
||||
rollupPluginPackageJson({
|
||||
json: ({
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
author,
|
||||
license,
|
||||
homepage,
|
||||
bugs,
|
||||
repository,
|
||||
keywords,
|
||||
peerDependencies,
|
||||
}) => {
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
author,
|
||||
license,
|
||||
homepage,
|
||||
bugs,
|
||||
repository,
|
||||
keywords,
|
||||
main: 'overlayscrollbars-vue.umd.js',
|
||||
module: 'overlayscrollbars-vue.es.js',
|
||||
types: 'types/overlayscrollbars-vue.d.ts',
|
||||
peerDependencies,
|
||||
sideEffects: false,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [esbuildResolve(), vue()],
|
||||
plugins: [
|
||||
esbuildResolve(),
|
||||
vue(),
|
||||
vueJsx(), // used for testing
|
||||
],
|
||||
});
|
||||
|
||||
@@ -174,9 +174,10 @@ export interface OverlayScrollbars {
|
||||
/**
|
||||
* Adds event listeners to the instance.
|
||||
* @param eventListeners An object which contains the added listeners.
|
||||
* @param pure If true all already added event listeners will be removed before the new listeners are added.
|
||||
* @returns Returns a function which removes the added listeners.
|
||||
*/
|
||||
on(eventListeners: EventListeners): () => void;
|
||||
on(eventListeners: EventListeners, pure?: boolean): () => void;
|
||||
/**
|
||||
* Adds an event listener to the instance.
|
||||
* @param name The name of the event.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isArray, isFunction, isString } from '~/support/utils/types';
|
||||
import { isArray, isBoolean, isFunction, isString } from '~/support/utils/types';
|
||||
import { keys } from '~/support/utils/object';
|
||||
import { each, push, from, isEmptyArray, runEachAndClear } from '~/support/utils/array';
|
||||
|
||||
@@ -20,12 +20,12 @@ export type RemoveEvent<EventMap extends Record<string, any[]>> = {
|
||||
};
|
||||
|
||||
export type AddEvent<EventMap extends Record<string, any[]>> = {
|
||||
(eventListeners: EventListeners<EventMap>): () => void;
|
||||
(eventListeners: EventListeners<EventMap>, pure?: boolean): () => void;
|
||||
<N extends keyof EventMap>(name: N, listener: EventListener<EventMap, N>): () => void;
|
||||
<N extends keyof EventMap>(name: N, listener: EventListener<EventMap, N>[]): () => void;
|
||||
<N extends keyof EventMap>(
|
||||
nameOrEventListeners: N | EventListeners<EventMap>,
|
||||
listener?: EventListener<EventMap, N> | EventListener<EventMap, N>[]
|
||||
listener?: EventListener<EventMap, N> | EventListener<EventMap, N>[] | boolean
|
||||
): () => void;
|
||||
};
|
||||
|
||||
@@ -67,16 +67,25 @@ export const createEventListenerHub = <EventMap extends Record<string, any[]>>(
|
||||
}
|
||||
};
|
||||
|
||||
const addEvent: AddEvent<EventMap> = ((nameOrEventListeners, listener) => {
|
||||
const addEvent: AddEvent<EventMap> = ((
|
||||
nameOrEventListeners: keyof EventMap | EventListeners<EventMap>,
|
||||
listenerOrPure:
|
||||
| EventListener<EventMap, keyof EventMap>
|
||||
| EventListener<EventMap, keyof EventMap>[]
|
||||
| boolean
|
||||
) => {
|
||||
if (isString(nameOrEventListeners)) {
|
||||
const eventSet = events.get(nameOrEventListeners) || new Set();
|
||||
events.set(nameOrEventListeners, eventSet);
|
||||
|
||||
manageListener((currListener) => {
|
||||
isFunction(currListener) && eventSet.add(currListener);
|
||||
}, listener as any);
|
||||
}, listenerOrPure as any);
|
||||
|
||||
return removeEvent.bind(0, nameOrEventListeners as any, listener as any);
|
||||
return removeEvent.bind(0, nameOrEventListeners as any, listenerOrPure as any);
|
||||
}
|
||||
if (isBoolean(listenerOrPure) && listenerOrPure) {
|
||||
removeEvent();
|
||||
}
|
||||
|
||||
const eventListenerKeys = keys(nameOrEventListeners) as (keyof EventListeners<EventMap>)[];
|
||||
|
||||
@@ -128,6 +128,30 @@ describe('eventListeners', () => {
|
||||
|
||||
expect(onUndefined).toHaveBeenCalledTimes(1);
|
||||
expect(onUndefined).toHaveBeenLastCalledWith();
|
||||
|
||||
addEvent(
|
||||
{
|
||||
onBoolean: [onBooleanA, onBooleanB],
|
||||
onString: [onString, onString],
|
||||
onUndefined,
|
||||
},
|
||||
true
|
||||
);
|
||||
addEvent(
|
||||
{
|
||||
onUndefined,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
triggerEvent('onBoolean', [true, 'hi']);
|
||||
triggerEvent('onString', ['hi']);
|
||||
triggerEvent('onUndefined');
|
||||
|
||||
expect(onBooleanA).toHaveBeenCalledTimes(1);
|
||||
expect(onBooleanB).toHaveBeenCalledTimes(1);
|
||||
expect(onString).toHaveBeenCalledTimes(1);
|
||||
expect(onUndefined).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('removeEvent', () => {
|
||||
|
||||
Reference in New Issue
Block a user