diff --git a/packages/overlayscrollbars/src/environment.ts b/packages/overlayscrollbars/src/environment.ts index 33a5267..9173e75 100644 --- a/packages/overlayscrollbars/src/environment.ts +++ b/packages/overlayscrollbars/src/environment.ts @@ -31,9 +31,9 @@ import { Options, defaultOptions } from 'options'; import { PartialOptions } from 'typings'; import { InitializationStrategy } from 'initialization'; -export interface EnvironmentListenersNameArgsMap { - _: undefined; -} +type EnvironmentEventMap = { + _: []; +}; export interface InternalEnvironment { readonly _nativeScrollbarSize: XY; @@ -44,7 +44,7 @@ export interface InternalEnvironment { readonly _cssCustomProperties: boolean; readonly _defaultInitializationStrategy: InitializationStrategy; readonly _defaultDefaultOptions: Options; - _addListener(listener: EventListener): () => void; + _addListener(listener: EventListener): () => void; _getInitializationStrategy(): InitializationStrategy; _setInitializationStrategy(newInitializationStrategy: Partial): void; _getDefaultOptions(): Options; @@ -152,7 +152,7 @@ const createEnvironment = (): InternalEnvironment => { const envDOM = createDOM(`
`); const envElm = envDOM[0] as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement; - const [addEvent, , triggerEvent] = createEventListenerHub(); + const [addEvent, , triggerEvent] = createEventListenerHub(); const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache({ _initialValue: getNativeScrollbarSize(body, envElm, envChildElm), _equal: equalXY, diff --git a/packages/overlayscrollbars/src/overlayscrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars.ts index b0e30f8..8fd8847 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars.ts @@ -99,12 +99,12 @@ export interface OnUpdatedEventListenerArgs { force: boolean; } -export interface EventListenerMap { - initialized: undefined; - initializationWithdrawn: undefined; - updated: OnUpdatedEventListenerArgs; - destroyed: undefined; -} +export type EventListenerMap = { + initialized: []; + initializationWithdrawn: []; + updated: [OnUpdatedEventListenerArgs]; + destroyed: []; +}; export type InitialEventListeners = GeneralInitialEventListeners; @@ -128,8 +128,8 @@ export interface OverlayScrollbars { on(name: Name, listener: EventListener): () => void; on(name: Name, listener: EventListener[]): () => void; - off(name: Name, listener?: EventListener): void; - off(name: Name, listener?: EventListener[]): void; + off(name: Name, listener: EventListener): void; + off(name: Name, listener: EventListener[]): void; } /** @@ -205,19 +205,21 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( _hostMutation, } = updateHints; - triggerEvent('updated', { - updateHints: { - sizeChanged: _sizeChanged, - directionChanged: _directionChanged, - heightIntrinsicChanged: _heightIntrinsicChanged, - overflowAmountChanged: _overflowAmountChanged, - overflowStyleChanged: _overflowStyleChanged, - contentMutation: _contentMutation, - hostMutation: _hostMutation, + triggerEvent('updated', [ + { + updateHints: { + sizeChanged: _sizeChanged, + directionChanged: _directionChanged, + heightIntrinsicChanged: _heightIntrinsicChanged, + overflowAmountChanged: _overflowAmountChanged, + overflowStyleChanged: _overflowStyleChanged, + contentMutation: _contentMutation, + hostMutation: _hostMutation, + }, + changedOptions, + force, }, - changedOptions, - force, - }); + ]); }); const instance: OverlayScrollbars = { diff --git a/packages/overlayscrollbars/src/support/eventListeners.ts b/packages/overlayscrollbars/src/support/eventListeners.ts index f55ffad..42096ee 100644 --- a/packages/overlayscrollbars/src/support/eventListeners.ts +++ b/packages/overlayscrollbars/src/support/eventListeners.ts @@ -1,28 +1,28 @@ import { isArray } from 'support/utils/types'; import { keys } from 'support/utils/object'; -import { each, from } from 'support/utils/array'; +import { each, from, isEmptyArray } from 'support/utils/array'; export type EventListener< - EventMap extends Record, + EventMap extends Record, Name extends keyof EventMap = keyof EventMap -> = (...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]]) => void; +> = (...args: EventMap[Name]) => void; -export type InitialEventListeners> = { +export type InitialEventListeners> = { [K in keyof EventMap]?: EventListener | EventListener[]; }; -const manageListener = >( +const manageListener = >( callback: (listener?: EventListener) => void, listener?: EventListener | EventListener[] ) => { each(isArray(listener) ? listener : [listener], callback); }; -export const createEventListenerHub = >( +export const createEventListenerHub = >( initialEventListeners?: InitialEventListeners ) => { type EventListener = ( - ...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]] + ...args: EventMap[Name] ) => void; const events = new Map>(); @@ -76,15 +76,12 @@ export const createEventListenerHub = >( return removeEvent.bind(0, name as any, listener as any); } - function triggerEvent( - name: Name, - ...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]] - ): void { + function triggerEvent(name: Name, args?: EventMap[Name]): void { const eventSet = events.get(name); each(from(eventSet), (event) => { - if (args) { - (event as (args: EventMap[keyof EventMap]) => void)(args as any); + if (args && !isEmptyArray(args)) { + (event as (...args: EventMap[keyof EventMap]) => void).apply(0, args as any); } else { (event as () => void)(); } diff --git a/packages/overlayscrollbars/tests/jest/support/eventListeners.test.ts b/packages/overlayscrollbars/tests/jest/support/eventListeners.test.ts new file mode 100644 index 0000000..b6ec75e --- /dev/null +++ b/packages/overlayscrollbars/tests/jest/support/eventListeners.test.ts @@ -0,0 +1,166 @@ +import { createEventListenerHub } from 'support/eventListeners'; + +type EventMap = { + onBoolean: [boolean, string]; + onUndefined: []; +}; + +describe('eventListeners', () => { + describe('createEventListenerHub', () => { + test('initialization', () => { + const onBooleanA = jest.fn(); + const onBooleanB = jest.fn(); + const onUndefined = jest.fn(); + const [, , triggerEvent] = createEventListenerHub({ + onBoolean: [onBooleanA, onBooleanB], + onUndefined, + }); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(1); + expect(onBooleanA).toHaveBeenLastCalledWith(true, 'hi'); + expect(onBooleanB).toHaveBeenCalledTimes(1); + expect(onBooleanB).toHaveBeenLastCalledWith(true, 'hi'); + + expect(onUndefined).toHaveBeenCalledTimes(1); + expect(onUndefined).toHaveBeenLastCalledWith(); + }); + + test('addEvent', () => { + const onBooleanA = jest.fn(); + const onBooleanB = jest.fn(); + const onUndefinedA = jest.fn(); + const onUndefinedB = jest.fn(); + const [addEvent, , triggerEvent] = createEventListenerHub(); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + addEvent('onBoolean', onBooleanA); + addEvent('onUndefined', [onUndefinedA, onUndefinedB]); + + triggerEvent('onBoolean', [true, 'hi']); + + expect(onBooleanA).toHaveBeenCalledTimes(1); + expect(onBooleanA).toHaveBeenLastCalledWith(true, 'hi'); + expect(onUndefinedA).not.toHaveBeenCalled(); + expect(onUndefinedB).not.toHaveBeenCalled(); + + triggerEvent('onUndefined'); + + expect(onUndefinedA).toHaveBeenCalledTimes(1); + expect(onUndefinedA).toHaveBeenLastCalledWith(); + expect(onUndefinedB).toHaveBeenCalledTimes(1); + expect(onUndefinedB).toHaveBeenLastCalledWith(); + + addEvent('onBoolean', onBooleanB); + + triggerEvent('onBoolean', [false, 'hi2']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(2); + expect(onBooleanA).toHaveBeenLastCalledWith(false, 'hi2'); + expect(onBooleanB).toHaveBeenCalledTimes(1); + expect(onBooleanB).toHaveBeenLastCalledWith(false, 'hi2'); + + expect(onUndefinedA).toHaveBeenCalledTimes(2); + expect(onUndefinedA).toHaveBeenLastCalledWith(); + expect(onUndefinedB).toHaveBeenCalledTimes(2); + expect(onUndefinedB).toHaveBeenLastCalledWith(); + + const something = jest.fn(); + // @ts-ignore + addEvent('something', something); + // @ts-ignore + triggerEvent('something'); + expect(something).toHaveBeenCalledTimes(1); + }); + + test('removeEvent', () => { + const onBooleanA = jest.fn(); + const onBooleanB = jest.fn(); + const onUndefinedA = jest.fn(); + const onUndefinedB = jest.fn(); + const [addEvent, removeEvent, triggerEvent] = createEventListenerHub({ + onBoolean: [onBooleanA, onBooleanB], + }); + const removeUndefined = addEvent('onUndefined', [onUndefinedA, onUndefinedB]); + + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + expect(onBooleanA).toHaveBeenCalledTimes(1); + expect(onBooleanB).toHaveBeenCalledTimes(1); + expect(onUndefinedA).toHaveBeenCalledTimes(1); + expect(onUndefinedB).toHaveBeenCalledTimes(1); + + removeUndefined(); + + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + expect(onBooleanA).toHaveBeenCalledTimes(2); + expect(onBooleanB).toHaveBeenCalledTimes(2); + expect(onUndefinedA).toHaveBeenCalledTimes(1); + expect(onUndefinedB).toHaveBeenCalledTimes(1); + + removeEvent('onBoolean', onBooleanA); + triggerEvent('onBoolean', [true, 'hi']); + expect(onBooleanA).toHaveBeenCalledTimes(2); + expect(onBooleanB).toHaveBeenCalledTimes(3); + + removeEvent('onBoolean', onBooleanB); + triggerEvent('onBoolean', [true, 'hi']); + expect(onBooleanA).toHaveBeenCalledTimes(2); + expect(onBooleanB).toHaveBeenCalledTimes(3); + + addEvent('onBoolean', [onBooleanA, onBooleanB]); + addEvent('onUndefined', [onUndefinedA, onUndefinedB]); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(3); + expect(onBooleanB).toHaveBeenCalledTimes(4); + expect(onUndefinedA).toHaveBeenCalledTimes(2); + expect(onUndefinedB).toHaveBeenCalledTimes(2); + + removeEvent('onBoolean'); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(3); + expect(onBooleanB).toHaveBeenCalledTimes(4); + expect(onUndefinedA).toHaveBeenCalledTimes(3); + expect(onUndefinedB).toHaveBeenCalledTimes(3); + + addEvent('onBoolean', [onBooleanA, onBooleanB]); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(4); + expect(onBooleanB).toHaveBeenCalledTimes(5); + expect(onUndefinedA).toHaveBeenCalledTimes(4); + expect(onUndefinedB).toHaveBeenCalledTimes(4); + + removeEvent(); + triggerEvent('onBoolean', [true, 'hi']); + triggerEvent('onUndefined'); + + expect(onBooleanA).toHaveBeenCalledTimes(4); + expect(onBooleanB).toHaveBeenCalledTimes(5); + expect(onUndefinedA).toHaveBeenCalledTimes(4); + expect(onUndefinedB).toHaveBeenCalledTimes(4); + + // @ts-ignore + const something = jest.fn(); + // @ts-ignore + addEvent('something', something); + // @ts-ignore + removeEvent('something'); + // @ts-ignore + triggerEvent('something'); + expect(something).not.toHaveBeenCalled(); + + // @ts-ignore + removeEvent('not previously added'); + }); + }); +});