From 29af442b3831c3e4a07089ade9cf95af612054db Mon Sep 17 00:00:00 2001 From: Rene Date: Sat, 24 Oct 2020 01:14:52 +0200 Subject: [PATCH] size observer observe appearance --- .../observers/createSizeObserver.ts | 27 +-- .../overlayscrollbars/src/sizeobserver.scss | 7 +- .../src/support/dom/dimensions.ts | 7 + .../src/support/dom/traversal.ts | 17 +- .../jsdom/support/dom/dimensions.test.ts | 21 ++- .../tests/jsdom/support/dom/traversal.test.ts | 11 -- .../tests/puppeteer/SizeObserver/index.html | 17 ++ .../tests/puppeteer/SizeObserver/index.scss | 21 ++- .../tests/puppeteer/SizeObserver/index.ts | 154 ++++++++++++------ 9 files changed, 186 insertions(+), 96 deletions(-) diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts index 05f3b7d..524670f 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/observers/createSizeObserver.ts @@ -1,6 +1,6 @@ import { createDOM, style, appendChildren, offsetSize, scrollLeft, scrollTop, jsAPI, addClass, each } from 'support'; -const animationStartEventName = 'animationstart mozAnimationStart webkitAnimationStart MSAnimationStart'; +const animationStartEventName = 'animationstart'; const scrollEventName = 'scroll'; const scrollAmount = 3333333; const ResizeObserverConstructor = jsAPI('ResizeObserver'); @@ -15,20 +15,20 @@ const rAF = requestAnimationFrame; // 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 test for appearance (display: none => display: block) -// 5. add functionality & tests for direction change -// 6. MAYBE add comparison function to offsetSize etc. -// 7. Create test utils (waitFor) +// 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) => { const baseElements = createDOM(`
`); const sizeObserver = baseElements[0] as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement; + let appearCallback = onSizeChangedCallback; if (ResizeObserverConstructor) { - addClass(sizeObserver, 'resize-observer'); const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallback); resizeObserverInstance.observe(listenerElement); } else { + addClass(sizeObserver, 'scroll-observer'); const observerElementChildren = createDOM( `
` ); @@ -58,7 +58,7 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => { }; const onScroll = (scrollEvent?: Event) => { currSize = offsetSize(listenerElement); - isDirty = currSize.w !== cacheSize.w || currSize.h !== cacheSize.h; + isDirty = !scrollEvent || currSize.w !== cacheSize.w || currSize.h !== cacheSize.h; if (scrollEvent && isDirty && !rAFId) { cAF(rAFId); @@ -75,18 +75,21 @@ export const createSizeObserver = (onSizeChangedCallback: () => void) => { expandElement.addEventListener(scrollEventName, onScroll); shrinkElement.addEventListener(scrollEventName, onScroll); - each(animationStartEventName.split(' '), (eventName) => { - sizeObserver.addEventListener(eventName, () => { - onScroll(); - }); - }); + // lets assume that the divs will never be that large and a constant value is enough style(expandElementChild, { width: scrollAmount, height: scrollAmount, }); reset(); + appearCallback = onScroll; } + each(animationStartEventName.split(' '), (eventName) => { + sizeObserver.addEventListener(eventName, () => { + appearCallback(); + }); + }); + return sizeObserver; }; diff --git a/packages/overlayscrollbars/src/sizeobserver.scss b/packages/overlayscrollbars/src/sizeobserver.scss index 27960a1..b5db5e1 100644 --- a/packages/overlayscrollbars/src/sizeobserver.scss +++ b/packages/overlayscrollbars/src/sizeobserver.scss @@ -7,6 +7,7 @@ $scrollbar-cushion: 100px; pointer-events: none; overflow: hidden; visibility: hidden; + box-sizing: border-box; } .os-size-observer, @@ -25,10 +26,9 @@ $scrollbar-cushion: 100px; animation-duration: 0.001s; animation-name: os-size-observer-appear-animation; - &.resize-observer { + &.scroll-observer { .os-size-observer-listener { - position: absolute; - box-sizing: border-box; + box-sizing: content-box; } } } @@ -37,7 +37,6 @@ $scrollbar-cushion: 100px; 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/src/support/dom/dimensions.ts b/packages/overlayscrollbars/src/support/dom/dimensions.ts index a26fb1a..531937b 100644 --- a/packages/overlayscrollbars/src/support/dom/dimensions.ts +++ b/packages/overlayscrollbars/src/support/dom/dimensions.ts @@ -1,5 +1,6 @@ import { WH } from 'support/dom'; +const elementHasDimensions = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length); const zeroObj: WH = { w: 0, h: 0, @@ -42,3 +43,9 @@ export const clientSize = (elm: HTMLElement | null): WH => * @param elm The element of which the BoundingClientRect shall be returned. */ export const getBoundingClientRect = (elm: HTMLElement): DOMRect => elm.getBoundingClientRect(); + +/** + * Determines whether the passed element has any dimensions. + * @param elm The element. + */ +export const hasDimensions = (elm: HTMLElement | null): boolean => (elm ? elementHasDimensions(elm as HTMLElement) : false); diff --git a/packages/overlayscrollbars/src/support/dom/traversal.ts b/packages/overlayscrollbars/src/support/dom/traversal.ts index df61979..c2c37a7 100644 --- a/packages/overlayscrollbars/src/support/dom/traversal.ts +++ b/packages/overlayscrollbars/src/support/dom/traversal.ts @@ -1,7 +1,5 @@ import { each, from } from 'support/utils/array'; -const elementIsVisible = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length); - /** * Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided. * @param selector The selector which has to be searched by. @@ -29,20 +27,7 @@ export const findFirst = (selector: string, elm?: Element | null): Element | nul * @param elm The element which has to be compared with the passed selector. * @param selector The selector which has to be compared with the passed element. Additional selectors: ':visible' and ':hidden'. */ -export const is = (elm: Element | null, selector: string): boolean => { - if (elm) { - if (selector === ':visible') { - return elementIsVisible(elm as HTMLElement); - } - if (selector === ':hidden') { - return !elementIsVisible(elm as HTMLElement); - } - if (elm.matches(selector)) { - return true; - } - } - return false; -}; +export const is = (elm: Element | null, selector: string): boolean => (elm ? elm.matches(selector) : false); /** * Returns the children (no text-nodes or comments) of the passed element which are matching the passed selector. An empty array is returned if the passed element is null. diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts index b0078fc..7344eec 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts @@ -1,5 +1,6 @@ import { isNumber, isPlainObject } from 'support/utils/types'; -import { windowSize, offsetSize, clientSize, getBoundingClientRect } from 'support/dom/dimensions'; +import { createDiv } from 'support/dom/create'; +import { windowSize, offsetSize, clientSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions'; describe('dom dimensions', () => { describe('offsetSize', () => { @@ -44,4 +45,22 @@ describe('dom dimensions', () => { test('getBoundingClientRect', () => { expect(getBoundingClientRect(document.body)).toEqual(document.body.getBoundingClientRect()); }); + + describe('hasDimensions', () => { + test('DOM element', () => { + const result = hasDimensions(document.body); + expect(result).toBe(true); + }); + + test('generated element', () => { + const div = createDiv(); + const result = hasDimensions(div); + expect(result).toBe(false); + }); + + test('null', () => { + const result = hasDimensions(null); + expect(result).toBe(false); + }); + }); }); diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts index f7b64ed..2062c33 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts @@ -119,11 +119,6 @@ describe('dom traversal', () => { expect(is(findFirst('.div-class'), '.other-class')).toBe(false); }); - test('visibility', () => { - expect(is(findFirst('.div-class'), ':visible')).toBe(false); - expect(is(findFirst('.div-class'), ':hidden')).toBe(true); - }); - test('created', () => { const div = createDiv(); expect(div.parentNode).toBeNull(); @@ -139,9 +134,6 @@ describe('dom traversal', () => { expect(is(div, '.div-class')).toBe(false); expect(is(div, '.other-class')).toBe(false); - - expect(is(div, ':visible')).toBe(false); - expect(is(div, ':hidden')).toBe(true); }); test('none', () => { @@ -154,9 +146,6 @@ describe('dom traversal', () => { expect(is(null, '.div-class')).toBe(false); expect(is(null, '.other-class')).toBe(false); - - expect(is(null, ':visible')).toBe(false); - expect(is(null, ':hidden')).toBe(false); }); }); diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html index f7b44ad..a97c43c 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.html @@ -16,6 +16,23 @@ + + + + + + + Detected resizes: 0
diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss index 7226546..7131e1b 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.scss @@ -1,8 +1,10 @@ #target { - border: 2px solid red; overflow: scroll; resize: both; position: relative; + // prevent container from reaching 0x0 dimensions for testing purposes + min-width: 50px; + min-height: 50px; } .padding0 { @@ -15,6 +17,16 @@ padding: 50px; } +.border2 { + border: 2px solid red; +} +.border10 { + border: 10px solid red; +} +.border0 { + border: none; +} + .heightAuto { height: auto; } @@ -35,3 +47,10 @@ .widthHundred { width: 100%; } + +.displayNone { + display: none; +} +.displayBlock { + display: block; +} diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.ts b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.ts index 2a0deb7..9046e77 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.ts +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.ts @@ -1,15 +1,28 @@ import 'overlayscrollbars.scss'; import './index.scss'; import { createSizeObserver } from 'overlayscrollbars/observers/createSizeObserver'; -import { from, removeClass, addClass } from 'support'; +import { from, removeClass, addClass, hasDimensions, isString, isNumber, offsetSize } from 'support'; const targetElm = document.querySelector('#target'); const heightSelect: HTMLSelectElement | null = document.querySelector('#height'); const widthSelect: HTMLSelectElement | null = document.querySelector('#width'); 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 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 getSelectOptions = (selectElement: HTMLSelectElement) => { const arr = from(selectElement.options).map((option) => option.value); return arr; @@ -24,31 +37,35 @@ const selectCallback = (event: Event) => { addClass(targetElm, selectedOption); }; -heightSelect?.addEventListener('change', selectCallback); -widthSelect?.addEventListener('change', selectCallback); -paddingSelect?.addEventListener('change', selectCallback); +const selectOption = (select: HTMLSelectElement | null, selectedOption: string | number): boolean => { + if (!select) { + return false; + } -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -selectCallback({ target: heightSelect }); -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -selectCallback({ target: widthSelect }); -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -selectCallback({ target: paddingSelect }); + const options = getSelectOptions(select); + const currValue = select.value; -let iterations = 0; -const observerElm = createSizeObserver(() => { - iterations += 1; - requestAnimationFrame(() => { - if (resizesSlot) { - resizesSlot.textContent = iterations.toString(); - } - }); -}); + if (selectedOption === currValue) { + return false; + } -targetElm?.appendChild(observerElm); + if (isString(selectedOption) && options.includes(selectedOption)) { + select.value = selectedOption; + } else if (isNumber(selectedOption) && options.length < selectedOption && selectedOption > -1) { + select.selectedIndex = selectedOption; + } + + let event; + if (typeof Event === 'function') { + event = new Event('change'); + } else { + event = document.createEvent('Event'); + event.initEvent('change', true, true); + } + select.dispatchEvent(event); + + return true; +}; const waitFor = (func: () => any) => { const start = Date.now(); @@ -77,52 +94,87 @@ const iterateSelect = async (select: HTMLSelectElement | null, afterEach?: () => const iterateOptions = [...selectOptions, ...selectOptionsReversed]; for (let i = 0; i < iterateOptions.length; i++) { const option = iterateOptions[i]; - const currValue = select.value; - if (option === currValue) { - continue; - } - select.value = option; const currIterations = iterations; + const currOffsetSize = offsetSize(targetElm as HTMLElement); + if (selectOption(select, option)) { + const newOffsetSize = offsetSize(targetElm as HTMLElement); + const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h; - let event; - if (typeof Event === 'function') { - event = new Event('change'); - } else { - event = document.createEvent('Event'); - event.initEvent('change', true, true); - } - select.dispatchEvent(event); + if (hasDimensions(targetElm as HTMLElement) && offsetSizeChanged) { + // eslint-disable-next-line + await waitFor(() => iterations === currIterations + 1); + } - // eslint-disable-next-line - await waitFor(() => iterations === currIterations + 1); - - if (typeof afterEach === 'function') { - // eslint-disable-next-line - await afterEach(); + if (typeof afterEach === 'function') { + // eslint-disable-next-line + await afterEach(); + } } } } }; -window.iteratePadding = async (afterEach?: () => any) => { +heightSelect?.addEventListener('change', selectCallback); +widthSelect?.addEventListener('change', selectCallback); +paddingSelect?.addEventListener('change', selectCallback); +borderSelect?.addEventListener('change', selectCallback); +boxSizingSelect?.addEventListener('change', selectCallback); +displaySelect?.addEventListener('change', selectCallback); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: heightSelect }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: widthSelect }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: paddingSelect }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: borderSelect }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: boxSizingSelect }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +selectCallback({ target: displaySelect }); + +const iteratePadding = (window.iteratePadding = async (afterEach?: () => any) => { await iterateSelect(paddingSelect, afterEach); -}; -window.iterateHeight = async (afterEach?: () => any) => { +}); +const iterateBorder = (window.iterateBorder = async (afterEach?: () => any) => { + await iterateSelect(borderSelect, afterEach); +}); +const iterateHeight = (window.iterateHeight = async (afterEach?: () => any) => { await iterateSelect(heightSelect, afterEach); -}; -window.iterateWidth = async (afterEach?: () => any) => { +}); +const iterateWidth = (window.iterateWidth = async (afterEach?: () => any) => { await iterateSelect(widthSelect, afterEach); -}; +}); +const iterateBoxSizing = (window.iterateBoxSizing = async (afterEach?: () => any) => { + await iterateSelect(boxSizingSelect, afterEach); +}); +const iterateDisplay = (window.iterateDisplay = async (afterEach?: () => any) => { + await iterateSelect(displaySelect, afterEach); +}); const start = (window.iterate = async () => { window.setTestResult(null); targetElm?.removeAttribute('style'); - await iterateHeight(async () => { - await iterateWidth(async () => { - await iteratePadding(); + await iterateDisplay(); + await iterateBoxSizing(async () => { + await iterateHeight(async () => { + await iterateWidth(async () => { + await iterateBorder(async () => { + await iteratePadding(); + }); + }); }); }); window.setTestResult(true); }); startBtn?.addEventListener('click', start); + +targetElm?.appendChild(observerElm);