diff --git a/package-lock.json b/package-lock.json index 19e8d80..70ce929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29556,6 +29556,7 @@ "@sveltejs/package": "next", "@sveltejs/vite-plugin-svelte": "^1.0.9", "@testing-library/svelte": "^3.2.2", + "overlayscrollbars": "file:./../overlayscrollbars/dist", "svelte": "^3.44.0", "svelte-check": "^2.7.1", "svelte-preprocess": "^4.10.6", @@ -29563,6 +29564,7 @@ "typescript": "^4.7.4" }, "peerDependencies": { + "overlayscrollbars": "^2.0.0", "svelte": "^3.44.0" } }, @@ -29623,6 +29625,10 @@ "optional": true, "peer": true }, + "packages/overlayscrollbars-svelte/node_modules/overlayscrollbars": { + "resolved": "packages/overlayscrollbars/dist", + "link": true + }, "packages/overlayscrollbars-svelte/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -45348,6 +45354,7 @@ "@sveltejs/package": "next", "@sveltejs/vite-plugin-svelte": "^1.0.9", "@testing-library/svelte": "^3.2.2", + "overlayscrollbars": "file:../overlayscrollbars/dist", "svelte": "^3.44.0", "svelte-check": "^2.7.1", "svelte-preprocess": "^4.10.6", @@ -45405,6 +45412,9 @@ "optional": true, "peer": true }, + "overlayscrollbars": { + "version": "file:packages/overlayscrollbars/dist" + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/packages/overlayscrollbars-ngx/README.md b/packages/overlayscrollbars-ngx/README.md index 3c60cdf..5a7d2b7 100644 --- a/packages/overlayscrollbars-ngx/README.md +++ b/packages/overlayscrollbars-ngx/README.md @@ -113,11 +113,13 @@ Additionally to the `events` property the `OverlayScrollbarsComponent` emits "na > ``` +All events are typed, but you can use the `EventListenerArgs` type as utility in case its needed: + ```ts import type { EventListenerArgs } from 'overlayscrollbars'; // example listener -onUpdated([instance, onUpdatedArgs]: EventListenerArgs['updated']) {} +const onUpdated = ([instance, onUpdatedArgs]: EventListenerArgs['updated']) => {} ``` ### Ref diff --git a/packages/overlayscrollbars-svelte/README.md b/packages/overlayscrollbars-svelte/README.md index 097cfd8..abe0846 100644 --- a/packages/overlayscrollbars-svelte/README.md +++ b/packages/overlayscrollbars-svelte/README.md @@ -7,6 +7,8 @@ [![OverlayScrollbars](https://img.shields.io/badge/OverlayScrollbars-%5E2.0.0-338EFF?style=flat-square)](https://github.com/KingSora/OverlayScrollbars) [![Svelte](https://img.shields.io/badge/Svelte-%5E3.44.0-FF3E00?style=flat-square&logo=svelte)](https://github.com/sveltejs/svelte) + [![Downloads](https://img.shields.io/npm/dt/overlayscrollbars-svelte.svg?style=flat-square)](https://www.npmjs.com/package/overlayscrollbars-svelte) + [![Version](https://img.shields.io/npm/v/overlayscrollbars-svelte.svg?style=flat-square)](https://www.npmjs.com/package/overlayscrollbars-svelte) [![License](https://img.shields.io/github/license/kingsora/overlayscrollbars.svg?style=flat-square)](#) @@ -15,4 +17,103 @@ This is the official OverlayScrollbars Svelte wrapper. -# This project is a WIP \ No newline at end of file +## Installation + +```sh +npm install overlayscrollbars-svelte +``` + +## Peer Dependencies + +OverlayScrollbars for Vue has the following **peer dependencies**: + +- The vanilla JavaScript library: [overlayscrollbars](https://www.npmjs.com/package/overlayscrollbars) + +``` +npm install overlayscrollbars +``` + +- The Vue framework: [svelte](https://www.npmjs.com/package/svelte) + +``` +npm install svelte +``` + +## Usage + +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"; + +// ... + + + example content + +``` + +### 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 + { /* ... */ } }} +/> +``` + +### Events + +Additionally to the `events` property the `OverlayScrollbarsComponent` emits "native" Svelte events. To prevent name collisions with DOM events the events have a `os` prefix. + +> __Note__: It doesn't matter whether you use the `events` property or the Svelte events or both. + +```jsx +// example usage + +``` + +All events are typed, but you can use the `EventListenerArgs` type as utility in case its needed: + +```ts +import type { EventListenerArgs } from 'overlayscrollbars'; + +// example listener +const onUpdated = (event) => { + const [instance, onUpdatedArgs] = event.detail as EventListenerArgs['updated']; +} +``` + +### 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: + +- `osInstance`: a function which returns the OverlayScrollbars instance. +- `getElement`: a function which returns the root element. + +## License + +MIT diff --git a/packages/overlayscrollbars-svelte/package.json b/packages/overlayscrollbars-svelte/package.json index 5342f1b..198f96f 100644 --- a/packages/overlayscrollbars-svelte/package.json +++ b/packages/overlayscrollbars-svelte/package.json @@ -4,7 +4,8 @@ "version": "0.4.0", "type": "module", "peerDependencies": { - "svelte": "^3.44.0" + "svelte": "^3.44.0", + "overlayscrollbars": "^2.0.0" }, "devDependencies": { "@sveltejs/adapter-auto": "next", @@ -12,6 +13,7 @@ "@sveltejs/package": "next", "@sveltejs/vite-plugin-svelte": "^1.0.9", "@testing-library/svelte": "^3.2.2", + "overlayscrollbars": "file:./../overlayscrollbars/dist", "svelte": "^3.44.0", "svelte-check": "^2.7.1", "svelte-preprocess": "^4.10.6", diff --git a/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.svelte b/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.svelte index 0a45b69..cd063fb 100644 --- a/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.svelte +++ b/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.svelte @@ -1,3 +1,101 @@ -

