From 84fbb436f95ce6598cc315db927154948de109bf Mon Sep 17 00:00:00 2001 From: Rene Date: Sun, 8 Nov 2020 03:08:28 +0100 Subject: [PATCH] add direction observer and improve sizeobserver tests --- ...{createSizeObserver.ts => SizeObserver.ts} | 57 +++++++-- .../overlayscrollbars/src/sizeobserver.scss | 9 +- .../puppeteer/SizeObserver/index.browser.ts | 111 ++++++++++++++---- .../tests/puppeteer/SizeObserver/index.html | 87 ++++++++------ .../tests/puppeteer/SizeObserver/index.scss | 45 ++++++- packages/testing-browser/src/Select.ts | 2 +- packages/testing-browser/src/Timeout.ts | 1 + rollup.config.base.js | 6 - 8 files changed, 231 insertions(+), 87 deletions(-) rename packages/overlayscrollbars/src/overlayscrollbars/observers/{createSizeObserver.ts => SizeObserver.ts} (68%) create mode 100644 packages/testing-browser/src/Timeout.ts diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts similarity index 68% rename from packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts rename to packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts index 524670f..888cf17 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts @@ -1,4 +1,5 @@ -import { createDOM, style, appendChildren, offsetSize, scrollLeft, scrollTop, jsAPI, addClass, each } from 'support'; +import { createDOM, style, appendChildren, offsetSize, scrollLeft, scrollTop, jsAPI, each, prependChildren, removeElements } from 'support'; +import { Environment } from 'environment'; const animationStartEventName = 'animationstart'; const scrollEventName = 'scroll'; @@ -10,25 +11,38 @@ const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`; const cAF = cancelAnimationFrame; const rAF = requestAnimationFrame; +const getDirection = (elm: HTMLElement) => style(elm, 'direction'); // TODO: // 1. handling for event listeners (animationStartEventName.split(' ')) // 2. return not just element but also destruction function // 3. shorthand handling for preventDefault & stopPropagation etc. -// 4. add functionality & tests for direction change // 5. MAYBE add comparison function to offsetSize etc. -// 6. Create test utils (waitFor) -export const createSizeObserver = (onSizeChangedCallback: () => void) => { +export const createSizeObserver = ( + target: HTMLElement, + onSizeChangedCallback: (direction?: boolean) => any, + environment: Environment, + direction?: boolean +) => { + const rtlScrollBehavior = environment._rtlScrollBehavior; const baseElements = createDOM(`
`); const sizeObserver = baseElements[0] as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement; - let appearCallback = onSizeChangedCallback; + const onSizeChangedCallbackProxy = (dir?: boolean) => { + if (direction) { + const rtl = getDirection(sizeObserver) === 'rtl'; + scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); + scrollTop(sizeObserver, scrollAmount); + } + onSizeChangedCallback(dir === true); + }; + let appearCallback: (...args: any) => any = onSizeChangedCallbackProxy; + if (ResizeObserverConstructor) { - const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallback); + const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallbackProxy); resizeObserverInstance.observe(listenerElement); } else { - addClass(sizeObserver, 'scroll-observer'); const observerElementChildren = createDOM( `
` ); @@ -54,7 +68,7 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => { if (!isDirty) return; cacheSize = currSize; - onSizeChangedCallback(); + onSizeChangedCallbackProxy(); }; const onScroll = (scrollEvent?: Event) => { currSize = offsetSize(listenerElement); @@ -91,5 +105,30 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => { }); }); - return sizeObserver; + if (direction) { + let dirCache: string | undefined; + sizeObserver.addEventListener('scroll', (event: Event) => { + const dir = getDirection(sizeObserver); + const changed = dir !== dirCache; + if (changed) { + if (dir === 'rtl') { + style(listenerElement, { left: 'auto', right: 0 }); + } else { + style(listenerElement, { left: 0, right: 'auto' }); + } + dirCache = dir; + onSizeChangedCallbackProxy(true); + } + + event.preventDefault(); + event.stopPropagation(); + return false; + }); + } + + prependChildren(target, sizeObserver); + + return () => { + removeElements(sizeObserver); + }; }; diff --git a/packages/overlayscrollbars/src/sizeobserver.scss b/packages/overlayscrollbars/src/sizeobserver.scss index b5db5e1..dc159bb 100644 --- a/packages/overlayscrollbars/src/sizeobserver.scss +++ b/packages/overlayscrollbars/src/sizeobserver.scss @@ -2,7 +2,9 @@ $scrollbar-cushion: 100px; .os-size-observer, .os-size-observer-listener { + direction: inherit; padding: inherit; + border: inherit; margin: 0; pointer-events: none; overflow: hidden; @@ -25,18 +27,13 @@ $scrollbar-cushion: 100px; z-index: -1; animation-duration: 0.001s; animation-name: os-size-observer-appear-animation; - - &.scroll-observer { - .os-size-observer-listener { - box-sizing: content-box; - } - } } .os-size-observer-listener { display: block; height: 200%; width: 200%; + box-sizing: content-box; // lets assume no scrollbar is 100px wide & > .os-size-observer-listener-item { diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts index 07f4126..651b18a 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts @@ -4,9 +4,24 @@ import should from 'should'; import { waitFor } from '@testing-library/dom'; import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select'; import { setTestResult } from '@/testing-browser/TestResult'; -import { hasDimensions, offsetSize, WH } from 'support'; +import { hasDimensions, offsetSize, WH, style } from 'support'; -import { createSizeObserver } from 'overlayscrollbars/observers/createSizeObserver'; +import { Environment } from 'environment'; +import { createSizeObserver } from 'overlayscrollbars/observers/SizeObserver'; + +let sizeIterations = 0; +let directionIterations = 0; +const contentBox = (elm: HTMLElement | null) => { + if (elm) { + const computedStyle = window.getComputedStyle(elm); + return { + width: elm.clientWidth - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)), + height: elm.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)), + }; + } + + return { width: 0, height: 0 }; +}; const targetElm = document.querySelector('#target'); const heightSelect: HTMLSelectElement | null = document.querySelector('#height'); @@ -15,44 +30,67 @@ const paddingSelect: HTMLSelectElement | null = document.querySelector('#padding const borderSelect: HTMLSelectElement | null = document.querySelector('#border'); const boxSizingSelect: HTMLSelectElement | null = document.querySelector('#boxSizing'); const displaySelect: HTMLSelectElement | null = document.querySelector('#display'); +const directionSelect: HTMLSelectElement | null = document.querySelector('#direction'); const startBtn: HTMLButtonElement | null = document.querySelector('#start'); const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes'); -let iterations = 0; -const observerElm = createSizeObserver(() => { - iterations += 1; - requestAnimationFrame(() => { - if (resizesSlot) { - resizesSlot.textContent = iterations.toString(); - } - }); -}); - const selectCallback = generateSelectCallback(targetElm as HTMLElement); - const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => { - await iterateSelect<{ currIterations: number; currOffsetSize: WH }>(select, { + interface IterateSelect { + currSizeIterations: number; + currDirectionIterations: number; + currOffsetSize: WH; + currDir: string; + } + + await iterateSelect(select, { beforeEach() { - const currIterations = iterations; + const currSizeIterations = sizeIterations; + const currDirectionIterations = directionIterations; const currOffsetSize = offsetSize(targetElm as HTMLElement); + const currDir = style(targetElm as HTMLElement, 'direction'); return { - currIterations, + currSizeIterations, + currDirectionIterations, currOffsetSize, + currDir, }; }, - async check({ currIterations, currOffsetSize }) { + async check({ currSizeIterations, currDirectionIterations, currOffsetSize, currDir }) { const newOffsetSize = offsetSize(targetElm as HTMLElement); + const newDir = style(targetElm as HTMLElement, 'direction'); const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h; + const dirChanged = currDir !== newDir; + const dimensions = hasDimensions(targetElm as HTMLElement); + const contentSize = contentBox(targetElm as HTMLElement); + const observerElm = targetElm?.firstElementChild as HTMLElement; - if (hasDimensions(targetElm as HTMLElement) && offsetSizeChanged) { - // eslint-disable-next-line - await waitFor(() => should.equal(iterations, currIterations + 1), { - onTimeout(error): Error { - setTestResult(false); - return error; + // no overflow if not needed + if (targetElm && contentSize.width > 0) { + should.ok(observerElm.getBoundingClientRect().right <= targetElm.getBoundingClientRect().right); + } + if (targetElm && contentSize.height > 0) { + should.ok(observerElm.getBoundingClientRect().bottom <= targetElm.getBoundingClientRect().bottom); + } + + if (dimensions && (offsetSizeChanged || dirChanged)) { + await waitFor( + async () => { + if (offsetSizeChanged) { + should.equal(sizeIterations, currSizeIterations + 1); + } + if (dirChanged) { + should.equal(directionIterations, currDirectionIterations + 1); + } }, - }); + { + onTimeout(error): Error { + setTestResult(false); + return error; + }, + } + ); } }, afterEach, @@ -65,6 +103,7 @@ paddingSelect?.addEventListener('change', selectCallback); borderSelect?.addEventListener('change', selectCallback); boxSizingSelect?.addEventListener('change', selectCallback); displaySelect?.addEventListener('change', selectCallback); +directionSelect?.addEventListener('change', selectCallback); selectCallback(heightSelect); selectCallback(widthSelect); @@ -72,6 +111,7 @@ selectCallback(paddingSelect); selectCallback(borderSelect); selectCallback(boxSizingSelect); selectCallback(displaySelect); +selectCallback(directionSelect); const iteratePadding = async (afterEach?: () => any) => { await iterate(paddingSelect, afterEach); @@ -91,16 +131,21 @@ const iterateBoxSizing = async (afterEach?: () => any) => { const iterateDisplay = async (afterEach?: () => any) => { await iterate(displaySelect, afterEach); }; +const iterateDirection = async (afterEach?: () => any) => { + await iterate(directionSelect, afterEach); +}; const start = async () => { setTestResult(null); targetElm?.removeAttribute('style'); await iterateDisplay(); + await iterateDirection(); await iterateBoxSizing(async () => { await iterateHeight(async () => { await iterateWidth(async () => { await iterateBorder(async () => { + await iterateDirection(); await iteratePadding(); }); }); @@ -112,6 +157,22 @@ const start = async () => { startBtn?.addEventListener('click', start); -targetElm?.appendChild(observerElm); +createSizeObserver( + targetElm as HTMLElement, + (direction?: boolean) => { + if (direction) { + directionIterations += 1; + } else { + sizeIterations += 1; + } + requestAnimationFrame(() => { + if (resizesSlot) { + resizesSlot.textContent = (directionIterations + sizeIterations).toString(); + } + }); + }, + new Environment(), + true +); export { start }; diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html index a97c43c..05167a7 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html @@ -1,40 +1,49 @@ - - - - - - - - - - - - +
+ + + + + + + + + + + + + + - -Detected resizes: 0 -
- -
+ + Detected resizes: 0 +
+
+
+
+
+
diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss index 7131e1b..8e9c890 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss @@ -1,5 +1,34 @@ +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: scroll; + overflow: hidden; resize: both; position: relative; // prevent container from reaching 0x0 dimensions for testing purposes @@ -48,9 +77,23 @@ 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/testing-browser/src/Select.ts b/packages/testing-browser/src/Select.ts index 154b93c..076007b 100644 --- a/packages/testing-browser/src/Select.ts +++ b/packages/testing-browser/src/Select.ts @@ -16,7 +16,7 @@ export const generateSelectCallback = (targetElm: HTMLElement | null) => (event: const selectOptions = getSelectOptions(target); if (targetElm) { - targetElm.classList.remove(...selectOptions); + selectOptions.forEach((clazz) => targetElm.classList.remove(clazz)); targetElm.classList.add(selectedOption); } } diff --git a/packages/testing-browser/src/Timeout.ts b/packages/testing-browser/src/Timeout.ts new file mode 100644 index 0000000..367d172 --- /dev/null +++ b/packages/testing-browser/src/Timeout.ts @@ -0,0 +1 @@ +export const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/rollup.config.base.js b/rollup.config.base.js index 7f03a2e..37d2a65 100644 --- a/rollup.config.base.js +++ b/rollup.config.base.js @@ -256,12 +256,6 @@ const rollupConfig = (config = {}, { project = process.cwd(), overwrite = {}, si if (isLast) { build.plugins.push({ name: 'deleteCacheDirs', - moduleParsed(moduleInfo) { - //if (filename.includes('index.browser')) { - console.log('moduleInfo', moduleInfo); - //console.log('importer', filename); - //} - }, writeBundle() { const cacheDirs = cache.map((dir) => path.resolve(projectPath, dir)); const deletedDirs = del.sync(cacheDirs);