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
+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: {