finish overlayscrollbars-react

This commit is contained in:
Rene Haas
2022-11-01 13:25:44 +01:00
parent fe6262939e
commit 3c882f5475
7 changed files with 197 additions and 31 deletions
@@ -2,6 +2,26 @@
"name": "overlayscrollbars-react",
"private": true,
"version": "0.4.0",
"description": "OverlayScrollbars for React.",
"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-react"
},
"keywords": [
"overlayscrollbars",
"react",
"component",
"hooks",
"styleable",
"scrollbar",
"scrollbars",
"scroll"
],
"main": "./dist/overlayscrollbars-react.umd.js",
"module": "./dist/overlayscrollbars-react.es.js",
"types": "./dist/overlayscrollbars-react.d.ts",
@@ -1,17 +1,23 @@
import { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
import { OverlayScrollbars } from 'overlayscrollbars';
import type { OverlayScrollbars } from 'overlayscrollbars';
import type { PartialOptions, EventListeners } from 'overlayscrollbars';
import type { ComponentPropsWithoutRef, ElementRef, ForwardedRef } from 'react';
import { useOverlayScrollbars } from './useOverlayScrollbars';
export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElements = 'div'> =
ComponentPropsWithoutRef<T> & {
/** Tag of the root element. */
element?: T;
/** OverlayScrollbars options. */
options?: PartialOptions;
/** OverlayScrollbars events. */
events?: EventListeners;
};
export interface OverlayScrollbarsComponentRef<T extends keyof JSX.IntrinsicElements = 'div'> {
/** Returns the OverlayScrollbars instance or null if not initialized. */
instance(): OverlayScrollbars | null;
/** Returns the target element. */
target(): ElementRef<T> | null;
}
@@ -22,50 +28,31 @@ 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 osTargetRef = useRef<ElementRef<T>>(null);
const osChildrenRef = useRef<HTMLDivElement>(null);
const osInstanceRef = useRef<OverlayScrollbars | null>(null);
useEffect(() => {
const { current: targetElm } = osTargetRef;
const { current: childrenElm } = osChildrenRef;
if (targetElm && childrenElm) {
const instance = OverlayScrollbars(
{
target: targetElm as any,
elements: {
viewport: childrenElm,
content: childrenElm,
},
const osInstance = initialize({
target: targetElm as any,
elements: {
viewport: childrenElm,
content: childrenElm,
},
options || {},
events
);
osInstanceRef.current = instance;
});
return () => instance.destroy();
return () => osInstance.destroy();
}
}, []);
useEffect(() => {
const { current: instance } = osInstanceRef;
if (OverlayScrollbars.valid(instance) && options) {
instance.options(options, true);
}
}, [options]);
useEffect(() => {
const { current: instance } = osInstanceRef;
if (OverlayScrollbars.valid(instance) && events) {
return instance.on(events);
}
}, [events]);
useImperativeHandle(
ref,
() => {
return {
instance: () => osInstanceRef.current,
instance,
target: () => osTargetRef.current,
};
},
@@ -1 +1,2 @@
export * from './OverlayScrollbarsComponent';
export * from './useOverlayScrollbars';
@@ -0,0 +1,69 @@
import { useEffect, useCallback, useRef } from 'react';
import { OverlayScrollbars } from 'overlayscrollbars';
import type { PartialOptions, InitializationTarget, EventListeners } from 'overlayscrollbars';
export type UseOverlayScrollbarsInitialization = (
target: InitializationTarget
) => OverlayScrollbars;
export type UseOverlayScrollbarsInstance = () => OverlayScrollbars | null;
/**
* Hook for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough)
* @param options OverlayScrollbars options.
* @param events OverlayScrollbars events.
* @returns A tuple with two values:
* The first value is the initialization function.
* The second value is an function which returns the current OverlayScrollbars instance or null if not initialized.
*/
export const useOverlayScrollbars = (
options?: PartialOptions,
events?: EventListeners
): [UseOverlayScrollbarsInitialization, UseOverlayScrollbarsInstance] => {
const osInstanceRef = useRef<OverlayScrollbars | null>(null);
const optionsRef = useRef<PartialOptions>();
const eventsRef = useRef<EventListeners>();
const offInitialEventsRef = useRef<(() => void) | void>();
useEffect(() => {
const { current: instance } = osInstanceRef;
if (OverlayScrollbars.valid(instance) && options) {
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);
}
}, [events]);
optionsRef.current = options;
eventsRef.current = events;
return [
useCallback((target: InitializationTarget): OverlayScrollbars => {
// if already initialized return the current instance
const presentInstance = osInstanceRef.current;
if (OverlayScrollbars.valid(presentInstance)) {
return presentInstance;
}
const currOptions = optionsRef.current || {};
const currEvents = eventsRef.current || {};
const osInstance = (osInstanceRef.current = OverlayScrollbars(
target,
currOptions,
currEvents
));
offInitialEventsRef.current = osInstance.on(currEvents);
return osInstance;
}, []),
useCallback(() => osInstanceRef.current, []),
];
};
@@ -122,4 +122,50 @@ describe('OverlayScrollbarsComponent', () => {
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);
rerender(
<OverlayScrollbarsComponent events={{ updated: [onUpdated, onUpdatedInitial] }} ref={ref} />
);
ref.current!.instance()!.update(true);
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
expect(onUpdated).toHaveBeenCalledTimes(3);
// unregister works with `[]`, `null` or `undefined`
rerender(<OverlayScrollbarsComponent events={{ updated: null }} ref={ref} />);
ref.current!.instance()!.update(true);
expect(onUpdatedInitial).toHaveBeenCalledTimes(3);
expect(onUpdated).toHaveBeenCalledTimes(3);
});
});
@@ -0,0 +1,43 @@
import { useRef } from 'react';
import { describe, test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useOverlayScrollbars } from '~/overlayscrollbars-react';
import type { OverlayScrollbars } from 'overlayscrollbars';
describe('useOverlayScrollbars', () => {
test('re-initialization', () => {
const Test = () => {
const instanceRef = useRef<OverlayScrollbars | null>(null);
const [initialize, instance] = useOverlayScrollbars();
return (
<>
<button
onClick={(event) => {
const osInstance = initialize(event.target as HTMLElement);
if (instanceRef.current) {
expect(instanceRef.current).toBe(osInstance);
expect(instanceRef.current).toBe(instance());
}
instanceRef.current = osInstance;
expect(instanceRef.current).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);
});
});
@@ -39,7 +39,7 @@ export interface OverlayScrollbarsStatic {
*/
(target: InitializationTarget): OverlayScrollbars | undefined;
/**
* Initialized a new OverlayScrollbars instance to the given target
* Initializes a new OverlayScrollbars instance to the given target
* or returns the current OverlayScrollbars instance if the target already has an instance.
* @param target The target.
* @param options The options. (Can be just an empty object)
@@ -57,7 +57,7 @@ export interface OverlayScrollbarsStatic {
*/
plugin(plugin: Plugin | Plugin[]): void;
/**
* Checkts whether the passed value is a valid overlayscrollbars instance.
* Checks whether the passed value is a valid and not destroyed overlayscrollbars instance.
* @param osInstance The value which shall be checked.
*/
valid(osInstance: any): osInstance is OverlayScrollbars;