mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-20 21:10:37 +03:00
add environment resize listener for content elements with viewport units
This commit is contained in:
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user