diff --git a/packages/overlayscrollbars/src/environment/environment.ts b/packages/overlayscrollbars/src/environment/environment.ts index 88b1a53..39cdd2f 100644 --- a/packages/overlayscrollbars/src/environment/environment.ts +++ b/packages/overlayscrollbars/src/environment/environment.ts @@ -12,6 +12,7 @@ import { removeElements, windowSize, runEach, + equalWH, } from 'support'; export type OnEnvironmentChanged = (env: Environment) => void; @@ -21,6 +22,7 @@ export interface Environment { _nativeScrollbarIsOverlaid: XY; _nativeScrollbarStyling: boolean; _rtlScrollBehavior: { n: boolean; i: boolean }; + _flexboxGlue: boolean; _addListener(listener: OnEnvironmentChanged): void; _removeListener(listener: OnEnvironmentChanged): void; } @@ -28,6 +30,8 @@ export interface Environment { let environmentInstance: Environment; const { abs, round } = Math; const environmentElmId = 'os-environment'; +const classNameFlexboxGlue = 'flexbox-glue'; +const classNameFlexboxGlueMax = `${classNameFlexboxGlue}-max`; const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => { appendChildren(body, measureElm); @@ -42,7 +46,7 @@ const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY const getNativeScrollbarStyling = (testElm: HTMLElement): boolean => { let result = false; - addClass(testElm, 'os-viewport-native-scrollbars-invisible'); + addClass(testElm, 'os-viewport-scrollbar-styled'); try { result = style(testElm, 'scrollbar-width') === 'none' || window.getComputedStyle(testElm, '::-webkit-scrollbar').getPropertyValue('display') === 'none'; @@ -78,6 +82,20 @@ const getRtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): { }; }; +const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean => { + addClass(parentElm, classNameFlexboxGlue); + const minOffsetsizeParent = offsetSize(parentElm); + const minOffsetsize = offsetSize(childElm); + const supportsMin = equalWH(minOffsetsize, minOffsetsizeParent); + + addClass(parentElm, classNameFlexboxGlueMax); + const maxOffsetsizeParent = offsetSize(parentElm); + const maxOffsetsize = offsetSize(childElm); + const supportsMax = equalWH(maxOffsetsize, maxOffsetsizeParent); + + return supportsMin && supportsMax; +}; + const getWindowDPR = (): number => { // eslint-disable-next-line // @ts-ignore @@ -113,6 +131,7 @@ const createEnvironment = (): Environment => { _nativeScrollbarIsOverlaid: nativeScrollbarIsOverlaid, _nativeScrollbarStyling: getNativeScrollbarStyling(envElm), _rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm), + _flexboxGlue: getFlexboxGlue(envElm, envChildElm), _addListener(listener: OnEnvironmentChanged): void { onChangedListener.add(listener); }, @@ -122,6 +141,7 @@ const createEnvironment = (): Environment => { }; removeAttr(envElm, 'style'); + removeAttr(envElm, 'class'); removeElements(envElm); if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) { diff --git a/packages/overlayscrollbars/src/overlayscrollbars.scss b/packages/overlayscrollbars/src/overlayscrollbars.scss index e31bb56..e871b93 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.scss +++ b/packages/overlayscrollbars/src/overlayscrollbars.scss @@ -1,19 +1,57 @@ @import './sizeobserver.scss'; @import './trinsicobserver.scss'; +@import './structurelifecycle.scss'; #os-environment { position: fixed; opacity: 0; visibility: hidden; overflow: scroll; - height: 500px; - width: 500px; -} -#os-environment > div { - width: 200%; - height: 200%; - margin: 10px 0; + height: 200px; + width: 200px; + + div { + width: 200%; + height: 200%; + margin: 10px 0; + } + + &.flexbox-glue { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + height: auto; + width: auto; + min-height: 200px; + min-width: 200px; + + div { + flex: auto; + width: auto; + height: auto; + max-height: 100%; + max-width: 100%; + margin: 0; + } + } + + &.flexbox-glue-max { + max-height: 200px; + + div { + overflow: visible; + + &::before { + content: ''; + display: block; + height: 999px; + width: 999px; + } + } + } } + +#os-environment /* fix restricted measuring */ #os-environment:before, #os-environment:after, @@ -33,14 +71,14 @@ .os-viewport { -ms-overflow-style: scrollbar !important; } -.os-viewport-native-scrollbars-invisible#os-environment, -.os-viewport-native-scrollbars-invisible.os-viewport { +.os-viewport-scrollbar-styled#os-environment, +.os-viewport-scrollbar-styled.os-viewport { scrollbar-width: none !important; } -.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar, -.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar, -.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar-corner, -.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar-corner { +.os-viewport-scrollbar-styled#os-environment::-webkit-scrollbar, +.os-viewport-scrollbar-styled.os-viewport::-webkit-scrollbar, +.os-viewport-scrollbar-styled#os-environment::-webkit-scrollbar-corner, +.os-viewport-scrollbar-styled.os-viewport::-webkit-scrollbar-corner { display: none !important; width: 0px !important; height: 0px !important; diff --git a/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/StructureLifecycle.ts b/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/StructureLifecycle.ts index a1aaee1..f8920ef 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/StructureLifecycle.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/StructureLifecycle.ts @@ -1,18 +1,81 @@ -import { OverlayScrollbarsLifecycle } from 'overlayscrollbars/lifecycles'; +import { + cssProperty, + createDOM, + runEach, + contents, + appendChildren, + removeElements, + addClass, + topRightBottomLeft, + TRBL, + equalTRBL, + createCache, +} from 'support'; +import { Lifecycle } from 'overlayscrollbars/lifecycles'; +import { getEnvironment, Environment } from 'environment'; +import { createSizeObserver } from 'overlayscrollbars/observers/SizeObserver'; +import { createTrinsicObserver } from 'overlayscrollbars/observers/TrinsicObserver'; +export type OverflowBehavior = 'hidden' | 'scroll' | 'visible-hidden' | 'visible-scroll'; export interface StructureLifecycleOptions { - _paddingAbsolute: boolean; - _autoSizeCapable: boolean; - _heightAuto: boolean; - _widthAuto: boolean; - _border: [number, number, number, number]; - _padding: [number, number, number, number]; - _margin: [number, number, number, number]; + _paddingAbsolute?: boolean; + _overflowBehavior?: { + x: OverflowBehavior; + y: OverflowBehavior; + }; } -export class StructureLifecycle extends OverlayScrollbarsLifecycle { - // eslint-disable-next-line - _update(options?: StructureLifecycleOptions): void {} - // eslint-disable-next-line - _destruct(): void {} +interface StructureLifecycleCache { + padding: TRBL; } + +const classNameHost = 'os-host'; +const classNameViewport = 'os-viewport'; +const classNameContent = 'os-content'; +const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`; + +const cssMarginEnd = cssProperty('margin-inline-end'); +const cssBorderEnd = cssProperty('border-inline-end'); + +export const createStructureLifecycle = (target: HTMLElement, options?: StructureLifecycleOptions): Lifecycle => { + const destructFns: (() => any)[] = []; + const env: Environment = getEnvironment(); + const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid; + const supportsScrollbarStyling = env._nativeScrollbarStyling; + const supportFlexboxGlue = env._flexboxGlue; + // direction change is only needed to update scrollbar hiding, therefore its not needed if css can do it, scrollbars are invisible or overlaid on y axis + const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y; + + const viewportElm = createDOM(`
`)[0]; + const contentElm = createDOM(`
`)[0]; + + const updateCache = createCache({ + padding: [() => topRightBottomLeft(target, 'padding'), equalTRBL], + }); + + const onSizeChanged = (direction?: 'ltr' | 'rtl') => { + updateCache('padding'); + }; + const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => { + console.log('heightAuot', heightIntrinsic); + }; + + appendChildren(viewportElm, contentElm); + appendChildren(contentElm, contents(target)); + appendChildren(target, viewportElm); + addClass(target, classNameHost); + + destructFns.push(createSizeObserver(target, onSizeChanged, { _appear: true, _direction: !directionObserverObsolete })); + destructFns.push(createTrinsicObserver(target, onTrinsicChanged)); + + return { + _options() { + // eslint-disable-next-line + console.log('_options'); + }, + _destruct() { + runEach(destructFns); + removeElements(viewportElm); + }, + }; +}; diff --git a/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/index.ts b/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/index.ts index 30fc536..a11e8ca 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/index.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/lifecycles/index.ts @@ -1,14 +1,6 @@ import { PlainObject } from 'typings'; -import { Environment } from 'environment'; -export abstract class OverlayScrollbarsLifecycle { - protected environment: Environment; - - constructor(environment: Environment) { - this.environment = environment; - } - - abstract _update(options?: T): void; - - abstract _destruct(): void; +export interface Lifecycle { + _options(options?: T): void; + _destruct(): void; } diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts index 914d8fa..3c4ac5c 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts @@ -13,6 +13,8 @@ import { preventDefault, stopPropagation, addClass, + isString, + equalWH, } from 'support'; import { getEnvironment } from 'environment'; @@ -31,11 +33,11 @@ const getDirection = (elm: HTMLElement) => style(elm, 'direction'); // TODO: // 1. MAYBE add comparison function to offsetSize etc. - +type Direction = 'ltr' | 'rtl'; export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean }; export const createSizeObserver = ( target: HTMLElement, - onSizeChangedCallback: (direction?: boolean) => any, + onSizeChangedCallback: (direction?: Direction) => any, options?: SizeObserverOptions ): (() => void) => { const { _direction: direction = false, _appear: appear = false } = options || {}; @@ -43,13 +45,13 @@ export const createSizeObserver = ( const baseElements = createDOM(`
`); const sizeObserver = baseElements[0] as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement; - const onSizeChangedCallbackProxy = (dir?: boolean) => { + const onSizeChangedCallbackProxy = (dir?: Direction) => { if (direction) { const rtl = getDirection(sizeObserver) === 'rtl'; scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); scrollTop(sizeObserver, scrollAmount); } - onSizeChangedCallback(dir === true); + onSizeChangedCallback(isString(dir) ? dir : undefined); }; const offListeners: (() => void)[] = []; let appearCallback: ((...args: any) => any) | null = appear ? onSizeChangedCallbackProxy : null; @@ -90,7 +92,7 @@ export const createSizeObserver = ( }; const onScroll = (scrollEvent?: Event) => { currSize = offsetSize(listenerElement); - isDirty = !scrollEvent || currSize.w !== cacheSize.w || currSize.h !== cacheSize.h; + isDirty = !scrollEvent || !equalWH(currSize, cacheSize); if (scrollEvent && isDirty && !rAFId) { cAF(rAFId); @@ -132,7 +134,7 @@ export const createSizeObserver = ( style(listenerElement, { left: 0, right: 'auto' }); } dirCache = dir; - onSizeChangedCallbackProxy(true); + onSizeChangedCallbackProxy(dir as Direction); } preventDefault(event); diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts index 88b09f9..6acfa32 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts @@ -38,7 +38,7 @@ export const createTrinsicObserver = ( const newHeightIntrinsic = newSize.h === 0; if (newHeightIntrinsic !== heightIntrinsic) { - onTrinsicChangedCallback(false, newSize.h === 0); + onTrinsicChangedCallback(false, newHeightIntrinsic); heightIntrinsic = newHeightIntrinsic; } }) diff --git a/packages/overlayscrollbars/src/structurelifecycle.scss b/packages/overlayscrollbars/src/structurelifecycle.scss new file mode 100644 index 0000000..b1d9874 --- /dev/null +++ b/packages/overlayscrollbars/src/structurelifecycle.scss @@ -0,0 +1,23 @@ +.os-host { + position: relative; + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.os-viewport { + box-sizing: border-box; + position: relative; + flex: auto; + height: auto; + width: auto; + padding: 0; + margin: 0; + border: none; + overflow: visible; +} + +.os-content { + position: relative; + z-index: 0; +} diff --git a/packages/overlayscrollbars/src/support/cache/cache.ts b/packages/overlayscrollbars/src/support/cache/cache.ts new file mode 100644 index 0000000..6a00351 --- /dev/null +++ b/packages/overlayscrollbars/src/support/cache/cache.ts @@ -0,0 +1,87 @@ +import { each, keys, isArray, isString } from 'support'; + +interface CacheEntry { + _current?: T; + _previous?: T; + _changed?: boolean; +} + +type Cache = { + [P in keyof T]: CacheEntry; +}; + +type UpdateCacheProp =

(prop: P, value: T[P], compare: (a?: T[P], b?: T[P]) => boolean) => void; + +export type CacheUpdateFunction = (current?: T[P], previous?: T[P]) => T[P]; + +export type CacheEqualFunction = (a?: T[P], b?: T[P]) => boolean; + +export type CacheUpdate = { + [P in keyof T]: boolean; +}; + +export type CacheUpdateInfo = { + [P in keyof T]: CacheUpdateFunction | [CacheUpdateFunction, CacheEqualFunction]; +}; + +/** + * Creates a internally managed generic cache which can be updated by the returned function. + * @param cacheUpdateInfo A object which accepts a function or a tuple of functions as values for its properties. + * { + * name: updateFn, + * // or + * name: [updateFn, equalFn] + * } + * The first function is the update function (updateFn) which is executed when this cache prop shall be updated. + * Two params are passed, the first one is the current cache value and the second one is the previous cache value. + * + * The second function is the equal function (equalFn) which is also executed when this cache prop shall be updated, + * but returns a boolean which indicates whether the current value and the new updated value are equal. + * If no equal function is passed a shallow comparison is carried out between the values. + * + * @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param. + * This function returns a object which contains all cache properties as booleans which indicate whether the corresponding cache values really changed or not. + */ +export const createCache = ( + cacheUpdateInfo: CacheUpdateInfo +): ((propsToUpdate?: Array | keyof T, force?: boolean) => CacheUpdate) => { + const cache: Cache = {} as T; + const allProps: Array = keys(cacheUpdateInfo) as Array; + + each(allProps, (prop) => { + cache[prop] = {}; + }); + + const updateCacheProp: UpdateCacheProp = (prop, value, equal): void => { + const curr = cache[prop]._current; + + cache[prop]._current = value; + cache[prop]._previous = curr; + cache[prop]._changed = equal ? !equal(curr, value) : curr !== value; + }; + + const flush = (force?: boolean): CacheUpdate => { + const result: CacheUpdate = {} as CacheUpdate; + + each(allProps, (prop: keyof T) => { + result[prop] = !!(cache[prop]._changed || force); + cache[prop]._changed = false; + }); + + return result; + }; + + return (propsToUpdate?: Array | keyof T, force?: boolean) => { + const finalPropsToUpdate: Array = + (isString(propsToUpdate) ? ([propsToUpdate] as Array) : (propsToUpdate as Array)) || allProps; + each(finalPropsToUpdate, (prop) => { + const cacheVal = cache[prop]; + const curr = cacheUpdateInfo[prop]; + const arr = isArray(curr); + const value = arr ? curr[0] : curr; + const equal = arr ? curr[1] : null; + updateCacheProp(prop, value(cacheVal._current, cacheVal._previous), equal); + }); + return flush(force); + }; +}; diff --git a/packages/overlayscrollbars/src/support/cache/index.ts b/packages/overlayscrollbars/src/support/cache/index.ts new file mode 100644 index 0000000..3061f36 --- /dev/null +++ b/packages/overlayscrollbars/src/support/cache/index.ts @@ -0,0 +1 @@ +export * from 'support/cache/cache'; diff --git a/packages/overlayscrollbars/src/support/dom/style.ts b/packages/overlayscrollbars/src/support/dom/style.ts index 9913163..aa8149a 100644 --- a/packages/overlayscrollbars/src/support/dom/style.ts +++ b/packages/overlayscrollbars/src/support/dom/style.ts @@ -2,6 +2,13 @@ import { each, keys } from 'support/utils'; import { isString, isNumber, isArray } from 'support/utils/types'; import { PlainObject } from 'typings'; +export interface TRBL { + t: number; + r: number; + b: number; + l: number; +} + type CssStyles = { [key: string]: string | number }; const cssNumber = { animationiterationcount: 1, @@ -19,6 +26,12 @@ const cssNumber = { zoom: 1, }; +const parseToZeroOrNumber = (value: string, toFloat?: boolean): number => { + /* istanbul ignore next */ + const num = toFloat ? parseFloat(value) : parseInt(value, 10); + /* istanbul ignore next */ + return Number.isNaN(num) ? 0 : num; +}; const adaptCSSVal = (prop: string, val: string | number): string | number => (!cssNumber[prop.toLowerCase()] && isNumber(val) ? `${val}px` : val); const getCSSVal = (elm: HTMLElement, computedStyle: CSSStyleDeclaration, prop: string): string => /* istanbul ignore next */ @@ -74,3 +87,23 @@ export const hide = (elm: HTMLElement | null): void => { export const show = (elm: HTMLElement | null): void => { style(elm, { display: 'block' }); }; + +/** + * Returns a top + * @param elm + * @param property + */ +export const topRightBottomLeft = (elm: HTMLElement | null, property?: string): TRBL => { + const finalProp = property || ''; + const top = `${finalProp}Top`; + const right = `${finalProp}Right`; + const bottom = `${finalProp}Bottom`; + const left = `${finalProp}Left`; + const result = style(elm, [top, right, bottom, left]); + return { + t: parseToZeroOrNumber(result[top]), + r: parseToZeroOrNumber(result[right]), + b: parseToZeroOrNumber(result[bottom]), + l: parseToZeroOrNumber(result[left]), + }; +}; diff --git a/packages/overlayscrollbars/src/support/index.ts b/packages/overlayscrollbars/src/support/index.ts index 25ec35a..34cac2c 100644 --- a/packages/overlayscrollbars/src/support/index.ts +++ b/packages/overlayscrollbars/src/support/index.ts @@ -1,3 +1,4 @@ +export * from 'support/cache'; export * from 'support/compatibility'; export * from 'support/dom'; export * from 'support/options'; diff --git a/packages/overlayscrollbars/src/support/utils/equal.ts b/packages/overlayscrollbars/src/support/utils/equal.ts new file mode 100644 index 0000000..fc11521 --- /dev/null +++ b/packages/overlayscrollbars/src/support/utils/equal.ts @@ -0,0 +1,46 @@ +import { each } from 'support/utils/array'; +import { WH, XY, TRBL } from 'support/dom'; +import { PlainObject } from 'typings'; + +/** + * Compares two objects and returns true if all values of the passed prop names are identical, false otherwise or if one of the two object is falsy. + * @param a Object a. + * @param b Object b. + * @param props The props which shall be compared. + */ +export const equal = (a: T | undefined, b: T | undefined, props: Array): boolean => { + if (a && b) { + let result = true; + each(props, (prop) => { + if (a[prop] !== b[prop]) { + result = false; + } + }); + return result; + } + return false; +}; + +/** + * Compares object a with object b and returns true if both have the same property values, false otherwise. + * Also returns false if one of the objects is undefined or null. + * @param a Object a. + * @param b Object b. + */ +export const equalWH = (a?: WH, b?: WH) => equal(a, b, ['w', 'h']); + +/** + * Compares object a with object b and returns true if both have the same property values, false otherwise. + * Also returns false if one of the objects is undefined or null. + * @param a Object a. + * @param b Object b. + */ +export const equalXY = (a?: XY, b?: XY) => equal(a, b, ['x', 'y']); + +/** + * Compares object a with object b and returns true if both have the same property values, false otherwise. + * Also returns false if one of the objects is undefined or null. + * @param a Object a. + * @param b Object b. + */ +export const equalTRBL = (a?: TRBL, b?: TRBL) => equal(a, b, ['t', 'r', 'b', 'l']); diff --git a/packages/overlayscrollbars/src/support/utils/index.ts b/packages/overlayscrollbars/src/support/utils/index.ts index 3a3a49c..e80b708 100644 --- a/packages/overlayscrollbars/src/support/utils/index.ts +++ b/packages/overlayscrollbars/src/support/utils/index.ts @@ -1,3 +1,4 @@ export * from 'support/utils/array'; +export * from 'support/utils/equal'; export * from 'support/utils/object'; export * from 'support/utils/types'; diff --git a/packages/overlayscrollbars/src/trinsicobserver.scss b/packages/overlayscrollbars/src/trinsicobserver.scss index 059f2ee..ec2dd46 100644 --- a/packages/overlayscrollbars/src/trinsicobserver.scss +++ b/packages/overlayscrollbars/src/trinsicobserver.scss @@ -14,7 +14,7 @@ &:not(:empty) { height: calc(100% + 1px); - top: auto; + top: -1px; & > .os-size-observer { width: 1000%; diff --git a/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts new file mode 100644 index 0000000..8428581 --- /dev/null +++ b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts @@ -0,0 +1,191 @@ +import { createCache } from 'support/cache'; + +const createUpdater = (updaterReturn: (i: number) => T) => { + const fn = jest.fn(); + let index = 0; + const update = (curr?: T, prev?: T): T => { + fn(curr, prev); + index += 1; + return updaterReturn(index); + }; + + return [fn, update]; +}; + +describe('cache', () => { + describe('createCache', () => { + test('creates and updates simple cache', () => { + const [updateNumberFn, updateNumber] = createUpdater((i) => i); + const [updateBooleanFn, updateBoolean] = createUpdater((i) => !!(i % 2)); + const [updateStringFn, updateString] = createUpdater((i) => `${i}`); + const [updateObjFn, updateObj] = createUpdater((i) => ({ [i]: i })); + + const updateCache = createCache({ + number: updateNumber, + boolean: updateBoolean, + string: updateString, + object: updateObj, + }); + + expect(updateCache('number').number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledTimes(1); + expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + + expect(updateCache('number').number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledTimes(2); + expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); + + expect(updateCache('number').number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledTimes(3); + expect(updateNumberFn).toHaveBeenCalledWith(2, 1); + + let { string, boolean, object, number } = updateCache('number'); + expect(string).toBe(false); + expect(boolean).toBe(false); + expect(object).toBe(false); + expect(number).toBe(true); + + expect(updateBooleanFn).not.toHaveBeenCalled(); + expect(updateStringFn).not.toHaveBeenCalled(); + expect(updateObjFn).not.toHaveBeenCalled(); + + ({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object'])); + expect(string).toBe(true); + expect(boolean).toBe(true); + expect(object).toBe(true); + expect(number).toBe(false); + + expect(updateBooleanFn).toHaveBeenCalledTimes(1); + expect(updateBooleanFn).toHaveBeenCalledWith(undefined, undefined); + + expect(updateStringFn).toHaveBeenCalledTimes(1); + expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); + + expect(updateObjFn).toHaveBeenCalledTimes(1); + expect(updateObjFn).toHaveBeenCalledWith(undefined, undefined); + + updateCache(['string', 'boolean', 'object']); + expect(updateBooleanFn).toHaveBeenCalledTimes(2); + expect(updateBooleanFn).toHaveBeenCalledWith(!!(1 % 2), undefined); + + expect(updateStringFn).toHaveBeenCalledTimes(2); + expect(updateStringFn).toHaveBeenCalledWith('1', undefined); + + expect(updateObjFn).toHaveBeenCalledTimes(2); + expect(updateObjFn).toHaveBeenCalledWith({ 1: 1 }, undefined); + + updateCache(['string', 'boolean', 'object']); + expect(updateBooleanFn).toHaveBeenCalledTimes(3); + expect(updateBooleanFn).toHaveBeenCalledWith(!!(2 % 2), !!(1 % 2)); + + expect(updateStringFn).toHaveBeenCalledTimes(3); + expect(updateStringFn).toHaveBeenCalledWith('2', '1'); + + expect(updateObjFn).toHaveBeenCalledTimes(3); + expect(updateObjFn).toHaveBeenCalledWith({ 2: 2 }, { 1: 1 }); + + updateCache(['string', 'boolean', 'object']); + ({ string, boolean, object, number } = updateCache()); + expect(string).toBe(true); + expect(boolean).toBe(true); + expect(object).toBe(true); + expect(number).toBe(true); + + expect(updateBooleanFn).toHaveBeenCalledTimes(5); + expect(updateStringFn).toHaveBeenCalledTimes(5); + expect(updateObjFn).toHaveBeenCalledTimes(5); + expect(updateNumberFn).toHaveBeenCalledTimes(5); + }); + + test('doesnt update if nothing changes with primitives', () => { + const [updateNumberFn, updateNumber] = createUpdater(() => 0); + const updateCache = createCache({ + number: updateNumber, + }); + + expect(updateCache('number').number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + + expect(updateCache('number').number).toBe(false); + expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); + + expect(updateCache('number').number).toBe(false); + expect(updateNumberFn).toHaveBeenCalledWith(0, 0); + }); + + test('doesnt update if nothing changes with non primitives', () => { + const constObj = { a: 0, b: 0 }; + const [updateConstObjFn, updateConstObj] = createUpdater<{ a: number; b: number }>(() => constObj); + const [updateSimilarObjFn, updateSimilarObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj })); + const [updateComparisonObjFn, updateComparisonObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj })); + const updateCache = createCache({ + constObj: updateConstObj, + similarObj: updateSimilarObj, + comparisonObj: [ + updateComparisonObj, + (a?: { a: number; b: number }, b?: { a: number; b: number }): boolean => !!(a && b && a.a === b.a && a.b === b.b), + ], + }); + + expect(updateCache('constObj').constObj).toBe(true); + expect(updateConstObjFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateCache('constObj').constObj).toBe(false); + expect(updateConstObjFn).toHaveBeenCalledWith(constObj, undefined); + expect(updateCache('constObj').constObj).toBe(false); + expect(updateConstObjFn).toHaveBeenCalledWith(constObj, constObj); + + expect(updateCache('similarObj').similarObj).toBe(true); + expect(updateSimilarObjFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateCache('similarObj').similarObj).toBe(true); + expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, undefined); + expect(updateCache('similarObj').similarObj).toBe(true); + expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, constObj); + + expect(updateCache('comparisonObj').comparisonObj).toBe(true); + expect(updateComparisonObjFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateCache('comparisonObj').comparisonObj).toBe(false); + expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, undefined); + expect(updateCache('comparisonObj').comparisonObj).toBe(false); + expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, constObj); + }); + + test('updates definitely with force', () => { + const [updateNumberFn, updateNumber] = createUpdater(() => 0); + const updateCache = createCache({ + number: updateNumber, + }); + + expect(updateCache('number', true).number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + + expect(updateCache('number', true).number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); + + expect(updateCache('number', true).number).toBe(true); + expect(updateNumberFn).toHaveBeenCalledWith(0, 0); + }); + + test('custom comparison on primitves', () => { + const [updateStringFn, updateString] = createUpdater(() => 'hi'); + const [updateNumberFn, updateNumber] = createUpdater((i) => i); + const updateCache = createCache({ + string: [updateString, () => false], + number: [updateNumber, () => true], + }); + + expect(updateCache('string').string).toBe(true); + expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateCache('string').string).toBe(true); + expect(updateStringFn).toHaveBeenCalledWith('hi', undefined); + expect(updateCache('string').string).toBe(true); + expect(updateStringFn).toHaveBeenCalledWith('hi', 'hi'); + + expect(updateCache('number').number).toBe(false); + expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateCache('number').number).toBe(false); + expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); + expect(updateCache('number').number).toBe(false); + expect(updateNumberFn).toHaveBeenCalledWith(2, 1); + }); + }); +}); diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/style.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/style.test.ts index f0d2891..f7685a4 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/style.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/style.test.ts @@ -1,5 +1,5 @@ import { isString, isPlainObject, isEmptyObject } from 'support/utils/types'; -import { style, hide, show } from 'support/dom/style'; +import { style, hide, show, topRightBottomLeft } from 'support/dom/style'; describe('dom style', () => { afterEach(() => { @@ -90,4 +90,22 @@ describe('dom style', () => { expect(show(null)).toBe(undefined); }); }); + + describe('topRightBottomLeft', () => { + test('normal', () => { + const result = topRightBottomLeft(document.body); + expect(result.t).toBe(0); + expect(result.r).toBe(0); + expect(result.b).toBe(0); + expect(result.l).toBe(0); + }); + + test('null', () => { + const result = topRightBottomLeft(null); + expect(result.t).toBe(0); + expect(result.r).toBe(0); + expect(result.b).toBe(0); + expect(result.l).toBe(0); + }); + }); }); diff --git a/packages/overlayscrollbars/tests/jsdom/support/utils/equal.test.ts b/packages/overlayscrollbars/tests/jsdom/support/utils/equal.test.ts new file mode 100644 index 0000000..db66f82 --- /dev/null +++ b/packages/overlayscrollbars/tests/jsdom/support/utils/equal.test.ts @@ -0,0 +1,36 @@ +import { equal, equalTRBL, equalWH, equalXY } from 'support/utils/equal'; + +describe('equal', () => { + test('equal', () => { + interface Test { + a: number; + b: number; + c?: number; + } + const equalTest = (a?: Test, b?: Test) => equal(a, b, ['a', 'b']); + + expect(equalTest({ a: 1, b: 1 }, { a: 1, b: 1 })).toBe(true); + expect(equalTest({ a: 1, b: 1 }, { a: 1, b: 1, c: 5 })).toBe(true); + expect(equalTest({ a: 1, b: 1, c: 4 }, { a: 1, b: 1, c: 5 })).toBe(true); + + expect(equalTest({ a: 1, b: 1 }, { a: 2, b: 2 })).toBe(false); + expect(equalTest({ a: 1, b: 1 }, { a: 2, b: 1 })).toBe(false); + expect(equalTest(undefined, { a: 2, b: 1 })).toBe(false); + expect(equalTest({ a: 1, b: 1 }, undefined)).toBe(false); + }); + + test('equalTRBL', () => { + expect(equalTRBL({ t: 0, r: 0, b: 0, l: 0 }, { t: 0, r: 0, b: 0, l: 0 })).toBe(true); + expect(equalTRBL({ t: 0, r: 0, b: 0, l: 0 }, { t: 0, r: 0, b: 0, l: 1 })).toBe(false); + }); + + test('equalWH', () => { + expect(equalWH({ w: 0, h: 0 }, { w: 0, h: 0 })).toBe(true); + expect(equalWH({ w: 0, h: 0 }, { w: 0, h: 1 })).toBe(false); + }); + + test('equalXY', () => { + expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 0 })).toBe(true); + expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 1 })).toBe(false); + }); +}); diff --git a/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts index 0aa1898..b5f5836 100644 --- a/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts @@ -1,7 +1,8 @@ import 'overlayscrollbars.scss'; +import { createDOM, appendChildren } from 'support'; import { getEnvironment } from 'environment'; const envInstance = getEnvironment(); -document.body.textContent = JSON.stringify(envInstance); +appendChildren(document.body, createDOM(`
${JSON.stringify(envInstance)}
`)[0]); export { envInstance }; diff --git a/packages/overlayscrollbars/tests/puppeteer/Environment/index.html b/packages/overlayscrollbars/tests/puppeteer/Environment/index.html index cf9775f..45b983b 100644 --- a/packages/overlayscrollbars/tests/puppeteer/Environment/index.html +++ b/packages/overlayscrollbars/tests/puppeteer/Environment/index.html @@ -1,4 +1 @@ -
-
-
-
+hi diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts index 8237b89..8fe0d0e 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts @@ -158,7 +158,7 @@ startBtn?.addEventListener('click', start); createSizeObserver( targetElm as HTMLElement, - (direction?: boolean) => { + (direction?: 'ltr' | 'rtl') => { if (direction) { directionIterations += 1; } else { diff --git a/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.browser.ts new file mode 100644 index 0000000..a3f2d0b --- /dev/null +++ b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.browser.ts @@ -0,0 +1,7 @@ +import 'overlayscrollbars.scss'; +import './index.scss'; +import { createStructureLifecycle } from 'overlayscrollbars/lifecycles/StructureLifecycle'; + +const targetElm = document.querySelector('#target') as HTMLElement; + +const structureLifecycle = createStructureLifecycle(targetElm); diff --git a/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.html b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.html new file mode 100644 index 0000000..67d5e9b --- /dev/null +++ b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.html @@ -0,0 +1,53 @@ +
+ + + + + + + + + + + + + + + + + Detected resizes: 0 +
+
+
+
+
Resize
+
100%
+
End
+
+
+
diff --git a/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.scss b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.scss new file mode 100644 index 0000000..0fe23c2 --- /dev/null +++ b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.scss @@ -0,0 +1,137 @@ +body { + display: flex; + flex-direction: column; +} +#controls { + flex: none; +} +#stage { + flex: auto; + position: relative; + + & > div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: lightgoldenrodyellow; + } +} + +#canvas > div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +#target { + overflow: hidden; + resize: both; + position: relative; + border: 2px solid red; + min-height: 100px; + min-width: 200px; + max-height: 300px; + max-width: 320px; +} + +#resize { + overflow: hidden; + resize: both; + background: blue; + border: 1px solid black; + padding: 10px; +} + +#hundred { + height: 100%; + background: purple; + border: 1px solid black; + padding: 10px; +} + +#end { + position: relative; + background: green; + border: 1px solid black; + padding: 10px; + margin: 10px; +} + +#end::before { + content: ''; + position: absolute; + display: block; + top: -11px; + right: -11px; + bottom: -11px; + left: -11px; + background: green; + z-index: -1; + opacity: 0.5; +} + +.padding0 { + padding: 0; +} +.padding10 { + padding: 10px; +} +.padding50 { + padding: 50px; +} + +.border2 { + border: 2px solid red; +} +.border10 { + border: 10px solid red; +} +.border0 { + border: none; +} + +.heightAuto { + height: auto; +} +.height200 { + height: 200px; +} +.heightHundred { + height: 100%; +} + +.widthAuto { + width: auto; + float: left; +} +.width200 { + width: 200px; +} +.widthHundred { + width: 100%; +} + +.boxSizingBorderBox { + box-sizing: border-box; +} +.boxSizingContentBox { + box-sizing: content-box; +} + +.displayNone { + display: none; +} +.displayBlock { + display: block; +} + +.directionltr { + direction: ltr; +} +.directionRTL { + direction: rtl; +} diff --git a/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.test.ts b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.test.ts new file mode 100644 index 0000000..45d7920 --- /dev/null +++ b/packages/overlayscrollbars/tests/puppeteer/StructureLifecycle/index.test.ts @@ -0,0 +1,15 @@ +import { Environment } from 'environment'; +import url from './.build/build.html'; + +describe('StructureLifecycle', () => { + beforeAll(async () => { + await page.goto(url); + }); + + it('should be titled "Environment"', async () => { + // @ts-ignore + const a: Environment = await page.evaluate(() => window.Environment.envInstance); + console.log(a); + await expect(page.title()).resolves.toMatch('Environment'); + }); +});