From a109bdc6b18e79b25869b9b7f8e40f08fb7831dc Mon Sep 17 00:00:00 2001 From: Rene Date: Sat, 8 May 2021 02:14:54 +0200 Subject: [PATCH] improve overflow lifecycle for fractional device pixel ratios --- .../src/lifecycles/overflowLifecycle.ts | 136 +++++++++--------- .../structureLifecycle/index.browser.ts | 80 ++++++----- .../lifecycles/structureLifecycle/index.scss | 8 +- 3 files changed, 116 insertions(+), 108 deletions(-) diff --git a/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts index 6e8be66..c1a7274 100644 --- a/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts @@ -22,15 +22,10 @@ import { OverflowBehavior } from 'options'; import { StyleObject } from 'typings'; import { classNameViewportArrange, classNameViewportScrollbarStyling } from 'classnames'; -interface ContentScrollSizeCacheContext { - _viewportRect: DOMRect; - _viewportOffsetSize: WH; - _viewportScrollSize: WH; -} - interface OverflowAmountCacheContext { - _contentScrollSize: WH; - _viewportSize: WH; + _viewportScrollSize: WH; + _viewportClientSize: WH; + _viewportSizeFraction: WH; } interface ViewportOverflowState { @@ -49,7 +44,20 @@ interface OverflowOption { y: OverflowBehavior; } +const { max, abs, round } = Math; const overlaidScrollbarsHideOffset = 42; +const whCacheOptions = { + _equal: equalWH, + _initialValue: { w: 0, h: 0 }, +}; +const sizeFraction = (elm: HTMLElement): WH => { + const viewportOffsetSize = offsetSize(elm); + const viewportRect = getBoundingClientRect(elm); + return { + w: viewportRect.width - viewportOffsetSize.w, + h: viewportRect.height - viewportOffsetSize.h, + }; +}; /** * Lifecycle with the responsibility to set the correct overflow and scrollbar hiding styles of the viewport element. @@ -59,41 +67,21 @@ const overlaidScrollbarsHideOffset = 42; export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle => { const { _structureSetup, _doViewportArrange, _getLifecycleCommunication, _setLifecycleCommunication } = lifecycleHub; const { _host, _viewport, _viewportArrange } = _structureSetup._targetObj; - const { _update: updateContentScrollSizeCache, _current: getCurrentContentScrollSizeCache } = createCache< - WH, - ContentScrollSizeCacheContext - >((ctx) => fixScrollSizeRounding(ctx._viewportScrollSize, ctx._viewportOffsetSize, ctx._viewportRect), { _equal: equalWH }); - const { _update: updateOverflowAmountCache, _current: getCurrentOverflowAmountCache } = createCache, OverflowAmountCacheContext>( - (ctx) => { - // @ts-ignore - //const { scrollLeftMax, scrollTopMax } = _viewport; - //const multiplicatorW = (isNumber(scrollLeftMax) ? scrollLeftMax !== 0 : true) ? 1 : 0; - //const multiplicatorH = (isNumber(scrollTopMax) ? scrollTopMax !== 0 : true) ? 1 : 0; - - return { - w: Math.round(Math.max(0, ctx._contentScrollSize.w - ctx._viewportSize.w)), - h: Math.round(Math.max(0, ctx._contentScrollSize.h - ctx._viewportSize.h)), - }; - }, - { _equal: equalWH, _initialValue: { w: 0, h: 0 } } + const { _update: updateViewportSizeFraction, _current: getCurrentViewportSizeFraction } = createCache>( + () => sizeFraction(_viewport), + whCacheOptions + ); + const { _update: updateViewportScrollSizeCache, _current: getCurrentViewportScrollSizeCache } = createCache>( + () => scrollSize(_viewport), + whCacheOptions + ); + const { _update: updateOverflowAmountCache, _current: getCurrentOverflowAmountCache } = createCache, OverflowAmountCacheContext>( + ({ _viewportScrollSize, _viewportClientSize, _viewportSizeFraction }) => ({ + w: round(max(0, _viewportScrollSize.w - _viewportClientSize.w) - max(0, _viewportSizeFraction.w)), + h: round(max(0, _viewportScrollSize.h - _viewportClientSize.h) - max(0, _viewportSizeFraction.h)), + }), + whCacheOptions ); - - /** - * Fixes incorrect roundng of scroll size. - * @param viewportScrollSize The potential incorrect viewport scroll size. - * @param viewportOffsetSize The viewport offset size. - * @param viewportRect The viewport bounding client rect. - * @returns The passed scroll size without rounding errors. - */ - const fixScrollSizeRounding = (viewportScrollSize: WH, viewportOffsetSize: WH, viewportRect: DOMRect): WH => { - const wFix = viewportRect.width - viewportOffsetSize.w; - const hFix = viewportRect.height - viewportOffsetSize.h; - - return { - w: viewportScrollSize.w + (Math.abs(wFix) < 1 ? wFix : 0), - h: viewportScrollSize.h + (Math.abs(hFix) < 1 ? hFix : 0), - }; - }; /** * Applies a fixed height to the viewport so it can't overflow or underflow the host element. @@ -200,11 +188,16 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle = /** * Sets the styles of the viewport arrange element. * @param viewportOverflowState The viewport overflow state according to which the scrollbars shall be hidden. - * @param contentScrollSize The content scroll size. + * @param viewportScrollSize The content scroll size. * @param directionIsRTL Whether the direction is RTL or not. * @returns A boolean which indicates whether the viewport arrange element was adjusted. */ - const arrangeViewport = (viewportOverflowState: ViewportOverflowState, contentScrollSize: WH, directionIsRTL: boolean) => { + const arrangeViewport = ( + viewportOverflowState: ViewportOverflowState, + viewportScrollSize: WH, + viewportSizeFraction: WH, + directionIsRTL: boolean + ) => { if (_doViewportArrange) { const { _scrollbarsHideOffset, _scrollbarsHideOffsetArrange } = viewportOverflowState; const { x: arrangeX, y: arrangeY } = _scrollbarsHideOffsetArrange; @@ -213,9 +206,11 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle = const viewportArrangeHorizontalPaddingKey: keyof StyleObject = directionIsRTL ? 'paddingRight' : 'paddingLeft'; const viewportArrangeHorizontalPaddingValue = viewportPaddingStyle[viewportArrangeHorizontalPaddingKey] as number; const viewportArrangeVerticalPaddingValue = viewportPaddingStyle.paddingTop as number; + const fractionalContentWidth = viewportScrollSize.w + (abs(viewportSizeFraction.w) < 1 ? viewportSizeFraction.w : 0); + const fractionalContenHeight = viewportScrollSize.h + (abs(viewportSizeFraction.h) < 1 ? viewportSizeFraction.h : 0); const arrangeSize = { - w: hideOffsetY && arrangeY ? `${hideOffsetY + contentScrollSize.w - viewportArrangeHorizontalPaddingValue}px` : '', - h: hideOffsetX && arrangeX ? `${hideOffsetX + contentScrollSize.h - viewportArrangeVerticalPaddingValue}px` : '', + w: hideOffsetY && arrangeY ? `${hideOffsetY + fractionalContentWidth - viewportArrangeHorizontalPaddingValue}px` : '', + h: hideOffsetX && arrangeX ? `${hideOffsetX + fractionalContenHeight - viewportArrangeVerticalPaddingValue}px` : '', }; // adjust content arrange / before element @@ -349,8 +344,9 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle = const showNativeOverlaidScrollbars = showNativeOverlaidScrollbarsOption && _nativeScrollbarIsOverlaid.x && _nativeScrollbarIsOverlaid.y; const adjustFlexboxGlue = !_flexboxGlue && (_sizeChanged || _contentMutation || _hostMutation || showNativeOverlaidScrollbarsChanged || heightIntrinsicChanged); + let viewportSizeFractionCache: CacheValues> = getCurrentViewportSizeFraction(force); + let viewportScrollSizeCache: CacheValues> = getCurrentViewportScrollSizeCache(force); let overflowAmuntCache: CacheValues> = getCurrentOverflowAmountCache(force); - let contentScrollSizeCache: CacheValues> = getCurrentContentScrollSizeCache(force); let preMeasureViewportOverflowState: ViewportOverflowState | undefined; if (showNativeOverlaidScrollbarsChanged && _nativeScrollbarStyling) { @@ -372,49 +368,47 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle = directionIsRTL!, preMeasureViewportOverflowState ); - const contentSize = clientSize(_viewport); - const viewportRect = getBoundingClientRect(_viewport); - const viewportOffsetSize = offsetSize(_viewport); - let viewportScrollSize = scrollSize(_viewport); - let viewportClientSize = contentSize; - const { _value: contentScrollSize, _changed: contentScrollSizeChanged } = (contentScrollSizeCache = updateContentScrollSizeCache(force, { - _viewportRect: viewportRect, - _viewportOffsetSize: viewportOffsetSize, - _viewportScrollSize: viewportScrollSize, - })); + const { _value: viewportSizeFraction, _changed: viewportSizeFractionCahnged } = (viewportSizeFractionCache = updateViewportSizeFraction(force)); + const { _value: viewportScrollSize, _changed: viewportScrollSizeChanged } = (viewportScrollSizeCache = updateViewportScrollSizeCache(force)); + const viewportContentSize = clientSize(_viewport); + let arrangedViewportScrollSize = viewportScrollSize!; + let arrangedViewportClientSize = viewportContentSize; _redoViewportArrange(); // if re measure is required (only required if content arrange strategy is used) if ( - (contentScrollSizeChanged || showNativeOverlaidScrollbarsChanged) && + (viewportScrollSizeChanged || viewportSizeFractionCahnged || showNativeOverlaidScrollbarsChanged) && undoViewportArrangeOverflowState && !showNativeOverlaidScrollbars && - arrangeViewport(undoViewportArrangeOverflowState, contentScrollSize!, directionIsRTL!) + arrangeViewport(undoViewportArrangeOverflowState, viewportScrollSize!, viewportSizeFraction!, directionIsRTL!) ) { - viewportClientSize = clientSize(_viewport); - viewportScrollSize = fixScrollSizeRounding(scrollSize(_viewport), offsetSize(_viewport), getBoundingClientRect(_viewport)); + arrangedViewportClientSize = clientSize(_viewport); + arrangedViewportScrollSize = scrollSize(_viewport); } overflowAmuntCache = updateOverflowAmountCache(force, { - _contentScrollSize: { - w: Math.max(contentScrollSize!.w, viewportScrollSize.w), - h: Math.max(contentScrollSize!.h, viewportScrollSize.h), + _viewportSizeFraction: viewportSizeFraction!, + _viewportScrollSize: { + w: max(viewportScrollSize!.w, arrangedViewportScrollSize.w), + h: max(viewportScrollSize!.h, arrangedViewportScrollSize.h), }, - _viewportSize: { - w: viewportClientSize.w + Math.max(0, contentSize.w - contentScrollSize!.w), - h: viewportClientSize.h + Math.max(0, contentSize.h - contentScrollSize!.h), + _viewportClientSize: { + w: arrangedViewportClientSize.w + max(0, viewportContentSize.w - viewportScrollSize!.w), + h: arrangedViewportClientSize.h + max(0, viewportContentSize.h - viewportScrollSize!.h), }, }); } - const { _value: overflow, _changed: overflowChanged } = checkOption('overflow'); - const { _value: contentScrollSize, _changed: contentScrollSizeChanged } = contentScrollSizeCache; + const { _value: viewportSizeFraction, _changed: viewportSizeFractionChanged } = viewportSizeFractionCache; + const { _value: viewportScrollSize, _changed: viewportScrollSizeChanged } = viewportScrollSizeCache; const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache; + const { _value: overflow, _changed: overflowChanged } = checkOption('overflow'); if ( _paddingStyleChanged || - contentScrollSizeChanged || + viewportSizeFractionChanged || + viewportScrollSizeChanged || overflowAmountChanged || overflowChanged || showNativeOverlaidScrollbarsChanged || @@ -432,7 +426,7 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle = }; const viewportOverflowState = setViewportOverflowState(showNativeOverlaidScrollbars, overflowAmount!, overflow, viewportStyle); - const viewportArranged = arrangeViewport(viewportOverflowState, contentScrollSize!, directionIsRTL!); + const viewportArranged = arrangeViewport(viewportOverflowState, viewportScrollSize!, viewportSizeFraction!, directionIsRTL!); hideNativeScrollbars(viewportOverflowState, directionIsRTL!, viewportArranged, viewportStyle); if (adjustFlexboxGlue) { diff --git a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts index 453e65f..db76d23 100644 --- a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts +++ b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts @@ -4,7 +4,7 @@ import './handleEnvironment'; import should from 'should'; import { resize } from '@/testing-browser/Resize'; import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; -import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select'; +import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select'; import { timeout } from '@/testing-browser/timeout'; import { OverlayScrollbars } from 'overlayscrollbars'; import { assignDeep, clientSize, from, getBoundingClientRect, style, parent, addClass, WH, removeAttr } from 'support'; @@ -57,19 +57,10 @@ const getMetrics = (elm: HTMLElement): Metrics => { const hasOverflow = (elm: HTMLElement) => { const measure = scrollMeasure(elm); - - elm.scrollLeft = 9999; - elm.scrollTop = 9999; - - const hasOverflow = { - x: measure.width > 0 && elm.scrollLeft >= 1, - y: measure.height > 0 && elm.scrollTop >= 1, + return { + x: measure.width > 0, + y: measure.height > 0, }; - - elm.scrollLeft = 0; - elm.scrollTop = 0; - - return hasOverflow; }; return { @@ -101,6 +92,8 @@ const metricsDimensionsEqual = (a: Metrics, b: Metrics) => { return JSON.stringify(aDimensions) === JSON.stringify(bDimensions); }; +const isFractionalPixelRatio = () => window.devicePixelRatio % 1 !== 0; + const plusMinusArr = (original: number, plusMinus: number) => { return [original, original + plusMinus, original - plusMinus]; }; @@ -158,9 +151,13 @@ target!.querySelector('.os-viewport')?.addEventListener('scroll', (e) => { comparison!.scrollTop = viewport.scrollTop; }); -resize(target!).addResizeListener((width, height) => style(comparison, { width, height })); +resize(target!).addResizeListener((width, height) => { + style(comparison, { width, height }); +}); //resize(comparison!).addResizeListener((width, height) => style(target, { width, height })); -resize(targetResize!).addResizeListener((width, height) => style(comparisonResize, { width, height })); +resize(targetResize!).addResizeListener((width, height) => { + style(comparisonResize, { width, height }); +}); //resize(comparisonRes!).addResizeListener((width, height) => style(targetRes, { width, height })); const selectCallbackEnv = generateClassChangeSelectCallback(from(envElms)); @@ -219,19 +216,32 @@ const checkMetrics = async (checkComparison: CheckComparisonObj) => { should.equal(targetMetrics.size.width, comparisonMetrics.size.width, 'Size width equality.'); should.equal(targetMetrics.size.height, comparisonMetrics.size.height, 'Size height equality.'); - //should.equal(targetMetrics.scroll.width, comparisonMetrics.scroll.width, 'Scroll width equality.'); - //should.equal(targetMetrics.scroll.height, comparisonMetrics.scroll.height, 'Scroll height equality.'); + if (isFractionalPixelRatio()) { + should.ok(plusMinusArr(targetMetrics.scroll.width, 1).indexOf(comparisonMetrics.scroll.width) > -1, 'Scroll width equality. (+-1)'); + should.ok(plusMinusArr(targetMetrics.scroll.height, 1).indexOf(comparisonMetrics.scroll.height) > -1, 'Scroll height equality. (+-1)'); - //should.equal(osInstance.state()._overflowAmount.w, comparisonMetrics.scroll.width, 'Overflow amount width equality.'); - //should.equal(osInstance.state()._overflowAmount.h, comparisonMetrics.scroll.height, 'Overflow amount height equality.'); + should.ok( + plusMinusArr(osInstance.state()._overflowAmount.w, 1).indexOf(comparisonMetrics.scroll.width) > -1, + 'Overflow amount width equality. (+-1)' + ); + should.ok( + plusMinusArr(osInstance.state()._overflowAmount.h, 1).indexOf(comparisonMetrics.scroll.height) > -1, + 'Overflow amount height equality. (+-1)' + ); + } else { + should.equal(targetMetrics.scroll.width, comparisonMetrics.scroll.width, 'Scroll width equality.'); + should.equal(targetMetrics.scroll.height, comparisonMetrics.scroll.height, 'Scroll height equality.'); - should.equal(targetMetrics.hasOverflow.x, comparisonMetrics.hasOverflow.x, 'Has overflow x equality.'); - should.equal(targetMetrics.hasOverflow.y, comparisonMetrics.hasOverflow.y, 'Has overflow y equality.'); + should.equal(osInstance.state()._overflowAmount.w, comparisonMetrics.scroll.width, 'Overflow amount width equality.'); + should.equal(osInstance.state()._overflowAmount.h, comparisonMetrics.scroll.height, 'Overflow amount height equality.'); + } + + //should.equal(targetMetrics.hasOverflow.x, comparisonMetrics.hasOverflow.x, 'Has overflow x equality.'); + //should.equal(targetMetrics.hasOverflow.y, comparisonMetrics.hasOverflow.y, 'Has overflow y equality.'); if (targetMetrics.hasOverflow.x) { should.equal(style(targetViewport!, 'overflowX'), 'scroll', 'Overflow-X should result in scroll.'); should.ok(osInstance.state()._overflowAmount.w > 0, 'Overflow amount width should be > 0 with overflow.'); - //should.ok(plusMinusArr(targetMetrics.scroll.width, 1).indexOf(comparisonMetrics.scroll.width) > -1, 'Scroll width equality. (+-1)'); } else { should.notEqual(style(targetViewport!, 'overflowX'), 'scroll', 'No Overflow-X shouldnt result in scroll.'); should.equal(osInstance.state()._overflowAmount.w, 0, 'Overflow amount width should be 0 without overflow.'); @@ -240,7 +250,6 @@ const checkMetrics = async (checkComparison: CheckComparisonObj) => { if (targetMetrics.hasOverflow.y) { should.equal(style(targetViewport!, 'overflowY'), 'scroll', 'Overflow-Y should result in scroll.'); should.ok(osInstance.state()._overflowAmount.h > 0, 'Overflow amount height should be > 0 with overflow.'); - //should.ok(plusMinusArr(targetMetrics.scroll.height, 1).indexOf(comparisonMetrics.scroll.height) > -1, 'Scroll height equality. (+-1)'); } else { should.notEqual(style(targetViewport!, 'overflowY'), 'scroll', 'No Overflow-Y shouldnt result in scroll.'); should.equal(osInstance.state()._overflowAmount.h, 0, 'Overflow amount height should be 0 without overflow.'); @@ -307,6 +316,12 @@ const iterateMinMax = async (afterEach?: () => any) => { }; const overflowTest = async () => { + const additiveOverflow = () => { + if (isFractionalPixelRatio()) { + return 1 + Math.max(1, Math.round(window.devicePixelRatio)); + } + return 1; + }; const contentBox = (elm: HTMLElement | null): WH => { if (elm) { const computedStyle = window.getComputedStyle(elm); @@ -335,14 +350,15 @@ const overflowTest = async () => { const { maxWidth, maxHeight } = style(comparison, ['maxWidth', 'maxHeight']); if (maxWidth !== 'none' && maxHeight !== 'none') { + const addOverflow = additiveOverflow(); const before: CheckComparisonObj = { updCount: updateCount, metrics: getMetrics(comparison!), }; const { paddingRight, paddingBottom } = style(comparison, ['paddingRight', 'paddingBottom']); const comparisonContentBox = contentBox(comparison); - const widthOverflow = width ? 1 : 0; - const heightOverflow = height ? 1 : 0; + const widthOverflow = width ? addOverflow : 0; + const heightOverflow = height ? addOverflow : 0; const styleObj = { width: comparisonContentBox.w + widthOverflow, height: comparisonContentBox.h + heightOverflow }; style(comparisonResize, styleObj); @@ -362,15 +378,15 @@ const overflowTest = async () => { style(comparisonResize, styleObj); if (width) { - while (comparison!.scrollWidth - comparison!.clientWidth <= 0) { - styleObj.width += 1; + while (comparison!.scrollWidth - comparison!.clientWidth <= addOverflow - 1) { + styleObj.width += addOverflow; style(comparisonResize, styleObj); } } if (height) { - while (comparison!.scrollHeight - comparison!.clientHeight <= 0) { - styleObj.height += 1; + while (comparison!.scrollHeight - comparison!.clientHeight <= addOverflow - 1) { + styleObj.height += addOverflow; style(comparisonResize, styleObj); } } @@ -381,13 +397,13 @@ const overflowTest = async () => { }; if (width) { - should.ok(overflowAmountCheck.width >= 1, 'Correct smallest possible overflow width.'); + should.ok(overflowAmountCheck.width >= addOverflow, 'Correct smallest possible overflow width.'); } else { should.equal(overflowAmountCheck.width, 0, 'Correct smallest possible overflow width.'); } if (height) { - should.ok(overflowAmountCheck.height >= 1, 'Correct smallest possible overflow height.'); + should.ok(overflowAmountCheck.height >= addOverflow, 'Correct smallest possible overflow height.'); } else { should.equal(overflowAmountCheck.height, 0, 'Correct smallest possible overflow height.'); } @@ -472,5 +488,3 @@ const start = async () => { }; startBtn?.addEventListener('click', start); - -window.getMetrics = getMetrics; diff --git a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss index 2dc77b2..2649609 100644 --- a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss +++ b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss @@ -105,10 +105,10 @@ body { content: ''; position: absolute; display: block; - top: -11px; - right: -11px; - bottom: -11px; - left: -11px; + top: -10px; + right: -10px; + bottom: -10px; + left: -10px; background: green; z-index: -1; opacity: 0.5;