add overlayscrollbars-solid

This commit is contained in:
Rene Haas
2022-11-11 18:06:25 +01:00
parent ace2071a22
commit 69e8045081
17 changed files with 1677 additions and 1 deletions
@@ -0,0 +1,8 @@
# Compiled output
/dist
# Node
/node_modules
# Miscellaneous
/.coverage
@@ -0,0 +1,6 @@
# Changelog
## 0.4.0
The component was created.
Depends on `OverlayScrollbars` version `^2.0.0` and `Solid` version `^1.5.1`.
+167
View File
@@ -0,0 +1,167 @@
<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://www.solidjs.com"><img src="https://raw.githubusercontent.com/KingSora/OverlayScrollbars/master/packages/overlayscrollbars-solid/logo.svg" width="160" height="160" alt="Vue"></a>
</div>
<br />
<div align="center">
[![OverlayScrollbars](https://img.shields.io/badge/OverlayScrollbars-%5E2.0.0-338EFF?style=flat-square)](https://github.com/KingSora/OverlayScrollbars)
[![Solid](https://img.shields.io/badge/Solid-%5E1.5.1-2C4F7C?style=flat-square&logo=solid)](https://github.com/solidjs/solid)
[![Downloads](https://img.shields.io/npm/dt/overlayscrollbars-solid.svg?style=flat-square)](https://www.npmjs.com/package/overlayscrollbars-solid)
[![Version](https://img.shields.io/npm/v/overlayscrollbars-solid.svg?style=flat-square)](https://www.npmjs.com/package/overlayscrollbars-solid)
[![License](https://img.shields.io/github/license/kingsora/overlayscrollbars.svg?style=flat-square)](#)
</div>
# OverlayScrollbars for Solid
This is the official OverlayScrollbars Solid wrapper.
## Installation
```sh
npm install overlayscrollbars-solid
```
## Peer Dependencies
OverlayScrollbars for Solid has the following **peer dependencies**:
- The vanilla JavaScript library: [overlayscrollbars](https://www.npmjs.com/package/overlayscrollbars)
```
npm install overlayscrollbars
```
- The Solid framework: [solid-js](https://www.npmjs.com/package/solid-js)
```
npm install solid-js
```
## 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-solid";
// ...
<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:
- `osInstance`: a function which returns the OverlayScrollbars instance.
- `getElement`: a function which returns the root element.
```jsx
// example usage
import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-solid';
const Component = () => {
let ref: OverlayScrollbarsComponentRef | undefined;
return <OverlayScrollbarsComponent ref={(r) => (ref = r)} />
}
```
## Primitive
In case the `OverlayScrollbarsComponent` is not enough, you can also use the `createOverlayScrollbars` primitive:
```jsx
import { createOverlayScrollbars } from "overlayscrollbars-solid";
// example usage
const Component = () => {
let div;
const [params, setParams] = createStore({ options, events });
const [initialize, instance] = useOverlayScrollbars(params);
/**
* or:
*
* const [params, setParams] = createSignal<{
* options?: PartialOptions;
* events?: EventListeners;
* }>({});
* const [initialize, instance] = createOverlayScrollbars(params);
*
* or:
*
* const [options, setOptions] = createSignal<PartialOptions | undefined>();
* const [events, setEvents] = createSignal<EventListeners | undefined>();
* const [initialize, instance] = createOverlayScrollbars({
* options,
* events,
* });
*
*/
onMount(() => {
initialize({ target: div });
});
onCleanup(() => {
instance().destroy();
});
return <div ref={div} />
}
```
The primitive 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, `store` or `signal` object. This also applies to the `options` and `events` fields.
### Return
The `createOverlayScrollbars` primitive 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 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,49 @@
{
"name": "overlayscrollbars-solid",
"private": true,
"version": "0.4.0",
"description": "OverlayScrollbars for Solid.",
"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-solid"
},
"keywords": [
"overlayscrollbars",
"solid",
"solidjs",
"component",
"hook",
"hooks",
"primitive",
"styleable",
"scrollbar",
"scrollbars",
"scroll"
],
"main": "./src/overlayscrollbars-solid.ts",
"module": "./src/overlayscrollbars-solid.ts",
"types": "./src/overlayscrollbars-solid.ts",
"peerDependencies": {
"overlayscrollbars": "^2.0.0",
"solid-js": "^1.5.1"
},
"devDependencies": {
"overlayscrollbars": "file:./../overlayscrollbars/dist",
"solid-js": "^1.5.1",
"solid-testing-library": "^0.5.0",
"typescript": "^4.8.2",
"vite": "^3.0.9",
"vite-plugin-solid": "^2.3.0"
},
"scripts": {
"build": "run-p build-js build-types",
"test": "vitest run --coverage",
"build-js": "vite build",
"build-types": "vue-tsc --emitDeclarationOnly -p tsconfig.types.json --composite false"
}
}
@@ -0,0 +1,100 @@
import {
mergeProps,
splitProps,
children,
onCleanup,
createEffect,
createRenderEffect,
createSignal,
} from 'solid-js';
import { Dynamic } from 'solid-js/web';
import type { JSX, ParentProps, ComponentProps, Ref } from 'solid-js';
import type { OverlayScrollbars, PartialOptions, EventListeners } from 'overlayscrollbars';
import { createOverlayScrollbars } from './createOverlayScrollbars';
type InferGeneric<T> = T extends JSX.HTMLAttributes<infer G> ? G : never;
export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElements = 'div'> = Omit<
ComponentProps<T>,
'ref'
> &
ParentProps<{
/** Tag of the root element. */
element?: T;
/** OverlayScrollbars options. */
options?: PartialOptions | false | null;
/** OverlayScrollbars events. */
events?: EventListeners | false | null;
/** OverlayScrollbarsComponent ref. */
ref?: Exclude<Ref<OverlayScrollbarsComponentRef>, OverlayScrollbarsComponentRef>;
}>;
export interface OverlayScrollbarsComponentRef<T extends keyof JSX.IntrinsicElements = 'div'> {
/** Returns the OverlayScrollbars instance or null if not initialized. */
osInstance(): OverlayScrollbars | null;
/** Returns the root element. */
getElement(): InferGeneric<JSX.IntrinsicElements[T]> | null;
}
export const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements = 'div'>(
props: OverlayScrollbarsComponentProps<T>
) => {
const [finalProps, other] = splitProps(
mergeProps({ element: 'div' }, props as OverlayScrollbarsComponentProps),
['element', 'options', 'events', 'ref', 'children']
);
const [elementRef, setElementRef] = createSignal<HTMLDivElement | undefined>();
const [childrenRef, setChildrenRef] = createSignal<HTMLDivElement | undefined>();
const [initialize, instance] = createOverlayScrollbars(finalProps);
createEffect(() => {
const currElement = elementRef();
const currChildrenElement = childrenRef();
if (currElement && currChildrenElement) {
const osInstance = initialize({
target: currElement,
elements: {
viewport: currChildrenElement,
content: currChildrenElement,
},
});
onCleanup(() => {
osInstance.destroy();
});
}
});
createRenderEffect(() => {
finalProps.ref?.({
osInstance: instance,
getElement: () =>
/* c8 ignore next */
elementRef() || null,
});
});
onCleanup(() => {
instance()?.destroy();
});
return (
<Dynamic
component={finalProps.element}
data-overlayscrollbars-initialize=""
ref={(ref: any) => {
setElementRef(ref);
}}
{...other}
>
<div
ref={(ref: any) => {
setChildrenRef(ref);
}}
>
{children(() => finalProps.children)}
</div>
</Dynamic>
);
};
@@ -0,0 +1,70 @@
import { createRenderEffect } from 'solid-js';
import { OverlayScrollbars } from 'overlayscrollbars';
import type { Accessor } from 'solid-js';
import type { Store } from 'solid-js/store';
import type { InitializationTarget } from 'overlayscrollbars';
import type {
OverlayScrollbarsComponentProps,
OverlayScrollbarsComponentRef,
} from './OverlayScrollbarsComponent';
export interface CreateOverlayScrollbarsParams {
/** OverlayScrollbars options. */
options?:
| OverlayScrollbarsComponentProps['options']
| Accessor<OverlayScrollbarsComponentProps['options']>;
/** OverlayScrollbars events. */
events?:
| OverlayScrollbarsComponentProps['events']
| Accessor<OverlayScrollbarsComponentProps['events']>;
}
export type CreateOverlayScrollbarsInitialization = (
target: InitializationTarget
) => OverlayScrollbars;
export type CreateOverlayScrollbarsInstance = () => ReturnType<
OverlayScrollbarsComponentRef['osInstance']
>;
const isAccessor = (obj: any): obj is Accessor<any> => typeof obj === 'function';
const unwrapAccessor = <T>(obj: Accessor<T> | T): T => (isAccessor(obj) ? obj() : obj);
export const createOverlayScrollbars = (
params?:
| CreateOverlayScrollbarsParams
| Accessor<CreateOverlayScrollbarsParams | undefined>
| Store<CreateOverlayScrollbarsParams | undefined>
): [CreateOverlayScrollbarsInitialization, CreateOverlayScrollbarsInstance] => {
let instance: OverlayScrollbars | null = null;
let options: OverlayScrollbarsComponentProps['options'];
let events: OverlayScrollbarsComponentProps['events'];
createRenderEffect(() => {
options = unwrapAccessor(unwrapAccessor(params)?.options);
if (OverlayScrollbars.valid(instance)) {
instance.options(options || {}, true);
}
});
createRenderEffect(() => {
events = unwrapAccessor(unwrapAccessor(params)?.events);
if (OverlayScrollbars.valid(instance)) {
instance.on(events || {}, true);
}
});
return [
(target: InitializationTarget): OverlayScrollbars => {
// if already initialized return the current instance
if (OverlayScrollbars.valid(instance)) {
return instance;
}
return (instance = OverlayScrollbars(target, options || {}, events || {}));
},
() => instance,
];
};
@@ -0,0 +1,2 @@
export * from './OverlayScrollbarsComponent';
export * from './createOverlayScrollbars';
@@ -0,0 +1,395 @@
import { describe, test, afterEach, expect, vitest } from 'vitest';
import { createSignal, createEffect } from 'solid-js';
import { render, screen, cleanup, fireEvent } from 'solid-testing-library';
import userEvent from '@testing-library/user-event';
import { OverlayScrollbars } from 'overlayscrollbars';
import { OverlayScrollbarsComponent } from '~/overlayscrollbars-solid';
import type { OverlayScrollbarsComponentRef } from '~/overlayscrollbars-solid';
const createTestComponent =
(props: any = {}) =>
() => {
let ref: OverlayScrollbarsComponentRef | undefined;
const [element, setElement]: any = createSignal(props.element || 'div');
const [options, setOptions]: any = createSignal(props.options);
const [events, setEvents]: any = createSignal(props.events);
const [className, setClassName]: any = createSignal(props.className);
const [style, setStyle]: any = createSignal(props.style);
createEffect(() => {
props?.getRef?.(ref);
});
return (
<>
<OverlayScrollbarsComponent
element={element()}
options={options()}
events={events()}
class={className()}
style={style()}
ref={(r) => (ref = r)}
/>
<button
// @ts-ignore
on:osProps={(e: CustomEvent) => {
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) {
setOptions(e.detail.options);
}
if (eventsChanged) {
setEvents(e.detail.events);
}
if (elementChanged) {
setElement(e.detail.element);
}
if (classChanged) {
setClassName(e.detail.className);
}
if (styleChanged) {
setStyle(e.detail.style);
}
}}
>
props
</button>
</>
);
};
/**
* rerender doesn't exist... so I am faking it with custom event...
*/
describe('OverlayScrollbarsComponent', () => {
afterEach(() => cleanup());
describe('correct rendering', () => {
test('correct root element with instance', () => {
const elementA = 'code';
const elementB = 'span';
let osInstance;
const { container } = render(createTestComponent());
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);
fireEvent(
screen.getByText('props'),
new CustomEvent('osProps', {
detail: { 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);
fireEvent(
screen.getByText('props'),
new CustomEvent('osProps', {
detail: { 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('data-overlayscrollbars-initialize', async () => {
const { container } = render(() => <OverlayScrollbarsComponent />);
expect(container.querySelector('[data-overlayscrollbars-initialize]')).toBeTruthy();
});
test('children', () => {
const { container } = render(() => (
<OverlayScrollbarsComponent>
hello <span>solid</span>
</OverlayScrollbarsComponent>
));
expect(screen.getByText(/hello/)).toBeInTheDocument();
expect(screen.getByText(/solid/)).toBeInTheDocument();
expect(screen.getByText(/solid/).parentElement).not.toBe(container.firstElementChild);
});
test('dynamic children', async () => {
render(() => {
const [elements, setElements] = createSignal(1);
return (
<>
<OverlayScrollbarsComponent>
{elements() === 0 ? 'empty' : null}
{[...Array(elements()).keys()].map((i) => (
<span>{i}</span>
))}
</OverlayScrollbarsComponent>
<button onClick={() => setElements((curr) => curr + 1)}>add</button>
<button onClick={() => setElements((curr) => curr - 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();
userEvent.click(addBtn);
expect((await screen.findByText('1')).parentElement).toBe(initialElementParent);
userEvent.click(removeBtn);
userEvent.click(removeBtn);
expect(await screen.findByText('empty')).toBe(initialElementParent);
});
test('className', () => {
const { container } = render(
createTestComponent({
className: 'overlay scrollbars',
})
);
expect(container.firstElementChild).toHaveClass('overlay', 'scrollbars');
fireEvent(
screen.getByText('props'),
new CustomEvent('osProps', {
detail: { className: 'overlay scrollbars solid' },
})
);
expect(container.firstElementChild).toHaveClass('overlay', 'scrollbars', 'solid');
});
test('style', () => {
const { container } = render(
createTestComponent({
style: { width: '22px' },
})
);
expect(container.firstElementChild).toHaveStyle({ width: '22px' });
fireEvent(
screen.getByText('props'),
new CustomEvent('osProps', {
detail: { style: { height: '33px' } },
})
);
expect(container.firstElementChild).toHaveStyle({ height: '33px' });
});
});
test('ref', () => {
let osRef: OverlayScrollbarsComponentRef | undefined;
const { container } = render(
createTestComponent({
getRef(ref: any) {
osRef = ref;
},
})
);
expect(osRef).toBeTruthy();
const { osInstance, getElement } = osRef!;
expect(osInstance).toBeTypeOf('function');
expect(getElement).toBeTypeOf('function');
expect(OverlayScrollbars.valid(osInstance())).toBe(true);
expect(getElement()).toBe(container.firstElementChild);
});
test('options', () => {
let osRef: OverlayScrollbarsComponentRef | undefined;
render(
createTestComponent({
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');
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());
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 `{}`
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', () => {
const onUpdatedInitial = vitest.fn();
const onUpdated = vitest.fn();
let osRef: OverlayScrollbarsComponentRef | undefined;
render(
createTestComponent({
events: { updated: onUpdatedInitial },
getRef: (ref: any) => {
osRef = ref;
},
})
);
const instance = osRef!.osInstance()!;
expect(onUpdatedInitial).toHaveBeenCalledTimes(1);
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);
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`
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());
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 `{}`
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(
createTestComponent({
getRef(ref: any) {
osRef = ref;
},
})
);
const { osInstance } = osRef!;
expect(OverlayScrollbars.valid(osInstance())).toBe(true);
unmount();
expect(osInstance()).toBeDefined();
expect(OverlayScrollbars.valid(osInstance())).toBe(false);
});
});
@@ -0,0 +1,186 @@
import { describe, test, afterEach, expect, vitest } from 'vitest';
import { createSignal, createEffect, onMount } from 'solid-js';
import { createStore } from 'solid-js/store';
import { render, screen, cleanup } from 'solid-testing-library';
import userEvent from '@testing-library/user-event';
import { createOverlayScrollbars } from '~/overlayscrollbars-solid';
import type { OverlayScrollbars, PartialOptions, EventListeners } from 'overlayscrollbars';
describe('OverlayScrollbarsComponent', () => {
afterEach(() => cleanup());
test('re-initialization', () => {
const Test = () => {
let instanceRef: OverlayScrollbars | null = null;
const [initialize, instance] = createOverlayScrollbars();
return (
<>
<button
onClick={(event) => {
const osInstance = initialize(event.target as HTMLElement);
if (instanceRef) {
expect(instanceRef).toBe(osInstance);
expect(instanceRef).toBe(instance());
}
instanceRef = osInstance;
expect(instanceRef).toBe(instance());
}}
>
initialize
</button>
</>
);
};
render(Test);
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);
});
test('params store', () => {
let osInstance: OverlayScrollbars;
const onUpdated = vitest.fn();
render(() => {
let div: HTMLDivElement;
const [params, setParams] = createStore<{
options?: PartialOptions;
events?: EventListeners;
}>({});
const [initialize, instance] = createOverlayScrollbars(params);
onMount(() => {
osInstance = initialize({ target: div! });
});
createEffect(() => {
if (params.events?.updated) {
instance()?.update(true);
}
});
return () => (
<>
<div ref={div} />
<button
onClick={() => {
setParams({
options: { paddingAbsolute: true },
events: { updated: onUpdated },
});
}}
>
trigger
</button>
</>
);
});
expect(onUpdated).not.toHaveBeenCalled();
const triggerBtn = screen.getByRole('button');
userEvent.click(triggerBtn);
expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true);
});
test('params signal', () => {
let osInstance: OverlayScrollbars;
const onUpdated = vitest.fn();
render(() => {
let div: HTMLDivElement;
const [params, setParams] = createSignal<{
options?: PartialOptions;
events?: EventListeners;
}>({});
const [initialize, instance] = createOverlayScrollbars(params);
onMount(() => {
osInstance = initialize({ target: div! });
});
createEffect(() => {
if (params().events?.updated) {
instance()?.update(true);
}
});
return () => (
<>
<div ref={div} />
<button
onClick={() => {
setParams({
options: { paddingAbsolute: true },
events: { updated: onUpdated },
});
}}
>
trigger
</button>
</>
);
});
expect(onUpdated).not.toHaveBeenCalled();
const triggerBtn = screen.getByRole('button');
userEvent.click(triggerBtn);
expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true);
});
test('params fields signal', async () => {
let osInstance: OverlayScrollbars;
const onUpdated = vitest.fn();
render(() => {
let div: HTMLDivElement;
const [options, setOptions] = createSignal<PartialOptions | undefined>();
const [events, setEvents] = createSignal<EventListeners | undefined>();
const [initialize, instance] = createOverlayScrollbars({
options,
events,
});
onMount(() => {
osInstance = initialize({ target: div! });
});
createEffect(() => {
if (events()?.updated) {
instance()?.update(true);
}
});
return () => (
<>
<div ref={div} />
<button
onClick={() => {
setOptions({ paddingAbsolute: true });
setEvents({ updated: onUpdated });
}}
>
trigger
</button>
</>
);
});
expect(onUpdated).not.toHaveBeenCalled();
const triggerBtn = screen.getByRole('button');
userEvent.click(triggerBtn);
expect(onUpdated).toHaveBeenCalledTimes(1);
expect(osInstance!.options().paddingAbsolute).toBe(true);
});
});
@@ -0,0 +1,18 @@
{
"extends": "@~local/tsconfig",
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client", "jest", "@testing-library/jest-dom"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src/",
"outDir": "./dist/types",
"declaration": true
},
"include": ["src/**/*"]
}
@@ -0,0 +1,62 @@
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import { esbuildResolve } from 'rollup-plugin-esbuild-resolve';
import solidPlugin from 'vite-plugin-solid';
import rollupPluginPackageJson from '@~local/rollup/plugin/packageJson';
import rollupPluginCopy from '@~local/rollup/plugin/copy';
export default defineConfig({
build: {
sourcemap: true,
outDir: 'dist',
lib: {
formats: ['es', 'cjs'],
entry: resolve(__dirname, 'src/overlayscrollbars-solid.ts'),
name: 'OverlayScrollbarsSolid',
fileName: (format) => `overlayscrollbars-solid.${format}.js`,
},
rollupOptions: {
external: ['solid-js', 'solid-js/web', 'solid-js/store', 'overlayscrollbars'],
output: {
globals: {
overlayscrollbars: 'OverlayScrollbarsGlobal',
},
},
plugins: [
rollupPluginCopy({ paths: ['README.md', 'CHANGELOG.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.cjs.js',
module: 'overlayscrollbars-vue.es.js',
types: 'types/overlayscrollbars-vue.d.ts',
peerDependencies,
sideEffects: false,
};
},
}),
],
},
},
plugins: [esbuildResolve(), solidPlugin()],
});
@@ -0,0 +1,20 @@
import { mergeConfig } from 'vite';
import vitestConfig from '@~local/config/vitest';
import viteConfig from './vite.config';
export default mergeConfig(
{
...viteConfig,
resolve: {
conditions: ['development', 'browser'],
},
},
{
test: {
...vitestConfig.test,
deps: {
inline: [/solid-testing-library/],
},
},
}
);
@@ -3,7 +3,7 @@ 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 type { OverlayScrollbarsComponentRef } from '~/OverlayScrollbarsComponent.types'; // eslint-disable-line import/named
import Test from './Test.svelte';
/**