From e7cd130fc897608132b15b2b62ae02ea0740aa17 Mon Sep 17 00:00:00 2001 From: Rene Haas Date: Fri, 4 Nov 2022 00:29:52 +0100 Subject: [PATCH] add environment resize listener for content elements with viewport units --- packages/overlayscrollbars/src/environment.ts | 20 ++++++-- .../src/overlayscrollbars.ts | 6 +-- .../setups/structureSetup/structureSetup.ts | 14 ++++-- .../test/jest-jsdom/environment.test.ts | 46 ++++++++++++++++--- .../test/jest-jsdom/overlayscrollbars.test.ts | 39 ++++++++++++++++ 5 files changed, 107 insertions(+), 18 deletions(-) diff --git a/packages/overlayscrollbars/src/environment.ts b/packages/overlayscrollbars/src/environment.ts index 540a42a..9602c03 100644 --- a/packages/overlayscrollbars/src/environment.ts +++ b/packages/overlayscrollbars/src/environment.ts @@ -17,6 +17,7 @@ import { createCache, equalXY, createEventListenerHub, + selfClearTimeout, } from '~/support'; import { classNameEnvironment, @@ -32,7 +33,8 @@ import type { ScrollbarsHidingPluginInstance } from '~/plugins'; import type { Initialization, PartialInitialization } from '~/initialization'; type EnvironmentEventMap = { - _: []; + z: []; + r: []; }; /** @@ -86,7 +88,8 @@ export interface InternalEnvironment { readonly _cssCustomProperties: boolean; readonly _staticDefaultInitialization: Initialization; readonly _staticDefaultOptions: Options; - _addListener(listener: EventListener): () => void; + _addZoomListener(listener: EventListener): () => void; + _addResizeListener(listener: EventListener): () => void; _getDefaultInitialization(): Initialization; _setDefaultInitialization(newInitialization: PartialInitialization): Initialization; _getDefaultOptions(): Options; @@ -180,6 +183,7 @@ const createEnvironment = (): InternalEnvironment => { const envDOM = createDOM(`
`); const envElm = envDOM[0] as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement; + const [requestResizeAnimationFrame] = selfClearTimeout(); const [addEvent, , triggerEvent] = createEventListenerHub(); const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache( { @@ -227,7 +231,8 @@ const createEnvironment = (): InternalEnvironment => { _cssCustomProperties: style(envElm, 'zIndex') === '-1', _rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm), _flexboxGlue: getFlexboxGlue(envElm, envChildElm), - _addListener: (listener) => addEvent('_', listener), + _addZoomListener: addEvent.bind(0, 'z'), + _addResizeListener: addEvent.bind(0, 'r'), _getDefaultInitialization: getDefaultInitialization, _setDefaultInitialization: (newInitializationStrategy) => assignDeep(staticDefaultInitialization, newInitializationStrategy) && @@ -238,21 +243,26 @@ const createEnvironment = (): InternalEnvironment => { _staticDefaultInitialization: assignDeep({}, staticDefaultInitialization), _staticDefaultOptions: assignDeep({}, staticDefaultOptions), }; + const windowAddEventListener = window.addEventListener; removeAttr(envElm, 'style'); removeElements(envElm); if (!nativeScrollbarsHiding && (!nativeScrollbarsOverlaid.x || !nativeScrollbarsOverlaid.y)) { let resizeFn: undefined | ReturnType; - window.addEventListener('resize', () => { + windowAddEventListener('resize', () => { const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as | ScrollbarsHidingPluginInstance | undefined; resizeFn = resizeFn || (scrollbarsHidingPlugin && scrollbarsHidingPlugin._envWindowZoom()); - resizeFn && resizeFn(env, updateNativeScrollbarSizeCache, triggerEvent.bind(0, '_')); + resizeFn && resizeFn(env, updateNativeScrollbarSizeCache, triggerEvent.bind(0, 'z', [])); }); } + // needed in case content has css viewport units + windowAddEventListener('resize', () => { + requestResizeAnimationFrame(triggerEvent.bind(0, 'r', [])); + }); return env; }; diff --git a/packages/overlayscrollbars/src/overlayscrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars.ts index bd1f149..89029be 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars.ts @@ -237,7 +237,7 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( options?: PartialOptions, eventListeners?: EventListeners ) => { - const { _getDefaultOptions, _getDefaultInitialization, _addListener } = getEnvironment(); + const { _getDefaultOptions, _getDefaultInitialization, _addZoomListener } = getEnvironment(); const plugins = getPlugins(); const targetIsElement = isHTMLElement(target); const instanceTarget = targetIsElement ? target : target.target; @@ -270,10 +270,10 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( ); const update = (changedOptions: PartialOptions, force?: boolean): boolean => updateStructure(changedOptions, !!force); - const removeEnvListener = _addListener(update.bind(0, {}, true)); + const removeZoomListener = _addZoomListener(update.bind(0, {}, true)); const destroy = (canceled?: boolean) => { removeInstance(instanceTarget); - removeEnvListener(); + removeZoomListener(); destroyScrollbars(); destroyStructure(); diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts index fb56640..be0fe12 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts @@ -1,3 +1,4 @@ +import { getEnvironment } from '~/environment'; import { createEventListenerHub, isEmptyObject, keys, scrollLeft, scrollTop } from '~/support'; import { createState, createOptionCheck } from '~/setups/setups'; import { createStructureSetupElements } from '~/setups/structureSetup/structureSetup.elements'; @@ -69,6 +70,7 @@ export const createStructureSetup = ( target: InitializationTarget, options: ReadonlyOptions ): Setup => { + const { _addResizeListener } = getEnvironment(); const checkOptionsFallback = createOptionCheck(options, {}); const state = createState(initialStructureSetupUpdateState); const [addEvent, removeEvent, triggerEvent] = createEventListenerHub(); @@ -87,10 +89,15 @@ export const createStructureSetup = ( } return changed; }; + const updateWithHints = (updateHints: Partial) => + triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false); const [destroyObservers, appendObserverElements, updateObservers, updateObserversOptions] = - createStructureSetupObservers(elements, setState, (updateHints) => - triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false) - ); + createStructureSetupObservers(elements, setState, updateWithHints); + const removeResizeListener = _addResizeListener( + updateWithHints.bind(0, { + _contentMutation: true, + }) + ); const structureSetupState = getState.bind(0) as (() => StructureSetupState) & StructureSetupStaticState; @@ -123,6 +130,7 @@ export const createStructureSetup = ( removeEvent(); destroyObservers(); destroyElements(); + removeResizeListener(); }, ]; }; diff --git a/packages/overlayscrollbars/test/jest-jsdom/environment.test.ts b/packages/overlayscrollbars/test/jest-jsdom/environment.test.ts index 7dd3de9..c6f185d 100644 --- a/packages/overlayscrollbars/test/jest-jsdom/environment.test.ts +++ b/packages/overlayscrollbars/test/jest-jsdom/environment.test.ts @@ -5,6 +5,24 @@ import type { DeepPartial } from '~/typings'; import type { Options } from '~/options'; import type { Initialization } from '~/initialization'; +jest.useFakeTimers(); + +jest.mock('~/support/compatibility/apis', () => { + const originalModule = jest.requireActual('~/support/compatibility/apis'); + const mockRAF = (arg: any) => setTimeout(arg, 0); + return { + ...originalModule, + // @ts-ignore + rAF: jest.fn().mockImplementation((...args) => mockRAF(...args)), + // @ts-ignore + cAF: jest.fn().mockImplementation((...args) => clearTimeout(...args)), + // @ts-ignore + setT: jest.fn().mockImplementation((...args) => setTimeout(...args)), + // @ts-ignore + clearT: jest.fn().mockImplementation((...args) => clearTimeout(...args)), + }; +}); + const defaultInitialization = { elements: { host: null, @@ -144,27 +162,27 @@ describe('environment', () => { }); }); - describe('addListener', () => { + describe('addZoomListener', () => { test('with scrollbarsHidingPlugin registered before environment was created', async () => { const { getPlugins } = await import('~/plugins'); (getPlugins as jest.Mock).mockImplementation(() => ({ [scrollbarsHidingPluginName]: ScrollbarsHidingPlugin[scrollbarsHidingPluginName], })); - const { _addListener } = getEnv(); + const { _addZoomListener } = getEnv(); const listener = jest.fn(); - _addListener(listener); + _addZoomListener(listener); window.dispatchEvent(new Event('resize')); expect(listener).toHaveBeenCalledTimes(1); }); test('with scrollbarsHidingPlugin registered after environment was created', async () => { - const { _addListener } = getEnv(); + const { _addZoomListener } = getEnv(); const listener = jest.fn(); - _addListener(listener); + _addZoomListener(listener); const { getPlugins } = await import('~/plugins'); (getPlugins as jest.Mock).mockImplementation(() => ({ @@ -177,13 +195,27 @@ describe('environment', () => { }); test('without scrollbarsHidingPlugin', () => { - const { _addListener } = getEnv(); + const { _addZoomListener } = getEnv(); const listener = jest.fn(); - _addListener(listener); + _addZoomListener(listener); window.dispatchEvent(new Event('resize')); expect(listener).toHaveBeenCalledTimes(0); }); }); + + test('addResizeListener', () => { + const { _addResizeListener } = getEnv(); + const listener = jest.fn(); + + _addResizeListener(listener); + window.dispatchEvent(new Event('resize')); + + expect(listener).not.toHaveBeenCalled(); + + jest.runAllTimers(); + + expect(listener).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/overlayscrollbars/test/jest-jsdom/overlayscrollbars.test.ts b/packages/overlayscrollbars/test/jest-jsdom/overlayscrollbars.test.ts index 2cdee74..d667129 100644 --- a/packages/overlayscrollbars/test/jest-jsdom/overlayscrollbars.test.ts +++ b/packages/overlayscrollbars/test/jest-jsdom/overlayscrollbars.test.ts @@ -4,6 +4,24 @@ import { OptionsValidationPlugin } from '~/plugins'; import { OverlayScrollbars as originalOverlayScrollbars } from '~/overlayscrollbars'; import type { PartialOptions } from '~/options'; +jest.useFakeTimers(); + +jest.mock('~/support/compatibility/apis', () => { + const originalModule = jest.requireActual('~/support/compatibility/apis'); + const mockRAF = (arg: any) => setTimeout(arg, 0); + return { + ...originalModule, + // @ts-ignore + rAF: jest.fn().mockImplementation((...args) => mockRAF(...args)), + // @ts-ignore + cAF: jest.fn().mockImplementation((...args) => clearTimeout(...args)), + // @ts-ignore + setT: jest.fn().mockImplementation((...args) => setTimeout(...args)), + // @ts-ignore + clearT: jest.fn().mockImplementation((...args) => clearTimeout(...args)), + }; +}); + const bodyElm = document.body; const div = document.createElement('div'); @@ -502,6 +520,27 @@ describe('overlayscrollbars', () => { div.style.cursor = 'pointer'; expect(osInstance.update()).toBe(true); }); + + test('environment resize listener', () => { + const updated = jest.fn(); + OverlayScrollbars( + div, + {}, + { + updated, + } + ); + + expect(updated).toHaveBeenCalledTimes(1); + + window.dispatchEvent(new Event('resize')); + + expect(updated).toHaveBeenCalledTimes(1); + + jest.runAllTimers(); + + expect(updated).toHaveBeenCalledTimes(2); + }); }); describe('static', () => {