mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-21 10:00:36 +03:00
finish overlayscrollbars-react
This commit is contained in:
@@ -2,6 +2,26 @@
|
|||||||
"name": "overlayscrollbars-react",
|
"name": "overlayscrollbars-react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"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",
|
"main": "./dist/overlayscrollbars-react.umd.js",
|
||||||
"module": "./dist/overlayscrollbars-react.es.js",
|
"module": "./dist/overlayscrollbars-react.es.js",
|
||||||
"types": "./dist/overlayscrollbars-react.d.ts",
|
"types": "./dist/overlayscrollbars-react.d.ts",
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
|
import { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
|
||||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
import type { OverlayScrollbars } from 'overlayscrollbars';
|
||||||
import type { PartialOptions, EventListeners } from 'overlayscrollbars';
|
import type { PartialOptions, EventListeners } from 'overlayscrollbars';
|
||||||
import type { ComponentPropsWithoutRef, ElementRef, ForwardedRef } from 'react';
|
import type { ComponentPropsWithoutRef, ElementRef, ForwardedRef } from 'react';
|
||||||
|
import { useOverlayScrollbars } from './useOverlayScrollbars';
|
||||||
|
|
||||||
export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElements = 'div'> =
|
export type OverlayScrollbarsComponentProps<T extends keyof JSX.IntrinsicElements = 'div'> =
|
||||||
ComponentPropsWithoutRef<T> & {
|
ComponentPropsWithoutRef<T> & {
|
||||||
|
/** Tag of the root element. */
|
||||||
element?: T;
|
element?: T;
|
||||||
|
/** OverlayScrollbars options. */
|
||||||
options?: PartialOptions;
|
options?: PartialOptions;
|
||||||
|
/** OverlayScrollbars events. */
|
||||||
events?: EventListeners;
|
events?: EventListeners;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface OverlayScrollbarsComponentRef<T extends keyof JSX.IntrinsicElements = 'div'> {
|
export interface OverlayScrollbarsComponentRef<T extends keyof JSX.IntrinsicElements = 'div'> {
|
||||||
|
/** Returns the OverlayScrollbars instance or null if not initialized. */
|
||||||
instance(): OverlayScrollbars | null;
|
instance(): OverlayScrollbars | null;
|
||||||
|
/** Returns the target element. */
|
||||||
target(): ElementRef<T> | null;
|
target(): ElementRef<T> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,50 +28,31 @@ const OverlayScrollbarsComponent = <T extends keyof JSX.IntrinsicElements>(
|
|||||||
const { element = 'div', options, events, children, ...other } = props;
|
const { element = 'div', options, events, children, ...other } = props;
|
||||||
const Tag = element;
|
const Tag = element;
|
||||||
|
|
||||||
|
const [initialize, instance] = useOverlayScrollbars(options, events);
|
||||||
const osTargetRef = useRef<ElementRef<T>>(null);
|
const osTargetRef = useRef<ElementRef<T>>(null);
|
||||||
const osChildrenRef = useRef<HTMLDivElement>(null);
|
const osChildrenRef = useRef<HTMLDivElement>(null);
|
||||||
const osInstanceRef = useRef<OverlayScrollbars | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current: targetElm } = osTargetRef;
|
const { current: targetElm } = osTargetRef;
|
||||||
const { current: childrenElm } = osChildrenRef;
|
const { current: childrenElm } = osChildrenRef;
|
||||||
if (targetElm && childrenElm) {
|
if (targetElm && childrenElm) {
|
||||||
const instance = OverlayScrollbars(
|
const osInstance = initialize({
|
||||||
{
|
target: targetElm as any,
|
||||||
target: targetElm as any,
|
elements: {
|
||||||
elements: {
|
viewport: childrenElm,
|
||||||
viewport: childrenElm,
|
content: 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(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => {
|
() => {
|
||||||
return {
|
return {
|
||||||
instance: () => osInstanceRef.current,
|
instance,
|
||||||
target: () => osTargetRef.current,
|
target: () => osTargetRef.current,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './OverlayScrollbarsComponent';
|
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(onUpdatedInitial).toHaveBeenCalledTimes(2);
|
||||||
expect(onUpdated).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;
|
(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.
|
* or returns the current OverlayScrollbars instance if the target already has an instance.
|
||||||
* @param target The target.
|
* @param target The target.
|
||||||
* @param options The options. (Can be just an empty object)
|
* @param options The options. (Can be just an empty object)
|
||||||
@@ -57,7 +57,7 @@ export interface OverlayScrollbarsStatic {
|
|||||||
*/
|
*/
|
||||||
plugin(plugin: Plugin | Plugin[]): void;
|
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.
|
* @param osInstance The value which shall be checked.
|
||||||
*/
|
*/
|
||||||
valid(osInstance: any): osInstance is OverlayScrollbars;
|
valid(osInstance: any): osInstance is OverlayScrollbars;
|
||||||
|
|||||||
Reference in New Issue
Block a user