Welcome to your library project

-

Create your package using @sveltejs/package and preview/showcase your work with SvelteKit

-

Visit kit.svelte.dev to read the documentation

+ + + +
+ +
+
\ No newline at end of file diff --git a/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.types.ts b/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.types.ts new file mode 100644 index 0000000..e4f14fd --- /dev/null +++ b/packages/overlayscrollbars-svelte/src/OverlayScrollbarsComponent.types.ts @@ -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. */ + osInstance(): OverlayScrollbars | null; + /** Returns the root element. */ + getElement(): HTMLElement | null; +} diff --git a/packages/overlayscrollbars-svelte/test/OverlayScrollbarsComponent.test.ts b/packages/overlayscrollbars-svelte/test/OverlayScrollbarsComponent.test.ts index d7962df..25ff18f 100644 --- a/packages/overlayscrollbars-svelte/test/OverlayScrollbarsComponent.test.ts +++ b/packages/overlayscrollbars-svelte/test/OverlayScrollbarsComponent.test.ts @@ -1,14 +1,368 @@ -import { describe, it, expect, afterEach } from 'vitest'; -import { cleanup, render } from '@testing-library/svelte'; +import { describe, test, expect, afterEach, vitest } from 'vitest'; +import { cleanup, render, screen, fireEvent } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbarsComponent } from '~/index'; // eslint-disable-line import/named +import type { OverlayScrollbarsComponentRef } from '~/index'; // eslint-disable-line import/named +import Test from './Test.svelte'; -describe('Hello.svelte', () => { - // TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach +/** + * rerender would unmount and re-mount component... so I am faking it with custom event... + */ +describe('OverlayScrollbarsComponent', () => { afterEach(() => cleanup()); - it('mounts', () => { - const { container } = render(OverlayScrollbarsComponent); - expect(container).toBeTruthy(); - expect(container.innerHTML).toContain('Welcome to your library project'); + describe('correct rendering', () => { + test('correct root element with instance', async () => { + const elementA = 'code'; + const elementB = 'span'; + let osInstance; + const { container } = render(Test); + const realContainer = container.firstElementChild!; + + expect(realContainer).not.toBeEmptyDOMElement(); + expect(realContainer.querySelector('div')).toBe(realContainer.firstElementChild); // default is div + + expect(OverlayScrollbars.valid(osInstance)).toBe(false); + osInstance = OverlayScrollbars(realContainer.firstElementChild as HTMLElement); + + expect(osInstance).toBeDefined(); + expect(OverlayScrollbars.valid(osInstance)).toBe(true); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { element: elementA }, + }) + ); + expect(realContainer.querySelector(elementA)).toBe(realContainer.firstElementChild); + + expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed + osInstance = OverlayScrollbars(realContainer.firstElementChild as HTMLElement); + expect(osInstance).toBeDefined(); + expect(OverlayScrollbars.valid(osInstance)).toBe(true); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { element: elementB }, + }) + ); + expect(realContainer.querySelector(elementB)).toBe(realContainer.firstElementChild); + + expect(OverlayScrollbars.valid(osInstance)).toBe(false); // prev instance is destroyed + osInstance = OverlayScrollbars(realContainer.firstElementChild as HTMLElement); + expect(osInstance).toBeDefined(); + expect(OverlayScrollbars.valid(osInstance)).toBe(true); + }); + + test('data-overlayscrollbars-initialize', async () => { + const { container } = render(OverlayScrollbarsComponent); + + expect(container.querySelector('[data-overlayscrollbars-initialize]')).toBeTruthy(); + }); + + test('children', () => { + const { container } = render(Test); + expect(screen.getByText(/hello/)).toBeInTheDocument(); + expect(screen.getByText(/svelte/)).toBeInTheDocument(); + expect(screen.getByText(/svelte/).parentElement).not.toBe(container.firstElementChild); + }); + + test('dynamic children', async () => { + render(Test); + 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(); + + userEvent.click(addBtn); + expect((await screen.findByText('1')).parentElement).toBe(initialElementParent); + + userEvent.click(removeBtn); + userEvent.click(removeBtn); + expect((await screen.findByText('empty')).parentElement).toBe(initialElementParent); + }); + + test('className', async () => { + const { container } = render(Test, { + props: { + className: 'overlay scrollbars', + }, + }); + const realContainer = container.firstElementChild!; + + expect(realContainer.firstElementChild).toHaveClass('overlay', 'scrollbars'); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { className: 'overlay scrollbars svelte' }, + }) + ); + + expect(realContainer.firstElementChild).toHaveClass('overlay', 'scrollbars', 'svelte'); + }); + + test('style', async () => { + const { container } = render(Test, { + props: { + style: 'width: 22px', + }, + }); + const realContainer = container.firstElementChild!; + + expect(realContainer.firstElementChild).toHaveStyle({ width: '22px' }); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { style: 'height: 33px' }, + }) + ); + + expect(realContainer.firstElementChild).toHaveStyle({ height: '33px' }); + }); + }); + + test('ref', () => { + let osRef: OverlayScrollbarsComponentRef | undefined; + const { container } = render(Test, { + props: { + getRef: (ref: any) => { + osRef = ref; + }, + }, + }); + const realContainer = container.firstElementChild!; + + expect(osRef).toBeTruthy(); + + const { osInstance, getElement } = osRef!; + expect(osInstance).toBeTypeOf('function'); + expect(getElement).toBeTypeOf('function'); + expect(OverlayScrollbars.valid(osInstance())).toBe(true); + expect(getElement()).toBe(realContainer.firstElementChild); + }); + + test('options', async () => { + let osRef: OverlayScrollbarsComponentRef | undefined; + render(Test, { + props: { + options: { paddingAbsolute: true, overflow: { y: 'hidden' } }, + getRef: (ref: any) => { + osRef = ref; + }, + }, + }); + + const instance = osRef!.osInstance()!; + + const opts = instance.options(); + expect(opts.paddingAbsolute).toBe(true); + expect(opts.overflow.y).toBe('hidden'); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + 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!.osInstance()); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + element: 'span', + options: { overflow: { x: 'hidden', y: 'hidden' } }, + }, + }) + ); + + const newElementInstance = osRef!.osInstance()!; + 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 fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + options: undefined, + }, + }) + ); + + const clearedOpts = newElementInstance.options(); + expect(osRef!.osInstance()).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(); + let osRef: OverlayScrollbarsComponentRef | undefined; + render(Test, { + props: { + events: { updated: onUpdatedInitial }, + getRef: (ref: any) => { + osRef = ref; + }, + }, + }); + + const instance = osRef!.osInstance()!; + + expect(onUpdatedInitial).toHaveBeenCalledTimes(1); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + events: { updated: onUpdated }, + }, + }) + ); + expect(onUpdated).not.toHaveBeenCalled(); + + instance.update(true); + expect(onUpdatedInitial).toHaveBeenCalledTimes(1); + expect(onUpdated).toHaveBeenCalledTimes(1); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + events: { updated: [onUpdated, onUpdatedInitial] }, + }, + }) + ); + + instance.update(true); + expect(onUpdatedInitial).toHaveBeenCalledTimes(2); + expect(onUpdated).toHaveBeenCalledTimes(2); + + // unregister with `[]`, `null` or `undefined` + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { + events: { updated: null }, + }, + }) + ); + + instance.update(true); + expect(onUpdatedInitial).toHaveBeenCalledTimes(2); + expect(onUpdated).toHaveBeenCalledTimes(2); + + // instance didn't change + expect(instance).toBe(osRef!.osInstance()); + + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { element: 'span', events: { updated: [onUpdated, onUpdatedInitial] } }, + }) + ); + + const newElementInstance = osRef!.osInstance()!; + expect(newElementInstance).not.toBe(instance); + expect(onUpdatedInitial).toHaveBeenCalledTimes(3); + expect(onUpdated).toHaveBeenCalledTimes(3); + + // reset events with `undefined`, `null`, `false` or `{}` + await fireEvent( + screen.getByText('props'), + new CustomEvent('osProps', { + detail: { element: 'span', events: undefined }, + }) + ); + + newElementInstance.update(true); + expect(newElementInstance).toBe(osRef!.osInstance()); + expect(onUpdatedInitial).toHaveBeenCalledTimes(3); + expect(onUpdated).toHaveBeenCalledTimes(3); + }); + + test('destroy', () => { + let osRef: OverlayScrollbarsComponentRef | undefined; + const { unmount } = render(Test, { + props: { + getRef(ref: any) { + osRef = ref; + }, + }, + }); + + const { osInstance } = osRef!; + + expect(OverlayScrollbars.valid(osInstance())).toBe(true); + + unmount(); + + expect(osInstance()).toBeDefined(); + expect(OverlayScrollbars.valid(osInstance())).toBe(false); + }); + + test('dispatch events', async () => { + const initialized = vitest.fn((e: any) => { + const args = e.detail; + expect(args).toEqual([expect.any(Object)]); + }); + const updated = vitest.fn((e: any) => { + const args = e.detail; + expect(args).toEqual([expect.any(Object), expect.any(Object)]); + }); + const destroyed = vitest.fn((e: any) => { + const args = e.detail; + expect(args).toEqual([expect.any(Object), expect.any(Boolean)]); + }); + const scroll = vitest.fn((e: any) => { + const args = e.detail; + expect(args).toEqual([expect.any(Object), expect.any(Event)]); + }); + const { container, unmount } = render(Test, { + props: { + initialized, + updated, + destroyed, + scroll, + }, + }); + + expect(initialized).toHaveBeenCalledTimes(1); + expect(updated).toHaveBeenCalledTimes(1); + expect(destroyed).not.toHaveBeenCalled(); + expect(scroll).not.toHaveBeenCalled(); + + container.querySelectorAll('*').forEach((e) => { + fireEvent.scroll(e); + }); + + expect(destroyed).not.toHaveBeenCalled(); + expect(scroll).toHaveBeenCalledTimes(1); + + unmount(); + + expect(destroyed).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/overlayscrollbars-svelte/test/Test.svelte b/packages/overlayscrollbars-svelte/test/Test.svelte new file mode 100644 index 0000000..520515e --- /dev/null +++ b/packages/overlayscrollbars-svelte/test/Test.svelte @@ -0,0 +1,66 @@ + + + + hello svelte + {#if children === 0}
empty
{/if} + {#each [...Array(children).keys()] as child} +
{child}
+ {/each} +
+ + + + props + \ No newline at end of file diff --git a/packages/overlayscrollbars-svelte/vitest.config.js b/packages/overlayscrollbars-svelte/vitest.config.js index 11d2400..fea6396 100644 --- a/packages/overlayscrollbars-svelte/vitest.config.js +++ b/packages/overlayscrollbars-svelte/vitest.config.js @@ -2,4 +2,12 @@ import { mergeConfig } from 'vite'; import vitestConfig from '@~local/config/vitest'; import viteConfig from './vite.config'; -export default mergeConfig(viteConfig, vitestConfig); +export default mergeConfig(viteConfig, { + test: { + ...vitestConfig.test, + coverage: { + ...vitestConfig.test.coverage, + exclude: [...vitestConfig.test.coverage.exclude, '**/Test.svelte'], + }, + }, +});