improve api, readmes, overlayscrollbars-react and finish overlayscrollbars-vue

This commit is contained in:
Rene Haas
2022-11-03 23:11:17 +01:00
parent 98676071e1
commit dc79d0c64e
28 changed files with 1788 additions and 1684 deletions
+1 -1
View File
@@ -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
+7 -1521
View File
File diff suppressed because it is too large Load Diff
+725 -10
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -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.
+5 -6
View File
@@ -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: {
+21
View File
@@ -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.
+134 -31
View File
@@ -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
+13
View File
@@ -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

+30 -9
View File
@@ -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();
});
});
+3 -1
View File
@@ -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": []
},
+44 -2
View File
@@ -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', () => {