add environment resize listener for content elements with viewport units

This commit is contained in:
Rene Haas
2022-11-04 00:29:52 +01:00
parent 7d3d060d0c
commit e7cd130fc8
5 changed files with 107 additions and 18 deletions
+15 -5
View File
@@ -17,6 +17,7 @@ import {
createCache, createCache,
equalXY, equalXY,
createEventListenerHub, createEventListenerHub,
selfClearTimeout,
} from '~/support'; } from '~/support';
import { import {
classNameEnvironment, classNameEnvironment,
@@ -32,7 +33,8 @@ import type { ScrollbarsHidingPluginInstance } from '~/plugins';
import type { Initialization, PartialInitialization } from '~/initialization'; import type { Initialization, PartialInitialization } from '~/initialization';
type EnvironmentEventMap = { type EnvironmentEventMap = {
_: []; z: [];
r: [];
}; };
/** /**
@@ -86,7 +88,8 @@ export interface InternalEnvironment {
readonly _cssCustomProperties: boolean; readonly _cssCustomProperties: boolean;
readonly _staticDefaultInitialization: Initialization; readonly _staticDefaultInitialization: Initialization;
readonly _staticDefaultOptions: Options; readonly _staticDefaultOptions: Options;
_addListener(listener: EventListener<EnvironmentEventMap, '_'>): () => void; _addZoomListener(listener: EventListener<EnvironmentEventMap, 'z'>): () => void;
_addResizeListener(listener: EventListener<EnvironmentEventMap, 'r'>): () => void;
_getDefaultInitialization(): Initialization; _getDefaultInitialization(): Initialization;
_setDefaultInitialization(newInitialization: PartialInitialization): Initialization; _setDefaultInitialization(newInitialization: PartialInitialization): Initialization;
_getDefaultOptions(): Options; _getDefaultOptions(): Options;
@@ -180,6 +183,7 @@ const createEnvironment = (): InternalEnvironment => {
const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`); const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`);
const envElm = envDOM[0] as HTMLElement; const envElm = envDOM[0] as HTMLElement;
const envChildElm = envElm.firstChild as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement;
const [requestResizeAnimationFrame] = selfClearTimeout();
const [addEvent, , triggerEvent] = createEventListenerHub<EnvironmentEventMap>(); const [addEvent, , triggerEvent] = createEventListenerHub<EnvironmentEventMap>();
const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache( const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache(
{ {
@@ -227,7 +231,8 @@ const createEnvironment = (): InternalEnvironment => {
_cssCustomProperties: style(envElm, 'zIndex') === '-1', _cssCustomProperties: style(envElm, 'zIndex') === '-1',
_rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm), _rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm),
_flexboxGlue: getFlexboxGlue(envElm, envChildElm), _flexboxGlue: getFlexboxGlue(envElm, envChildElm),
_addListener: (listener) => addEvent('_', listener), _addZoomListener: addEvent.bind(0, 'z'),
_addResizeListener: addEvent.bind(0, 'r'),
_getDefaultInitialization: getDefaultInitialization, _getDefaultInitialization: getDefaultInitialization,
_setDefaultInitialization: (newInitializationStrategy) => _setDefaultInitialization: (newInitializationStrategy) =>
assignDeep(staticDefaultInitialization, newInitializationStrategy) && assignDeep(staticDefaultInitialization, newInitializationStrategy) &&
@@ -238,21 +243,26 @@ const createEnvironment = (): InternalEnvironment => {
_staticDefaultInitialization: assignDeep({}, staticDefaultInitialization), _staticDefaultInitialization: assignDeep({}, staticDefaultInitialization),
_staticDefaultOptions: assignDeep({}, staticDefaultOptions), _staticDefaultOptions: assignDeep({}, staticDefaultOptions),
}; };
const windowAddEventListener = window.addEventListener;
removeAttr(envElm, 'style'); removeAttr(envElm, 'style');
removeElements(envElm); removeElements(envElm);
if (!nativeScrollbarsHiding && (!nativeScrollbarsOverlaid.x || !nativeScrollbarsOverlaid.y)) { if (!nativeScrollbarsHiding && (!nativeScrollbarsOverlaid.x || !nativeScrollbarsOverlaid.y)) {
let resizeFn: undefined | ReturnType<ScrollbarsHidingPluginInstance['_envWindowZoom']>; let resizeFn: undefined | ReturnType<ScrollbarsHidingPluginInstance['_envWindowZoom']>;
window.addEventListener('resize', () => { windowAddEventListener('resize', () => {
const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as
| ScrollbarsHidingPluginInstance | ScrollbarsHidingPluginInstance
| undefined; | undefined;
resizeFn = resizeFn || (scrollbarsHidingPlugin && scrollbarsHidingPlugin._envWindowZoom()); 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; return env;
}; };
@@ -237,7 +237,7 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
options?: PartialOptions, options?: PartialOptions,
eventListeners?: EventListeners eventListeners?: EventListeners
) => { ) => {
const { _getDefaultOptions, _getDefaultInitialization, _addListener } = getEnvironment(); const { _getDefaultOptions, _getDefaultInitialization, _addZoomListener } = getEnvironment();
const plugins = getPlugins(); const plugins = getPlugins();
const targetIsElement = isHTMLElement(target); const targetIsElement = isHTMLElement(target);
const instanceTarget = targetIsElement ? target : target.target; const instanceTarget = targetIsElement ? target : target.target;
@@ -270,10 +270,10 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
); );
const update = (changedOptions: PartialOptions, force?: boolean): boolean => const update = (changedOptions: PartialOptions, force?: boolean): boolean =>
updateStructure(changedOptions, !!force); updateStructure(changedOptions, !!force);
const removeEnvListener = _addListener(update.bind(0, {}, true)); const removeZoomListener = _addZoomListener(update.bind(0, {}, true));
const destroy = (canceled?: boolean) => { const destroy = (canceled?: boolean) => {
removeInstance(instanceTarget); removeInstance(instanceTarget);
removeEnvListener(); removeZoomListener();
destroyScrollbars(); destroyScrollbars();
destroyStructure(); destroyStructure();
@@ -1,3 +1,4 @@
import { getEnvironment } from '~/environment';
import { createEventListenerHub, isEmptyObject, keys, scrollLeft, scrollTop } from '~/support'; import { createEventListenerHub, isEmptyObject, keys, scrollLeft, scrollTop } from '~/support';
import { createState, createOptionCheck } from '~/setups/setups'; import { createState, createOptionCheck } from '~/setups/setups';
import { createStructureSetupElements } from '~/setups/structureSetup/structureSetup.elements'; import { createStructureSetupElements } from '~/setups/structureSetup/structureSetup.elements';
@@ -69,6 +70,7 @@ export const createStructureSetup = (
target: InitializationTarget, target: InitializationTarget,
options: ReadonlyOptions options: ReadonlyOptions
): Setup<StructureSetupState, StructureSetupStaticState, [], boolean> => { ): Setup<StructureSetupState, StructureSetupStaticState, [], boolean> => {
const { _addResizeListener } = getEnvironment();
const checkOptionsFallback = createOptionCheck(options, {}); const checkOptionsFallback = createOptionCheck(options, {});
const state = createState(initialStructureSetupUpdateState); const state = createState(initialStructureSetupUpdateState);
const [addEvent, removeEvent, triggerEvent] = createEventListenerHub<StructureSetupEventMap>(); const [addEvent, removeEvent, triggerEvent] = createEventListenerHub<StructureSetupEventMap>();
@@ -87,10 +89,15 @@ export const createStructureSetup = (
} }
return changed; return changed;
}; };
const updateWithHints = (updateHints: Partial<StructureSetupUpdateHints>) =>
triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false);
const [destroyObservers, appendObserverElements, updateObservers, updateObserversOptions] = const [destroyObservers, appendObserverElements, updateObservers, updateObserversOptions] =
createStructureSetupObservers(elements, setState, (updateHints) => createStructureSetupObservers(elements, setState, updateWithHints);
triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false) const removeResizeListener = _addResizeListener(
); updateWithHints.bind(0, {
_contentMutation: true,
})
);
const structureSetupState = getState.bind(0) as (() => StructureSetupState) & const structureSetupState = getState.bind(0) as (() => StructureSetupState) &
StructureSetupStaticState; StructureSetupStaticState;
@@ -123,6 +130,7 @@ export const createStructureSetup = (
removeEvent(); removeEvent();
destroyObservers(); destroyObservers();
destroyElements(); destroyElements();
removeResizeListener();
}, },
]; ];
}; };
@@ -5,6 +5,24 @@ import type { DeepPartial } from '~/typings';
import type { Options } from '~/options'; import type { Options } from '~/options';
import type { Initialization } from '~/initialization'; 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 = { const defaultInitialization = {
elements: { elements: {
host: null, host: null,
@@ -144,27 +162,27 @@ describe('environment', () => {
}); });
}); });
describe('addListener', () => { describe('addZoomListener', () => {
test('with scrollbarsHidingPlugin registered before environment was created', async () => { test('with scrollbarsHidingPlugin registered before environment was created', async () => {
const { getPlugins } = await import('~/plugins'); const { getPlugins } = await import('~/plugins');
(getPlugins as jest.Mock).mockImplementation(() => ({ (getPlugins as jest.Mock).mockImplementation(() => ({
[scrollbarsHidingPluginName]: ScrollbarsHidingPlugin[scrollbarsHidingPluginName], [scrollbarsHidingPluginName]: ScrollbarsHidingPlugin[scrollbarsHidingPluginName],
})); }));
const { _addListener } = getEnv(); const { _addZoomListener } = getEnv();
const listener = jest.fn(); const listener = jest.fn();
_addListener(listener); _addZoomListener(listener);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledTimes(1);
}); });
test('with scrollbarsHidingPlugin registered after environment was created', async () => { test('with scrollbarsHidingPlugin registered after environment was created', async () => {
const { _addListener } = getEnv(); const { _addZoomListener } = getEnv();
const listener = jest.fn(); const listener = jest.fn();
_addListener(listener); _addZoomListener(listener);
const { getPlugins } = await import('~/plugins'); const { getPlugins } = await import('~/plugins');
(getPlugins as jest.Mock).mockImplementation(() => ({ (getPlugins as jest.Mock).mockImplementation(() => ({
@@ -177,13 +195,27 @@ describe('environment', () => {
}); });
test('without scrollbarsHidingPlugin', () => { test('without scrollbarsHidingPlugin', () => {
const { _addListener } = getEnv(); const { _addZoomListener } = getEnv();
const listener = jest.fn(); const listener = jest.fn();
_addListener(listener); _addZoomListener(listener);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(listener).toHaveBeenCalledTimes(0); 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);
});
}); });
@@ -4,6 +4,24 @@ import { OptionsValidationPlugin } from '~/plugins';
import { OverlayScrollbars as originalOverlayScrollbars } from '~/overlayscrollbars'; import { OverlayScrollbars as originalOverlayScrollbars } from '~/overlayscrollbars';
import type { PartialOptions } from '~/options'; 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 bodyElm = document.body;
const div = document.createElement('div'); const div = document.createElement('div');
@@ -502,6 +520,27 @@ describe('overlayscrollbars', () => {
div.style.cursor = 'pointer'; div.style.cursor = 'pointer';
expect(osInstance.update()).toBe(true); 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', () => { describe('static', () => {