improve plugin system & add and improve various tests

This commit is contained in:
Rene Haas
2022-08-24 16:47:19 +02:00
parent 0d10faccd0
commit b7ebfd3dbd
9 changed files with 727 additions and 140 deletions
+14 -1
View File
@@ -1,4 +1,13 @@
import { assignDeep, each, isObject, keys, isArray, hasOwnProperty, isFunction } from 'support';
import {
assignDeep,
each,
isObject,
keys,
isArray,
hasOwnProperty,
isFunction,
isEmptyObject,
} from 'support';
import { DeepPartial, DeepReadonly } from 'typings';
const opsStringify = (value: any) =>
@@ -80,6 +89,10 @@ export const getOptionsDiff = <T>(currOptions: T, newOptions: DeepPartial<T>): D
if (isObject(currOptionValue) && isObject(newOptionValue)) {
assignDeep((diff[optionKey] = {}), getOptionsDiff(currOptionValue, newOptionValue));
// delete empty nested objects
if (isEmptyObject(diff[optionKey])) {
delete diff[optionKey];
}
} else if (hasOwnProperty(newOptions, optionKey) && newOptionValue !== currOptionValue) {
let isDiff = true;
@@ -13,7 +13,7 @@ import { getEnvironment } from 'environment';
import { cancelInitialization } from 'initialization';
import { addInstance, getInstance, removeInstance } from 'instances';
import { createStructureSetup, createScrollbarsSetup } from 'setups';
import { getPlugins, addPlugin, optionsValidationPluginName } from 'plugins';
import { getPlugins, addPlugin, optionsValidationPluginName, PluginInstance } from 'plugins';
import type { XY, TRBL } from 'support';
import type { Options, ReadonlyOptions } from 'options';
import type { Plugin, OptionsValidationPluginInstance } from 'plugins';
@@ -109,6 +109,16 @@ export interface OverlayScrollbars {
destroy(): void;
}
const invokePluginInstance = (
pluginInstance: PluginInstance,
staticObj?: OverlayScrollbarsStatic | false | null | undefined | 0,
instanceObj?: OverlayScrollbars | false | null | undefined | 0
) => {
if (isFunction(pluginInstance)) {
pluginInstance(staticObj || undefined, instanceObj || undefined);
}
};
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const OverlayScrollbars: OverlayScrollbarsStatic = (
target: InitializationTarget,
@@ -122,13 +132,12 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const potentialInstance = getInstance(instanceTarget);
if (options && !potentialInstance) {
let destroyed = false;
const optionsValidationPlugin = plugins[
optionsValidationPluginName
] as OptionsValidationPluginInstance;
const validateOptions = (newOptions?: DeepPartial<Options>) => {
const opts = newOptions || {};
const validateOptions = (newOptions: DeepPartial<Options>) => {
const optionsValidationPlugin = getPlugins()[
optionsValidationPluginName
] as OptionsValidationPluginInstance;
const validate = optionsValidationPlugin && optionsValidationPlugin._;
return validate ? validate(opts, true) : opts;
return validate ? validate(newOptions, true) : newOptions;
};
const currentOptions: ReadonlyOptions = assignDeep(
{},
@@ -166,7 +175,6 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
options(newOptions?: DeepPartial<Options>) {
if (newOptions) {
const changedOptions = getOptionsDiff(currentOptions, validateOptions(newOptions));
if (!isEmptyObject(changedOptions)) {
assignDeep(currentOptions, changedOptions);
update(changedOptions);
@@ -260,12 +268,11 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
updateScrollbars(changedOptions, force, updateHints);
});
each(keys(plugins), (pluginName) => {
const pluginInstance = plugins[pluginName];
if (isFunction(pluginInstance)) {
pluginInstance(OverlayScrollbars, instance);
}
});
// valid inside plugins
addInstance(instanceTarget, instance);
// init plugins
each(keys(plugins), (pluginName) => invokePluginInstance(plugins[pluginName], 0, instance));
if (
cancelInitialization(
@@ -281,7 +288,6 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
structureState._appendElements();
scrollbarsState._appendElements();
addInstance(instanceTarget, instance);
triggerEvent('initialized', [instance]);
structureState._addOnUpdatedListener((updateHints, changedOptions, force) => {
@@ -322,7 +328,11 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
return potentialInstance!;
};
OverlayScrollbars.plugin = addPlugin;
OverlayScrollbars.plugin = (plugins: Plugin | Plugin[]) => {
each(addPlugin(plugins), (pluginInstance) =>
invokePluginInstance(pluginInstance, OverlayScrollbars)
);
};
OverlayScrollbars.valid = (osInstance: any): osInstance is OverlayScrollbars => {
const hasElmsFn = osInstance && (osInstance as OverlayScrollbars).elements;
const elements = isFunction(hasElmsFn) && hasElmsFn();
@@ -1,9 +1,9 @@
import { each, isArray, keys } from 'support';
import { each, isArray, keys, push } from 'support';
import { OverlayScrollbars, OverlayScrollbarsStatic } from 'overlayscrollbars';
export type PluginInstance =
| Record<string, unknown>
| ((staticObj: OverlayScrollbarsStatic, instanceObj: OverlayScrollbars) => void);
| ((staticObj?: OverlayScrollbarsStatic, instanceObj?: OverlayScrollbars) => void);
export type Plugin<T extends PluginInstance = PluginInstance> = {
[pluginName: string]: T;
};
@@ -12,9 +12,14 @@ const pluginRegistry: Record<string, PluginInstance> = {};
export const getPlugins = () => pluginRegistry;
export const addPlugin = (addedPlugin: Plugin | Plugin[]): void => {
export const addPlugin = (addedPlugin: Plugin | Plugin[]): Plugin[] => {
const result: Plugin[] = [];
each((isArray(addedPlugin) ? addedPlugin : [addedPlugin]) as Plugin[], (plugin) => {
const pluginName = keys(plugin)[0];
pluginRegistry[pluginName] = plugin[pluginName];
// multiple "sub-plugins" per plugin object possible to support "static", "instanceObj" and "staticObj" sub-plugins per plugin
const pluginNameKeys = keys(plugin);
each(pluginNameKeys, (key) => {
push(result, (pluginRegistry[key] = plugin[key]));
});
});
return result;
};
@@ -45,7 +45,7 @@ import type {
} from 'initialization';
export type StructureSetupElements = [
targetObj: StructureSetupElementsObj,
elements: StructureSetupElementsObj,
appendElements: () => void,
destroy: () => void
];
@@ -41,6 +41,7 @@ describe('options', () => {
b: 0,
};
expect(getOptionsDiff(options, options)).toEqual({});
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
@@ -74,6 +75,7 @@ describe('options', () => {
},
};
expect(getOptionsDiff(options, options)).toEqual({});
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
@@ -1,6 +1,7 @@
import { DeepPartial } from 'typings';
import { defaultOptions, Options } from 'options';
import { assignDeep } from 'support';
import { optionsValidationPlugin } from 'plugins';
import { OverlayScrollbars as originalOverlayScrollbars } from '../../src/overlayscrollbars';
const bodyElm = document.body;
@@ -155,26 +156,144 @@ describe('overlayscrollbars', () => {
});
});
test('options', () => {
const customOptions: DeepPartial<Options> = {
paddingAbsolute: !defaultOptions.paddingAbsolute,
overflow: { x: 'hidden' },
};
const osInstance = OverlayScrollbars(div, {});
expect(osInstance.options()).not.toBe(defaultOptions);
expect(osInstance.options()).toEqual(defaultOptions);
OverlayScrollbars(div)!.destroy();
describe('elements', () => {
test('get elements', () => {
const osInstance = OverlayScrollbars(div, {});
const elements = osInstance.elements();
const elementsObj = {
target: div,
host: div,
padding: expect.any(HTMLElement),
viewport: expect.any(HTMLElement),
content: expect.any(HTMLElement),
scrollOffsetElement: expect.any(HTMLElement),
scrollEventElement: expect.any(HTMLElement),
scrollbarHorizontal: {
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
clone: expect.any(Function),
},
scrollbarVertical: {
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
clone: expect.any(Function),
},
};
expect(OverlayScrollbars(div, customOptions).options()).toEqual(
assignDeep({}, defaultOptions, customOptions)
);
OverlayScrollbars(div)!.destroy();
expect(elements).not.toBe(osInstance.elements());
// clone function identity is always different
expect(
assignDeep({}, elements, {
scrollbarHorizontal: {
clone: null,
},
scrollbarVertical: {
clone: null,
},
})
).toEqual(
assignDeep({}, osInstance.elements(), {
scrollbarHorizontal: {
clone: null,
},
scrollbarVertical: {
clone: null,
},
})
);
expect(elements).toEqual(elementsObj);
const osInstance2 = OverlayScrollbars(div, {});
expect(osInstance2.options(customOptions)).toEqual(
assignDeep({}, defaultOptions, customOptions)
);
expect(osInstance2.options()).toEqual(assignDeep({}, defaultOptions, customOptions));
osInstance.destroy();
expect(elements).toEqual(elementsObj);
});
test('clone scrollbars', () => {
const osInstance = OverlayScrollbars(div, {});
const elements = osInstance.elements();
const hClone = elements.scrollbarHorizontal.clone();
const vClone = elements.scrollbarVertical.clone();
expect(hClone).toEqual({
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
});
expect(vClone).toEqual({
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
});
div.append(hClone.scrollbar);
div.append(vClone.scrollbar);
osInstance.destroy();
expect(div.innerHTML).toBe('');
});
});
describe('options', () => {
[false, true].forEach((withValidationPlugin) => {
describe(`${withValidationPlugin ? 'with' : 'without'} optionsValidationPlugin`, () => {
beforeEach(() => {
if (withValidationPlugin) {
OverlayScrollbars.plugin(optionsValidationPlugin);
}
});
test('equality', () => {
const osInstance = OverlayScrollbars(div, {});
expect(osInstance.options()).not.toBe(defaultOptions);
expect(osInstance.options()).toEqual(defaultOptions);
OverlayScrollbars(div)!.destroy();
});
test('initial options', () => {
const customOptions: DeepPartial<Options> = {
paddingAbsolute: !defaultOptions.paddingAbsolute,
overflow: { x: 'hidden' },
};
const osInstance = OverlayScrollbars(div, customOptions);
expect(osInstance.options()).toEqual(assignDeep({}, defaultOptions, customOptions));
});
test('changed options', () => {
const customOptions: DeepPartial<Options> = {
paddingAbsolute: !defaultOptions.paddingAbsolute,
overflow: { x: 'hidden' },
};
const osInstance = OverlayScrollbars(div, {});
expect(osInstance.options(customOptions)).toEqual(
assignDeep({}, defaultOptions, customOptions)
);
expect(osInstance.options()).toEqual(assignDeep({}, defaultOptions, customOptions));
osInstance.destroy();
});
test('unchanged wont trigger update', () => {
const updated = jest.fn();
const initialOpts: DeepPartial<Options> = {
paddingAbsolute: true,
overflow: {
y: 'hidden',
},
};
const osInstance4 = OverlayScrollbars(div, initialOpts, {
updated,
});
expect(updated).toHaveBeenCalledTimes(1);
osInstance4.options(initialOpts);
expect(updated).toHaveBeenCalledTimes(1);
});
});
});
});
test('on', () => {
@@ -296,59 +415,6 @@ describe('overlayscrollbars', () => {
);
});
test('elements', () => {
const osInstance = OverlayScrollbars(div, {});
const elements = osInstance.elements();
const elementsObj = {
target: div,
host: div,
padding: expect.any(HTMLElement),
viewport: expect.any(HTMLElement),
content: expect.any(HTMLElement),
scrollOffsetElement: expect.any(HTMLElement),
scrollEventElement: expect.any(HTMLElement),
scrollbarHorizontal: {
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
clone: expect.any(Function),
},
scrollbarVertical: {
scrollbar: expect.any(HTMLElement),
track: expect.any(HTMLElement),
handle: expect.any(HTMLElement),
clone: expect.any(Function),
},
};
expect(elements).not.toBe(osInstance.elements());
// clone function identity is always different
expect(
assignDeep({}, elements, {
scrollbarHorizontal: {
clone: null,
},
scrollbarVertical: {
clone: null,
},
})
).toEqual(
assignDeep({}, osInstance.elements(), {
scrollbarHorizontal: {
clone: null,
},
scrollbarVertical: {
clone: null,
},
})
);
expect(elements).toEqual(elementsObj);
osInstance.destroy();
expect(elements).toEqual(elementsObj);
});
test('update', () => {
const osInstance = OverlayScrollbars(div, {});
expect(osInstance.update()).toBe(false);
@@ -405,4 +471,64 @@ describe('overlayscrollbars', () => {
expect(OverlayScrollbars.valid(osInstance)).toBe(false);
});
});
test('plugins', () => {
const staticPluginFn = jest.fn();
const staticObjPluginFn = jest.fn();
const instanceObjPluginFn = jest.fn();
const staticPlugin = {
staticPlugin: {
staticFn: staticPluginFn,
},
};
expect(
OverlayScrollbars.plugin([
{
expect: (staticObj, instanceObj) => {
if (instanceObj) {
expect(staticObj).toBe(undefined);
expect(OverlayScrollbars.valid(instanceObj)).toBe(true);
}
if (staticObj) {
expect(staticObj).toBe(OverlayScrollbars);
expect(instanceObj).toBe(undefined);
}
},
},
{
staticPlugin,
staticObjPlugin: (staticObj, instanceObj) => {
if (staticObj) {
expect(instanceObj).toBe(undefined);
// @ts-ignore
staticObj.staticObjPluginFn = staticObjPluginFn;
}
},
instanceObjPlugin: (_, instanceObj) => {
if (instanceObj) {
// @ts-ignore
instanceObj.instanceObjPluginFn = instanceObjPluginFn;
}
},
},
])
).toBe(undefined);
expect(staticPluginFn).not.toHaveBeenCalled();
// @ts-ignore
staticPlugin.staticPlugin.staticFn();
expect(staticPluginFn).toHaveBeenCalledTimes(1);
// staticObj plugin must be available before any initialization
expect(staticObjPluginFn).not.toHaveBeenCalled();
// @ts-ignore
OverlayScrollbars.staticObjPluginFn();
expect(staticObjPluginFn).toHaveBeenCalledTimes(1);
const osInstance = OverlayScrollbars(div, {});
expect(instanceObjPluginFn).not.toHaveBeenCalled();
// @ts-ignore
osInstance.instanceObjPluginFn();
expect(instanceObjPluginFn).toHaveBeenCalledTimes(1);
});
});
@@ -9,19 +9,24 @@ describe('plugins', () => {
test('addPlugin single', () => {
const myPlugin = {};
const myPlugin2 = {};
addPlugin({
const addedPlugins = addPlugin({
myPlugin,
myPlugin2,
});
expect(addedPlugins.length).toBe(2);
expect(addedPlugins[0]).toBe(myPlugin);
expect(addedPlugins[1]).toBe(myPlugin2);
const plugins = getPlugins();
expect(plugins.myPlugin).toBe(myPlugin);
expect(plugins.myPlugin2).toBe(undefined); // one plugin per object
// multiple "sub-plugins" per plugin object possible to support "static", "instanceObj" and "staticObj" sub-plugins per plugin
expect(plugins.myPlugin2).toBe(myPlugin2);
});
test('addPlugin multiple', () => {
const myPlugin = {};
const myPlugin2 = {};
addPlugin([
const addedPlugins = addPlugin([
{
myPlugin,
},
@@ -29,6 +34,11 @@ describe('plugins', () => {
myPlugin2,
},
]);
expect(addedPlugins.length).toBe(2);
expect(addedPlugins[0]).toBe(myPlugin);
expect(addedPlugins[1]).toBe(myPlugin2);
const plugins = getPlugins();
expect(plugins.myPlugin).toBe(myPlugin);
expect(plugins.myPlugin2).toBe(myPlugin2);
@@ -0,0 +1,363 @@
import {
createScrollbarsSetupElements,
ScrollbarsSetupElement,
ScrollbarsSetupElementsObj,
ScrollbarStructure,
} from 'setups/scrollbarsSetup/scrollbarsSetup.elements';
import {
createStructureSetupElements,
StructureSetupElementsObj,
} from 'setups/structureSetup/structureSetup.elements';
import {
classNameScrollbar,
classNameScrollbarHorizontal,
classNameScrollbarVertical,
classNameScrollbarTrack,
classNameScrollbarHandle,
classNamesScrollbarTransitionless,
} from 'classnames';
import type { InitializationTarget } from 'initialization';
jest.useFakeTimers();
jest.mock('support/compatibility/apis', () => {
const originalModule = jest.requireActual('support/compatibility/apis');
return {
...originalModule,
// @ts-ignore
setT: jest.fn().mockImplementation((...args) => setTimeout(...args)),
clearT: jest.fn().mockImplementation((...args) => clearTimeout(...args)),
};
});
const getTarget = () => document.body.firstElementChild as HTMLElement;
const domSnapshot = (element?: HTMLElement) => {
const getResult = () => (element ? element.innerHTML : document.documentElement.outerHTML);
return [getResult(), () => getResult()] as [string, () => string];
};
const createStructureSetupElementsProxy = (target: InitializationTarget) => {
const [structureElements, , destroyStructureElements] = createStructureSetupElements(target);
const [elements, appendElements, destroy] = createScrollbarsSetupElements(
target,
structureElements,
() => () => {}
);
appendElements();
return [
elements,
() => {
destroyStructureElements();
destroy();
},
structureElements,
] as [
elements: ScrollbarsSetupElementsObj,
destroy: () => void,
structureElements: StructureSetupElementsObj
];
};
const assertCorrectDOMStructure = (
elements: ScrollbarsSetupElementsObj,
target: HTMLElement | ((isHorizontal?: boolean) => HTMLElement),
getTargetStructure?: (
structures: ScrollbarStructure[],
isHorizontal?: boolean
) => ScrollbarStructure
) => {
const getStructure = (isHorizontal?: boolean) =>
isHorizontal
? elements._horizontal._scrollbarStructures
: elements._vertical._scrollbarStructures;
const assertScrollbarStructure = (isHorizontal?: boolean) => {
const resolvedTarget = typeof target === 'function' ? target(isHorizontal) : target;
const domScrollbar = resolvedTarget.querySelector(
`.${isHorizontal ? classNameScrollbarHorizontal : classNameScrollbarVertical}`
) as HTMLElement;
const structures = getStructure(isHorizontal);
expect(structures.length).toBeGreaterThanOrEqual(1);
const targetStructure = getTargetStructure?.(structures, isHorizontal) || structures[0];
const isMainStructure = targetStructure === structures[0];
const { _scrollbar, _track, _handle } = targetStructure;
// classnames
expect(domScrollbar).toEqual(expect.any(HTMLElement));
expect(domScrollbar.classList.contains(classNameScrollbar)).toBe(true);
expect(_track.classList.contains(classNameScrollbarTrack)).toBe(true);
expect(_handle.classList.contains(classNameScrollbarHandle)).toBe(true);
expect(_track.classList.length).toBe(1);
expect(_handle.classList.length).toBe(1);
if (isMainStructure) {
expect(domScrollbar.classList.contains(classNamesScrollbarTransitionless)).toBe(true);
}
// structure
expect(_scrollbar).toBe(domScrollbar);
expect(_track.closest(`.${classNameScrollbar}`)).toBe(_scrollbar);
expect(_handle.closest(`.${classNameScrollbarTrack}`)).toBe(_track);
};
assertScrollbarStructure(true);
assertScrollbarStructure();
};
describe('scrollbarsSetup.elements', () => {
beforeEach(() => {
document.body.innerHTML = '<div></div>';
});
[false, true].forEach((targetIsObj) => {
describe(`as target ${targetIsObj ? 'object' : 'element'}`, () => {
test('initialization and destruction', () => {
const target = getTarget();
const [beforeInitSnapshot, beforeInitSnapshotFn] = domSnapshot();
const [elements, destroy] = createStructureSetupElementsProxy(
targetIsObj ? { target } : target
);
assertCorrectDOMStructure(elements, target);
destroy();
expect(beforeInitSnapshot).toBe(beforeInitSnapshotFn());
});
test('cloning', () => {
const target = getTarget();
const [beforeInitSnapshot, beforeInitSnapshotFn] = domSnapshot();
const [elements, destroy] = createStructureSetupElementsProxy(
targetIsObj ? { target } : target
);
const clonedHorizontalSlot = document.createElement('div');
const clonedVerticalSlot = document.createElement('div');
const [beforeCloneHorizontalSnapshot, beforeCloneHorizontalSnapshotFn] =
domSnapshot(clonedHorizontalSlot);
const [beforeCloneVerticalSnapshot, beforeCloneVerticalSnapshotFn] =
domSnapshot(clonedVerticalSlot);
const clonedHorizontal = elements._horizontal._clone();
const clonedVertical = elements._vertical._clone();
clonedHorizontalSlot.append(clonedHorizontal._scrollbar);
clonedVerticalSlot.append(clonedVertical._scrollbar);
target.append(clonedHorizontalSlot);
target.append(clonedVerticalSlot);
assertCorrectDOMStructure(elements, target);
assertCorrectDOMStructure(
elements,
(isHorizontal) => (isHorizontal ? clonedHorizontalSlot : clonedVerticalSlot),
(structures, isHorizontal) => {
const clonedStructure = isHorizontal ? clonedHorizontal : clonedVertical;
return structures.find(
(structure) => structure._scrollbar === clonedStructure._scrollbar
)!;
}
);
destroy();
// destroy cleans up clones as well
expect(beforeCloneHorizontalSnapshot).toBe(beforeCloneHorizontalSnapshotFn());
expect(beforeCloneVerticalSnapshot).toBe(beforeCloneVerticalSnapshotFn());
clonedHorizontalSlot.remove();
clonedVerticalSlot.remove();
expect(beforeInitSnapshot).toBe(beforeInitSnapshotFn());
});
});
});
describe('addRemoveClass', () => {
test('add & remove classes to both axis', () => {
const target = getTarget();
const [elements] = createStructureSetupElementsProxy(target);
const horizontalStructures = elements._horizontal._scrollbarStructures;
const verticalStructures = elements._vertical._scrollbarStructures;
const className = 'aksjhdkasjd';
// clones before have the class
elements._horizontal._clone();
elements._vertical._clone();
elements._scrollbarsAddRemoveClass(className, true);
// clones after do not have the class
elements._horizontal._clone();
elements._vertical._clone();
horizontalStructures.forEach(({ _scrollbar }, i, arr) => {
if (i === arr.length - 1) {
expect(_scrollbar.classList.contains(className)).toBe(false);
} else {
expect(_scrollbar.classList.contains(className)).toBe(true);
}
});
verticalStructures.forEach(({ _scrollbar }, i, arr) => {
if (i === arr.length - 1) {
expect(_scrollbar.classList.contains(className)).toBe(false);
} else {
expect(_scrollbar.classList.contains(className)).toBe(true);
}
});
elements._scrollbarsAddRemoveClass(className);
horizontalStructures.forEach(({ _scrollbar }) => {
expect(_scrollbar.classList.contains(className)).toBe(false);
});
verticalStructures.forEach(({ _scrollbar }) => {
expect(_scrollbar.classList.contains(className)).toBe(false);
});
});
test('add & remove classes to individual axis', () => {
const target = getTarget();
const [elements] = createStructureSetupElementsProxy(target);
const horizontalStructures = elements._horizontal._scrollbarStructures;
const verticalStructures = elements._vertical._scrollbarStructures;
const classNameHorizontal = 'hhhhhhhhh12sdsdf';
const classNameVertical = 'vvvvvvv12sdsdf';
// clones before have the class
elements._horizontal._clone();
elements._vertical._clone();
elements._scrollbarsAddRemoveClass(classNameHorizontal, true, true);
elements._scrollbarsAddRemoveClass(classNameVertical, true, false);
// clones after do not have the class
elements._horizontal._clone();
elements._vertical._clone();
horizontalStructures.forEach(({ _scrollbar }, i, arr) => {
if (i === arr.length - 1) {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(false);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(false);
} else {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(true);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(false);
}
});
verticalStructures.forEach(({ _scrollbar }, i, arr) => {
if (i === arr.length - 1) {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(false);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(false);
} else {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(false);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(true);
}
});
elements._scrollbarsAddRemoveClass(classNameHorizontal, false, true);
elements._scrollbarsAddRemoveClass(classNameVertical, false, false);
horizontalStructures.forEach(({ _scrollbar }) => {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(false);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(false);
});
verticalStructures.forEach(({ _scrollbar }) => {
expect(_scrollbar.classList.contains(classNameHorizontal)).toBe(false);
expect(_scrollbar.classList.contains(classNameVertical)).toBe(false);
});
});
});
test('initialization and destruction in custom slot', () => {
const target = getTarget();
const slotFn = jest.fn(() => document.body);
const [beforeInitSnapshot, beforeInitSnapshotFn] = domSnapshot();
const [elements, destroy, structureElements] = createStructureSetupElementsProxy({
target,
scrollbars: {
slot: slotFn,
},
});
expect(slotFn).toHaveBeenCalledTimes(1);
expect(slotFn).toHaveBeenCalledWith(
structureElements._target,
structureElements._host,
structureElements._viewport
);
assertCorrectDOMStructure(elements, document.body);
destroy();
expect(beforeInitSnapshot).toBe(beforeInitSnapshotFn());
});
test('handleStyle', () => {
const target = getTarget();
const [elements] = createStructureSetupElementsProxy(target);
const testHandleStyle = (scrollbarSetupElement: ScrollbarsSetupElement) => {
// before cloned elements have the style
scrollbarSetupElement._clone();
scrollbarSetupElement._handleStyle((structure) => {
const { _scrollbar } = structure;
return [_scrollbar, { width: '0px' }];
});
scrollbarSetupElement._handleStyle((structure) => {
const { _track } = structure;
return [_track, { width: '1px' }];
});
scrollbarSetupElement._handleStyle((structure) => {
const { _handle } = structure;
return [_handle, { width: '2px' }];
});
// before cloned elements don not have the style
scrollbarSetupElement._clone();
scrollbarSetupElement._scrollbarStructures.forEach(
({ _scrollbar, _track, _handle }, i, arr) => {
if (i === arr.length - 1) {
expect(_scrollbar.style.width).not.toBe('0px');
expect(_track.style.width).not.toBe('1px');
expect(_handle.style.width).not.toBe('2px');
} else {
expect(_scrollbar.style.width).toBe('0px');
expect(_track.style.width).toBe('1px');
expect(_handle.style.width).toBe('2px');
}
}
);
};
testHandleStyle(elements._horizontal);
testHandleStyle(elements._vertical);
});
test('removes transitionless class', () => {
const target = getTarget();
const [elements] = createStructureSetupElementsProxy(target);
const testHasTransitionlessClass = (
setupElement: ScrollbarsSetupElement,
expected: boolean
) => {
const { _scrollbar } = setupElement._scrollbarStructures[0];
expect(_scrollbar.classList.contains(classNamesScrollbarTransitionless)).toBe(expected);
};
testHasTransitionlessClass(elements._horizontal, true);
testHasTransitionlessClass(elements._vertical, true);
jest.runAllTimers();
testHasTransitionlessClass(elements._horizontal, false);
testHasTransitionlessClass(elements._vertical, false);
});
});
@@ -1,6 +1,11 @@
import { hasClass, is, isFunction, isHTMLElement } from 'support';
import { dataAttributeHost } from 'classnames';
import { InternalEnvironment } from 'environment';
import {
dataAttributeHost,
classNamePadding,
classNameViewport,
classNameContent,
} from 'classnames';
import { getEnvironment, InternalEnvironment } from 'environment';
import {
createStructureSetupElements,
StructureSetupElementsObj,
@@ -12,9 +17,8 @@ import type {
InitializationTargetObject,
} from 'initialization';
const mockGetEnvironment = jest.fn();
jest.mock('environment', () => ({
getEnvironment: jest.fn().mockImplementation(() => mockGetEnvironment()),
getEnvironment: jest.fn(),
}));
jest.mock('support/compatibility/apis', () => {
@@ -81,10 +85,10 @@ const clearBody = () => {
const getElements = (targetType: TargetType) => {
const target = getTarget(targetType);
const host = document.querySelector('[data-overlayscrollbars]')!;
const padding = document.querySelector('.os-padding')!;
const viewport = document.querySelector('.os-viewport')!;
const content = document.querySelector('.os-content')!;
const host = document.querySelector(`[${dataAttributeHost}]`)!;
const padding = document.querySelector(`.${classNamePadding}`)!;
const viewport = document.querySelector(`.${classNameViewport}`)!;
const content = document.querySelector(`.${classNameContent}`)!;
return {
target,
@@ -133,7 +137,9 @@ const assertCorrectDOMStructure = (targetType: TargetType, viewportIsTarget: boo
}
};
const createStructureSetupProxy = (target: InitializationTarget): StructureSetupElementsProxy => {
const createStructureSetupElementsProxy = (
target: InitializationTarget
): StructureSetupElementsProxy => {
const [elements, appendElements, destroy] = createStructureSetupElements(target);
appendElements();
return {
@@ -422,9 +428,15 @@ const envInitStrategyViewportIsTarget = {
},
};
describe('structureSetup', () => {
describe('structureSetup.elements', () => {
afterEach(() => clearBody());
beforeEach(() => {
(getEnvironment as jest.Mock).mockImplementation(() =>
jest.requireActual('environment').getEnvironment()
);
});
[
envDefault,
envNativeScrollbarStyling,
@@ -436,8 +448,8 @@ describe('structureSetup', () => {
].forEach((envWithName) => {
const { env: currEnv, name } = envWithName;
describe(`Environment: ${name}`, () => {
beforeAll(() => {
mockGetEnvironment.mockImplementation(() => currEnv);
beforeEach(() => {
(getEnvironment as jest.Mock).mockImplementation(() => currEnv);
});
(['element', 'textarea', 'body'] as TargetType[]).forEach((targetType) => {
@@ -447,7 +459,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy(getTarget(targetType)),
createStructureSetupElementsProxy(getTarget(targetType)),
currEnv
);
assertCorrectDOMStructure(targetType, elements._viewportIsTarget);
@@ -458,7 +470,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({ target: getTarget(targetType) }),
createStructureSetupElementsProxy({ target: getTarget(targetType) }),
currEnv
);
assertCorrectDOMStructure(targetType, elements._viewportIsTarget);
@@ -476,7 +488,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -497,7 +509,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: () => document.querySelector<HTMLElement>('#host'),
@@ -518,7 +530,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -541,7 +553,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -564,7 +576,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: () => document.querySelector<HTMLElement>('#host'),
@@ -586,7 +598,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -608,7 +620,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -628,7 +640,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
padding: false,
@@ -644,7 +656,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
content: () => false,
@@ -662,7 +674,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
padding: () => true,
@@ -678,7 +690,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
content: true,
@@ -696,7 +708,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
padding: false,
@@ -715,7 +727,7 @@ describe('structureSetup', () => {
const snapshot = fillBody(targetType);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
padding: true,
@@ -738,7 +750,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -761,7 +773,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -784,7 +796,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -807,7 +819,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -830,7 +842,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -852,7 +864,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -874,7 +886,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -896,7 +908,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -918,7 +930,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -941,7 +953,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -964,7 +976,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -986,7 +998,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: () => document.querySelector<HTMLElement>('#host'),
@@ -1008,7 +1020,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -1030,7 +1042,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -1052,7 +1064,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: document.querySelector<HTMLElement>('#host'),
@@ -1075,7 +1087,7 @@ describe('structureSetup', () => {
);
const [elements, destroy] = assertCorrectSetupElements(
targetType,
createStructureSetupProxy({
createStructureSetupElementsProxy({
target: getTarget(targetType),
elements: {
host: () => document.querySelector<HTMLElement>('#host'),
@@ -1095,4 +1107,50 @@ describe('structureSetup', () => {
});
});
});
describe('focus', () => {
describe('shift tabindex to viewport', () => {
test('with pointerdown on body', () => {
const { elements } = createStructureSetupElementsProxy(document.body);
expect(elements._viewport.getAttribute('tabindex')).toBe('-1');
expect(document.activeElement).toBe(elements._viewport);
elements._documentElm.dispatchEvent(new Event('pointerdown'));
expect(elements._viewport.getAttribute('tabindex')).toBe(null);
});
test('with keydown on element', () => {
document.body.innerHTML = '<div tabindex="123"></div>';
const target = document.body.firstElementChild as HTMLElement;
target.focus();
const { elements } = createStructureSetupElementsProxy(target);
expect(elements._viewport.getAttribute('tabindex')).toBe('-1');
expect(document.activeElement).toBe(elements._viewport);
elements._documentElm.dispatchEvent(new Event('keydown'));
expect(elements._viewport.getAttribute('tabindex')).toBe(null);
});
});
test('shift tabindex back to original activeElement', () => {
document.body.innerHTML = '<input type="text" value="hi"></input>';
const input = document.querySelector('input') as HTMLInputElement;
const target = document.body;
input.focus();
const preInitFocus = document.activeElement;
const { elements } = createStructureSetupElementsProxy(target);
expect(preInitFocus).toBe(document.activeElement);
elements._documentElm.dispatchEvent(new Event('pointerdown'));
elements._documentElm.dispatchEvent(new Event('keydown'));
expect(preInitFocus).toBe(document.activeElement);
});
});
});