improve size observer code and tests

This commit is contained in:
Rene
2021-04-14 14:09:45 +02:00
parent fbd8008087
commit a3573f87a1
9 changed files with 93 additions and 58 deletions
@@ -12,8 +12,7 @@ import {
prependChildren,
removeElements,
on,
preventDefault,
stopPropagation,
stopAndPrevent,
addClass,
equalWH,
push,
@@ -21,8 +20,6 @@ import {
rAF,
ResizeObserverConstructor,
isArray,
indexOf,
each,
isBoolean,
} from 'support';
import { getEnvironment } from 'environment';
@@ -35,10 +32,6 @@ import {
classNameSizeObserverListenerItemFinal,
} from 'classnames';
interface SizeObserverEntry {
contentRect: DOMRectReadOnly;
}
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
export interface SizeObserver {
@@ -50,21 +43,24 @@ export interface SizeObserver {
};
}
const animationStartEventName = 'animationstart';
const scrollEventName = 'scroll';
const scrollAmount = 3333333;
/*
const directionIsRTLMap = {
direction: ['rtl'],
// 'writing-mode': ['sideways-rl', 'tb', 'tb-rl', 'vertical-rl'],
'writing-mode': ['sideways-rl', 'tb', 'tb-rl', 'vertical-rl'],
};
const directionIsRTL = (elm: HTMLElement): boolean => {
let isRTL = false;
const styles = style(elm, ['direction' /* , 'writing-mode' */]);
const styles = style(elm, ['direction', 'writing-mode']);
each(styles, (value, key) => {
isRTL = isRTL || indexOf(directionIsRTLMap[key], value) > -1;
});
return isRTL;
};
*/
const animationStartEventName = 'animationstart';
const scrollEventName = 'scroll';
const scrollAmount = 3333333;
const directionIsRTL = (elm: HTMLElement): boolean => style(elm, 'direction') === 'rtl';
const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height || rect.width);
export const createSizeObserver = (
@@ -86,7 +82,7 @@ export const createSizeObserver = (
(!domRectHasDimensions(currVal) && domRectHasDimensions(newVal))
),
});
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | SizeObserverEntry[] | Event) => {
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | ResizeObserverEntry[] | Event) => {
const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as CacheValues<boolean>)._value);
let skip = false;
@@ -100,7 +96,7 @@ export const createSizeObserver = (
}
// else if its triggered with DirectionCache
else if (hasDirectionCache) {
doDirectionScroll = (sizeChangedContext as CacheValues<boolean>)._changed; // direction scroll when DirectionCache changed, false toherwise
doDirectionScroll = (sizeChangedContext as CacheValues<boolean>)._changed; // direction scroll when DirectionCache changed, false otherwise
}
if (observeDirectionChange) {
@@ -150,24 +146,22 @@ export const createSizeObserver = (
onSizeChangedCallbackProxy();
}
};
const onScroll = (scrollEvent?: Event) => {
const onScroll = (scrollEvent?: Event | false) => {
currSize = offsetSize(listenerElement);
isDirty = !scrollEvent || !equalWH(currSize, cacheSize);
if (scrollEvent && isDirty && !rAFId) {
cAF!(rAFId);
rAFId = rAF!(onResized);
} else if (!scrollEvent) {
if (scrollEvent) {
stopAndPrevent(scrollEvent);
if (isDirty && !rAFId) {
cAF!(rAFId);
rAFId = rAF!(onResized);
}
} else {
onResized();
}
reset();
if (scrollEvent) {
preventDefault(scrollEvent);
stopPropagation(scrollEvent);
}
return false;
};
push(offListeners, [on(expandElement, scrollEventName, onScroll), on(shrinkElement, scrollEventName, onScroll)]);
@@ -177,8 +171,10 @@ export const createSizeObserver = (
width: scrollAmount,
height: scrollAmount,
});
reset();
appearCallback = observeAppearChange ? () => onScroll() : reset;
appearCallback = observeAppearChange ? onScroll.bind(0, false) : reset;
}
if (observeDirectionChange) {
@@ -198,9 +194,7 @@ export const createSizeObserver = (
onSizeChangedCallbackProxy(directionIsRTLCacheValues);
}
preventDefault(event);
stopPropagation(event);
return false;
stopAndPrevent(event);
})
);
}
@@ -29,8 +29,8 @@ $scrollbar-cushion: 100px;
}
.os-size-observer-appear {
animation-duration: 0.001s;
animation-name: os-size-observer-appear-animation;
// "forwards" is important for older browsers
animation: os-size-observer-appear-animation 1ms forwards;
}
.os-size-observer-listener {
@@ -2,6 +2,6 @@ import { jsAPI } from 'support/compatibility/vendors';
export const MutationObserverConstructor = jsAPI<typeof MutationObserver>('MutationObserver');
export const IntersectionObserverConstructor = jsAPI<typeof IntersectionObserver>('IntersectionObserver');
export const ResizeObserverConstructor: any | undefined = jsAPI('ResizeObserver');
export const ResizeObserverConstructor = jsAPI<typeof ResizeObserver>('ResizeObserver');
export const cAF = jsAPI<typeof cancelAnimationFrame>('cancelAnimationFrame');
export const rAF = jsAPI<typeof requestAnimationFrame>('requestAnimationFrame');
@@ -87,10 +87,16 @@ export const on = <T extends Event = Event>(
* Shorthand for the stopPropagation event Method.
* @param evt The event of which the stopPropagation method shall be called.
*/
export const stopPropagation = (evt: Event) => evt.stopPropagation();
export const stopPropagation = (evt: Event): void => evt.stopPropagation();
/**
* Shorthand for the preventDefault event Method.
* @param evt The event of which the preventDefault method shall be called.
*/
export const preventDefault = (evt: Event) => evt.preventDefault();
export const preventDefault = (evt: Event): void => evt.preventDefault();
/**
* Shorthand for the stopPropagation and preventDefault event Method.
* @param evt The event of which the stopPropagation and preventDefault methods shall be called.
*/
export const stopAndPrevent = (evt: Event): void => (stopPropagation(evt) as undefined) || (preventDefault(evt) as undefined);
@@ -0,0 +1,13 @@
const url = new URL(window.location.toString());
const params = url.searchParams;
const useResizeObserverPolyfill = Boolean(params.get('resizeobserverpolyfill'));
if (useResizeObserverPolyfill) {
// @ts-ignore
window.ResizeObserver = undefined;
} else {
document.getElementById('resizeobserver-polyfill')?.addEventListener('click', () => {
params.set('resizeobserverpolyfill', 'true');
window.location.assign(url.toString());
});
}
@@ -1,7 +1,7 @@
import 'styles/overlayscrollbars.scss';
import './index.scss';
import './handleResizeObserver';
import should from 'should';
// import { generateClassChangeSelectCallback, iterateSelect, setTestResult, waitForOrFailTest, timeout } from '@/testing-browser';
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { timeout } from '@/testing-browser/timeout';
@@ -34,6 +34,23 @@ const directionSelect: HTMLSelectElement | null = document.querySelector('#direc
const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const resizesSlot: HTMLButtonElement | null = document.querySelector('#resizes');
const sizeObserver = createSizeObserver(
targetElm as HTMLElement,
(directionIsRTLCache?: any) => {
if (directionIsRTLCache) {
directionIterations += 1;
} else {
sizeIterations += 1;
}
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = (directionIterations + sizeIterations).toString();
}
});
},
{ _direction: true, _appear: true }
);
const selectCallback = generateClassChangeSelectCallback(targetElm as HTMLElement);
const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any) => {
interface IterateSelect {
@@ -84,7 +101,9 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
should.equal(sizeIterations, currSizeIterations + 1);
}
if (dirChanged) {
const expectedCacheValue = newDir === 'rtl';
should.equal(directionIterations, currDirectionIterations + 1);
should.equal(sizeObserver._getCurrentCacheValues()._directionIsRTL._value, expectedCacheValue);
}
});
}
@@ -133,7 +152,6 @@ const iterateDisplay = async (afterEach?: () => any) => {
const iterateDirection = async (afterEach?: () => any) => {
await iterate(directionSelect, afterEach);
};
const start = async () => {
setTestResult(null);
@@ -156,26 +174,11 @@ const start = async () => {
});
});
sizeObserver._destroy();
should.ok(targetElm?.children.length === 0);
setTestResult(true);
};
startBtn?.addEventListener('click', start);
createSizeObserver(
targetElm as HTMLElement,
(directionIsRTLCache?: any) => {
if (directionIsRTLCache) {
directionIterations += 1;
} else {
sizeIterations += 1;
}
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = (directionIterations + sizeIterations).toString();
}
});
},
{ _direction: true, _appear: true }
);
export { start };
@@ -1,4 +1,5 @@
<div id="controls">
<button id="resizeobserver-polyfill">ResizeObserver polyfill</button><br />
<label for="height">height</label>
<select name="height" id="height">
<option value="heightAuto">auto</option>
@@ -7,7 +7,14 @@ describe('SizeObserver', () => {
await page.goto(url);
});
test('test', async () => {
test('with ResizeOserver', async () => {
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('with ResizeOserver polyfill', async () => {
await page.click('#resizeobserver-polyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
@@ -1,4 +1,4 @@
import { off, preventDefault, stopPropagation, OnOptions } from 'support/dom/events';
import { off, preventDefault, stopPropagation, stopAndPrevent, OnOptions } from 'support/dom/events';
const testElm = document.body;
const mockEventListener = (passive?: boolean, add?: (...args: any) => any, remove?: (...args: any) => any) => {
@@ -80,7 +80,7 @@ describe('dom events', () => {
[true, false].forEach((passiveSupport) => {
describe(`passive event listeners support: ${passiveSupport}`, () => {
['testEventName', 'testEventName testEventName2 testEventName3'].forEach((eventNames) => {
const title = eventNames.split(' ').length === 1 ? 'signle' : 'multiple';
const title = eventNames.split(' ').length === 1 ? 'single' : 'multiple';
test(title, () => {
onOffTest(passiveSupport, eventNames);
onOffTest(passiveSupport, eventNames, { _capture: true });
@@ -113,7 +113,7 @@ describe('dom events', () => {
};
['testEventName', 'testEventName testEventName2 testEventName3'].forEach((eventNames) => {
const title = eventNames.split(' ').length === 1 ? 'signle' : 'multiple';
const title = eventNames.split(' ').length === 1 ? 'single' : 'multiple';
test(title, () => {
offTest(eventNames, false);
offTest(eventNames, true);
@@ -138,4 +138,15 @@ describe('dom events', () => {
stopPropagation(fakeEvent);
expect(fakeEvent.stopPropagation).toHaveBeenCalled();
});
test('stopAndPrevent', () => {
// @ts-ignore
const fakeEvent: Event = {
stopPropagation: jest.fn(),
preventDefault: jest.fn(),
};
stopAndPrevent(fakeEvent);
expect(fakeEvent.stopPropagation).toHaveBeenCalled();
expect(fakeEvent.preventDefault).toHaveBeenCalled();
});
});