finish svelte version

This commit is contained in:
Rene Haas
2022-11-11 09:13:39 +01:00
parent 5703a801cb
commit 5e8c96140a
9 changed files with 670 additions and 15 deletions
+10
View File
@@ -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",
+3 -1
View File
@@ -113,11 +113,13 @@ Additionally to the `events` property the `OverlayScrollbarsComponent` emits "na
></div>
```
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
+102 -1
View File
@@ -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)](#)
</div>
@@ -15,4 +17,103 @@
This is the official OverlayScrollbars Svelte wrapper.
# This project is a WIP
## 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";
// ...
<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: () => { /* ... */ } }}
/>
```
### 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
<OverlayScrollbarsComponent
on:osInitialized={onInitialized}
on:osUpdated={onUpdated}
on:osDestroyed={onDestroyed}
on:osScroll={onScroll}
/>
```
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
@@ -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",
@@ -1,3 +1,101 @@
<h1>Welcome to your library project</h1>
<p>Create your package using @sveltejs/package and preview/showcase your work with SvelteKit</p>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script lang="ts">
import { onMount, afterUpdate, createEventDispatcher } from 'svelte';
import { OverlayScrollbars } from 'overlayscrollbars';
import type { EventListeners, EventListenerArgs } from 'overlayscrollbars';
import type { OverlayScrollbarsComponentProps, OverlayScrollbarsComponentRef } from './OverlayScrollbarsComponent.types';
type EmitEventsMap = {
[N in keyof EventListenerArgs]: `os${Capitalize<N>}`;
};
export let element: OverlayScrollbarsComponentProps["element"] = 'div';
export let options: OverlayScrollbarsComponentProps["options"] = undefined;
export let events: OverlayScrollbarsComponentProps["events"] = undefined;
let instance: OverlayScrollbars | null = null;
let elementRef: HTMLElement | null = null;
let slotRef: HTMLElement | null = null;
let combinedEvents: OverlayScrollbarsComponentProps["events"] = undefined;
let prevElement: string | undefined;
const initialize = () => {
instance?.destroy();
instance = OverlayScrollbars(
{
target: elementRef!,
elements: {
viewport: slotRef,
content: slotRef
}
},
options || {},
combinedEvents || {}
);
prevElement = element;
return () => instance?.destroy();
};
const dispatchEvents: EmitEventsMap = {
initialized: 'osInitialized',
updated: 'osUpdated',
destroyed: 'osDestroyed',
scroll: 'osScroll',
};
const dispatchEvent = createEventDispatcher<{
osInitialized: EventListenerArgs["initialized"];
osUpdated: EventListenerArgs["updated"];
osDestroyed: EventListenerArgs["destroyed"];
osScroll: EventListenerArgs["scroll"];
}>();
export const osInstance: OverlayScrollbarsComponentRef["osInstance"] = () => instance;
export const getElement: OverlayScrollbarsComponentRef["getElement"] = () => elementRef;
onMount(initialize);
afterUpdate(() => {
if (prevElement !== element) {
initialize();
}
});
$: {
const currEvents = events || {};
combinedEvents = (
Object.keys(dispatchEvents) as (keyof EventListeners)[]
).reduce<EventListeners>(<N extends keyof EventListeners>(obj: EventListeners, name: N) => {
const eventListener = currEvents[name];
obj[name] = [
(...args: EventListenerArgs[N]) =>
dispatchEvent(
dispatchEvents[name],
// @ts-ignore
args
),
...(Array.isArray(eventListener) ? eventListener : [eventListener]).filter(Boolean),
];
return obj;
}, {});
}
$: {
if (OverlayScrollbars.valid(instance)) {
instance.options(options || {}, true);
}
}
$: {
if (OverlayScrollbars.valid(instance)) {
instance.on(
/* c8 ignore next */
combinedEvents || {},
true
);
}
}
</script>
<svelte:element data-overlayscrollbars-initialize="" this={element} bind:this={elementRef} {...$$restProps}>
<div bind:this={slotRef}>
<slot></slot>
</div>
</svelte:element>
@@ -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;
}
@@ -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);
});
});
@@ -0,0 +1,66 @@
<script lang="ts">
import { OverlayScrollbarsComponent } from '~/index';
let children = 1;
let ref: any;
export let element: any = 'div';
export let options: any = undefined;
export let events: any = undefined;
export let getRef: any = undefined;
export let initialized: any = undefined;
export let updated: any = undefined;
export let destroyed: any = undefined;
export let scroll: any = undefined;
export let className: any = undefined;
export let style: any = undefined;
const propsChange = (e: any) => {
const optionsChanged = Object.prototype.hasOwnProperty.call(e.detail, 'options');
const eventsChanged = Object.prototype.hasOwnProperty.call(e.detail, 'events');
const elementChanged = Object.prototype.hasOwnProperty.call(e.detail, 'element');
const classChanged = Object.prototype.hasOwnProperty.call(e.detail, 'className');
const styleChanged = Object.prototype.hasOwnProperty.call(e.detail, 'style');
if (optionsChanged) {
options = e.detail.options;
}
if (eventsChanged) {
events = e.detail.events;
}
if (elementChanged) {
element = e.detail.element;
}
if (classChanged) {
className = e.detail.className;
}
if (styleChanged) {
style = e.detail.style;
}
};
$: {
getRef?.(ref);
}
</script>
<OverlayScrollbarsComponent
bind:this={ref}
element={element}
options={options}
events={events}
class={className}
style={style}
on:osInitialized={initialized}
on:osUpdated={updated}
on:osDestroyed={destroyed}
on:osScroll={scroll}>
hello <span>svelte</span>
{#if children === 0}<div>empty</div>{/if}
{#each [...Array(children).keys()] as child}
<section>{child}</section>
{/each}
</OverlayScrollbarsComponent>
<button id="add" on:click={() => children++}>add</button>
<button id="remove" on:click={() => children--}>remove</button>
<optionsChange on:osProps={propsChange}>
props
</optionsChange>
@@ -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'],
},
},
});