fix flexboxglue

This commit is contained in:
Rene Haas
2021-03-30 19:35:09 +02:00
parent 598454b66e
commit fd09d377eb
9 changed files with 198 additions and 134 deletions
@@ -14,6 +14,7 @@ import {
clientSize,
offsetSize,
getBoundingClientRect,
topRightBottomLeft,
} from 'support';
import { LifecycleHub, Lifecycle } from 'lifecycles/lifecycleHub';
import { getEnvironment } from 'environment';
@@ -32,6 +33,17 @@ interface OverflowAmountCacheContext {
_viewportSize: WH<number>;
}
interface ViewportOverflowState {
_scrollbarsHideOffset: XY<number>;
_overflowScroll: XY<boolean>;
_overlaidHideOffset: number;
}
interface OverflowOption {
x: OverflowBehavior;
y: OverflowBehavior;
}
export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle => {
const { _structureSetup, _getPaddingStyle } = lifecycleHub;
const { _host, _padding, _viewport, _content, _contentArrange } = _structureSetup._targetObj;
@@ -52,7 +64,7 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
x: Math.max(0, ctx._contentScrollSize.w - ctx._viewportSize.w),
y: Math.max(0, ctx._contentScrollSize.h - ctx._viewportSize.h),
}),
{ _equal: equalXY }
{ _equal: equalXY, _initialValue: { x: 0, y: 0 } }
);
const fixScrollSizeRounding = (contentScrollSize: WH<number>, viewportSize: WH<number>, viewportScrollSize: WH<number>): WH<number> => {
@@ -72,76 +84,7 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
return contentScrollSize;
};
const setViewportOverflowStyle = (horizontal: boolean, amount: number, behavior: OverflowBehavior, styleObj: StyleObject) => {
const overflowKey = horizontal ? 'overflowX' : 'overflowY';
const behaviorIsScroll = behavior === 'scroll';
const behaviorIsVisibleScroll = behavior === 'visible-scroll';
const hideOverflow = behaviorIsScroll || behavior === 'hidden';
const applyStyle = amount > 0 && hideOverflow;
if (applyStyle) {
styleObj[overflowKey] = behavior;
}
return {
_visible: !applyStyle,
_behavior: behaviorIsVisibleScroll ? 'scroll' : 'hidden',
};
};
const hideNativeScrollbars = (
contentScrollSize: WH<number>,
showNativeOverlaidScrollbars: boolean,
directionIsRTL: boolean,
viewportStyleObj: StyleObject,
contentStyleObj: StyleObject
) => {
const { _nativeScrollbarSize, _nativeScrollbarIsOverlaid, _nativeScrollbarStyling } = getEnvironment();
const { x: overlaidX, y: overlaidY } = _nativeScrollbarIsOverlaid;
const paddingStyle = _getPaddingStyle();
const scrollX = viewportStyleObj.overflowX === 'scroll';
const scrollY = viewportStyleObj.overflowY === 'scroll';
const horizontalMarginKey = directionIsRTL ? 'marginLeft' : 'marginRight';
const horizontalBorderKey = directionIsRTL ? 'borderLeft' : 'borderRight';
const horizontalPaddingValue = paddingStyle[horizontalMarginKey] as number;
const overlaidHideOffset = _content && !_nativeScrollbarStyling && !showNativeOverlaidScrollbars ? overlaidScrollbarsHideOffset : 0;
const scrollbarsHideOffset = {
x: scrollX && !_nativeScrollbarStyling ? (overlaidX ? overlaidHideOffset : _nativeScrollbarSize.x) : 0,
y: scrollY && !_nativeScrollbarStyling ? (overlaidY ? overlaidHideOffset : _nativeScrollbarSize.y) : 0,
};
// vertical
viewportStyleObj.marginBottom = -scrollbarsHideOffset.x + (paddingStyle.marginBottom as number);
contentStyleObj.borderBottom = scrollX && overlaidX && overlaidHideOffset ? overlaidScrollbarsHideBorderStyle : '';
// horizontal
viewportStyleObj.maxWidth = `calc(100% + ${scrollbarsHideOffset.y + horizontalPaddingValue * -1}px)`;
viewportStyleObj[horizontalMarginKey] = -scrollbarsHideOffset.y + horizontalPaddingValue;
contentStyleObj[horizontalBorderKey] = scrollY && overlaidY && overlaidHideOffset ? overlaidScrollbarsHideBorderStyle : '';
// adjust content arrange (content arrange doesn't exist if its not needed)
style(_contentArrange, {
width: scrollY && !showNativeOverlaidScrollbars ? overlaidHideOffset + contentScrollSize.w : '',
height: scrollX && !showNativeOverlaidScrollbars ? overlaidHideOffset + contentScrollSize.h : '',
});
// hide overflowing scrollbars if there are any
if (!_nativeScrollbarStyling) {
style(_padding, {
overflow: scrollX || scrollY ? 'hidden' : 'visible',
});
}
return {
_scrollbarsHideOffset: scrollbarsHideOffset,
_scroll: {
x: scrollX,
y: scrollY,
},
};
};
const setFlexboxGlueStyle = (heightIntrinsic: boolean, scrollX: boolean, scrollbarsHideOffsetX: number) => {
const fixFlexboxGlue = (viewportOverflowState: ViewportOverflowState, heightIntrinsic: boolean) => {
const offsetLeft = scrollLeft(_viewport);
const offsetTop = scrollTop(_viewport);
@@ -150,8 +93,12 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
});
if (heightIntrinsic) {
const { _overflowScroll, _scrollbarsHideOffset } = viewportOverflowState;
const hostBCR = getBoundingClientRect(_host);
const border = topRightBottomLeft(_host, 'border', 'width');
style(_viewport, {
maxHeight: _host.clientHeight + (scrollX ? scrollbarsHideOffsetX : 0),
maxHeight: hostBCR.height - (border.t + border.b) + (_overflowScroll.x ? _scrollbarsHideOffset.x : 0),
});
}
@@ -159,13 +106,113 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
scrollTop(_viewport, offsetTop);
};
const getViewportOverflowState = (showNativeOverlaidScrollbars: boolean, viewportStyleObj?: StyleObject): ViewportOverflowState => {
const { _nativeScrollbarSize, _nativeScrollbarIsOverlaid, _nativeScrollbarStyling } = getEnvironment();
const { x: overlaidX, y: overlaidY } = _nativeScrollbarIsOverlaid;
const determineOverflow = !viewportStyleObj;
const overlaidHideOffset = _content && !_nativeScrollbarStyling && !showNativeOverlaidScrollbars ? overlaidScrollbarsHideOffset : 0;
const scroll = {
x: (determineOverflow ? style(_viewport, 'overflow-x') : viewportStyleObj!.overflowX) === 'scroll',
y: (determineOverflow ? style(_viewport, 'overflow-y') : viewportStyleObj!.overflowY) === 'scroll',
};
const scrollbarsHideOffset = {
x: scroll.x && !_nativeScrollbarStyling ? (overlaidX ? overlaidHideOffset : _nativeScrollbarSize.x) : 0,
y: scroll.y && !_nativeScrollbarStyling ? (overlaidY ? overlaidHideOffset : _nativeScrollbarSize.y) : 0,
};
return {
_overflowScroll: scroll,
_scrollbarsHideOffset: scrollbarsHideOffset,
_overlaidHideOffset: overlaidHideOffset,
};
};
const setViewportOverflowState = (
showNativeOverlaidScrollbars: boolean,
overflowAmount: XY<number>,
overflow: OverflowOption,
viewportStyleObj: StyleObject
): ViewportOverflowState => {
const setPartialStylePerAxis = (horizontal: boolean, overflowAmount: number, behavior: OverflowBehavior, styleObj: StyleObject) => {
const overflowKey = horizontal ? 'overflowX' : 'overflowY';
const behaviorIsScroll = behavior === 'scroll';
const behaviorIsVisibleScroll = behavior === 'visible-scroll';
const hideOverflow = behaviorIsScroll || behavior === 'hidden';
const applyStyle = overflowAmount > 0 && hideOverflow;
if (applyStyle) {
styleObj[overflowKey] = behavior;
}
return {
_visible: !applyStyle,
_behavior: behaviorIsVisibleScroll ? 'scroll' : 'hidden',
};
};
const { _visible: xVisible, _behavior: xVisibleBehavior } = setPartialStylePerAxis(true, overflowAmount!.x, overflow.x, viewportStyleObj);
const { _visible: yVisible, _behavior: yVisibleBehavior } = setPartialStylePerAxis(false, overflowAmount!.y, overflow.y, viewportStyleObj);
if (xVisible && !yVisible) {
viewportStyleObj.overflowX = xVisibleBehavior;
}
if (yVisible && !xVisible) {
viewportStyleObj.overflowY = yVisibleBehavior;
}
return getViewportOverflowState(showNativeOverlaidScrollbars, viewportStyleObj);
};
const setContentArrange = (viewportOverflowState: ViewportOverflowState, contentScrollSize: WH<number>, showNativeOverlaidScrollbars: boolean) => {
const { _overflowScroll, _overlaidHideOffset } = viewportOverflowState;
// adjust content arrange (content arrange doesn't exist if its not needed)
style(_contentArrange, {
width: _overflowScroll.y && !showNativeOverlaidScrollbars ? _overlaidHideOffset + contentScrollSize.w : '',
height: _overflowScroll.x && !showNativeOverlaidScrollbars ? _overlaidHideOffset + contentScrollSize.h : '',
});
};
const hideNativeScrollbars = (
viewportOverflowState: ViewportOverflowState,
directionIsRTL: boolean,
viewportStyleObj: StyleObject,
contentStyleObj: StyleObject
) => {
const { _nativeScrollbarIsOverlaid, _nativeScrollbarStyling } = getEnvironment();
const { _overflowScroll, _scrollbarsHideOffset, _overlaidHideOffset } = viewportOverflowState;
const { x: scrollX, y: scrollY } = _overflowScroll;
const { x: overlaidX, y: overlaidY } = _nativeScrollbarIsOverlaid;
const paddingStyle = _getPaddingStyle();
const horizontalMarginKey = directionIsRTL ? 'marginLeft' : 'marginRight';
const horizontalBorderKey = directionIsRTL ? 'borderLeft' : 'borderRight';
const horizontalPaddingValue = paddingStyle[horizontalMarginKey] as number;
// vertical
viewportStyleObj.marginBottom = -_scrollbarsHideOffset.x + (paddingStyle.marginBottom as number);
contentStyleObj.borderBottom = scrollX && overlaidX && _overlaidHideOffset ? overlaidScrollbarsHideBorderStyle : '';
// horizontal
viewportStyleObj.maxWidth = `calc(100% + ${_scrollbarsHideOffset.y + horizontalPaddingValue * -1}px)`;
viewportStyleObj[horizontalMarginKey] = -_scrollbarsHideOffset.y + horizontalPaddingValue;
contentStyleObj[horizontalBorderKey] = scrollY && overlaidY && _overlaidHideOffset ? overlaidScrollbarsHideBorderStyle : '';
// hide overflowing scrollbars if there are any
if (!_nativeScrollbarStyling) {
style(_padding, {
overflow: scrollX || scrollY ? 'hidden' : 'visible',
});
}
};
return (updateHints, checkOption, force) => {
const { _directionIsRTL, _heightIntrinsic, _sizeChanged, _hostMutation, _contentMutation, _paddingStyleChanged } = updateHints;
const { _flexboxGlue, _nativeScrollbarStyling, _nativeScrollbarIsOverlaid } = getEnvironment();
const { _value: heightIntrinsic, _changed: heightIntrinsicChanged } = _heightIntrinsic;
const { _value: showNativeOverlaidScrollbarsOption, _changed: showNativeOverlaidScrollbarsChanged } = checkOption<boolean>(
'nativeScrollbarsOverlaid.show'
);
const adjustFlexboxGlue = !_flexboxGlue && (_sizeChanged || _contentMutation || _hostMutation || showNativeOverlaidScrollbarsChanged);
const adjustFlexboxGlue =
!_flexboxGlue && (_sizeChanged || _contentMutation || _hostMutation || showNativeOverlaidScrollbarsChanged || heightIntrinsicChanged);
const showNativeOverlaidScrollbars = showNativeOverlaidScrollbarsOption && _nativeScrollbarIsOverlaid.x && _nativeScrollbarIsOverlaid.y;
let overflowAmuntCache: CacheValues<XY<number>> = getCurrentOverflowAmountCache(force);
let contentScrollSizeCache: CacheValues<WH<number>> = getCurrentContentScrollSizeCache(force);
@@ -178,6 +225,10 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
}
}
if (adjustFlexboxGlue) {
fixFlexboxGlue(getViewportOverflowState(showNativeOverlaidScrollbars), !!heightIntrinsic);
}
if (_sizeChanged || _contentMutation) {
const viewportSize = clientSize(_viewport); // needs to be client Size because possible scrollbar offset
const viewportScrollSize = scrollSize(_viewport);
@@ -190,10 +241,11 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
});
const { _value: contentScrollSize } = contentScrollSizeCache;
overflowAmuntCache = updateOverflowAmountCache(force, {
_contentScrollSize: {
w: Math.max(contentScrollSize!.w, viewportScrollSize.w, contentArrangeOffsetSize.w),
h: Math.max(contentScrollSize!.h, viewportScrollSize.h, contentArrangeOffsetSize.h),
w: Math.max(contentScrollSize!.w, contentArrangeOffsetSize.w),
h: Math.max(contentScrollSize!.h, contentArrangeOffsetSize.h),
},
_viewportSize: {
w: viewportSize.w + Math.max(0, contentClientSize.w - contentScrollSize!.w),
@@ -202,13 +254,10 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
});
}
const { _value: overflow, _changed: overflowChanged } = checkOption<OverflowOption>('overflow');
const { _value: directionIsRTL, _changed: directionChanged } = _directionIsRTL;
const { _value: contentScrollSize, _changed: contentScrollSizeChanged } = contentScrollSizeCache;
const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache;
const { _value: overflow, _changed: overflowChanged } = checkOption<{
x: OverflowBehavior;
y: OverflowBehavior;
}>('overflow');
const adjustDirection = directionChanged && !_nativeScrollbarStyling;
if (
@@ -236,26 +285,12 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
borderLeft: '',
};
const { _visible: xVisible, _behavior: xVisibleBehavior } = setViewportOverflowStyle(true, overflowAmount!.x, overflow.x, viewportStyle);
const { _visible: yVisible, _behavior: yVisibleBehavior } = setViewportOverflowStyle(false, overflowAmount!.y, overflow.y, viewportStyle);
if (xVisible && !yVisible) {
viewportStyle.overflowX = xVisibleBehavior;
}
if (yVisible && !xVisible) {
viewportStyle.overflowY = yVisibleBehavior;
}
const { _scrollbarsHideOffset, _scroll } = hideNativeScrollbars(
contentScrollSize!,
showNativeOverlaidScrollbars,
directionIsRTL!,
viewportStyle,
contentStyle
);
const viewportOverflowState = setViewportOverflowState(showNativeOverlaidScrollbars, overflowAmount!, overflow, viewportStyle);
hideNativeScrollbars(viewportOverflowState, directionIsRTL!, viewportStyle, contentStyle);
setContentArrange(viewportOverflowState, contentScrollSize!, showNativeOverlaidScrollbars);
if (adjustFlexboxGlue) {
setFlexboxGlueStyle(!!_heightIntrinsic._value, _scroll.x, _scrollbarsHideOffset.x);
fixFlexboxGlue(viewportOverflowState, !!heightIntrinsic);
}
// TODO: enlargen viewport if div too small for firefox scrollbar hiding behavior
@@ -1,4 +1,4 @@
import { createCache, topRightBottomLeft, TRBL, equalTRBL, style } from 'support';
import { createCache, topRightBottomLeft, equalTRBL, style } from 'support';
import { LifecycleHub, Lifecycle } from 'lifecycles/lifecycleHub';
import { StyleObject } from 'typings';
import { getEnvironment } from 'environment';
@@ -20,10 +20,11 @@ export const createPaddingLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =>
return (updateHints, checkOption, force) => {
let { _value: padding, _changed: paddingChanged } = currentPaddingCache(force);
const { _nativeScrollbarStyling } = getEnvironment();
const { _sizeChanged, _directionIsRTL } = updateHints;
const { _nativeScrollbarStyling, _flexboxGlue } = getEnvironment();
const { _sizeChanged, _directionIsRTL, _heightIntrinsic } = updateHints;
const { _value: directionIsRTL, _changed: directionRTLChanged } = _directionIsRTL;
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
const { _value: heightIntrinsic } = _heightIntrinsic;
if (_sizeChanged || paddingChanged) {
({ _value: padding, _changed: paddingChanged } = updatePaddingCache(force));
@@ -60,17 +60,15 @@ export const createTrinsicObserver = (
intersectionObserverInstance.observe(trinsicObserver);
push(offListeners, () => intersectionObserverInstance.disconnect());
} else {
push(
offListeners,
createSizeObserver(trinsicObserver, () => {
const newSize = offsetSize(trinsicObserver);
const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(heightIntrinsicCache);
}
})._destroy
);
const onSizeChanged = () => {
const newSize = offsetSize(trinsicObserver);
const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(heightIntrinsicCache);
}
};
push(offListeners, createSizeObserver(trinsicObserver, onSizeChanged)._destroy);
onSizeChanged();
}
prependChildren(target, trinsicObserver);
@@ -12,7 +12,7 @@
position: relative;
flex: auto;
height: auto;
width: auto;
width: 100%;
padding: 0;
margin: 0;
border: none;
@@ -92,12 +92,13 @@ export const show = (elm: HTMLElement | null | undefined): void => {
* @param elm
* @param property
*/
export const topRightBottomLeft = (elm: HTMLElement | null | undefined, property?: string): TRBL => {
const finalProp = property || '';
const top = `${finalProp}-top`;
const right = `${finalProp}-right`;
const bottom = `${finalProp}-bottom`;
const left = `${finalProp}-left`;
export const topRightBottomLeft = (elm: HTMLElement | null | undefined, propertyPrefix?: string, propertySuffix?: string): TRBL => {
const finalPrefix = propertyPrefix || '';
const finalSuffix = propertySuffix || '';
const top = `${finalPrefix}-top-${finalSuffix}`;
const right = `${finalPrefix}-right-${finalSuffix}`;
const bottom = `${finalPrefix}-bottom-${finalSuffix}`;
const left = `${finalPrefix}-left-${finalSuffix}`;
const result = style(elm, [top, right, bottom, left]);
return {
t: parseToZeroOrNumber(result[top]),
@@ -46,7 +46,7 @@
<div>
<div id="target">
<div id="resize">Resize</div>
<div id="hundred">100%</div>
<div id="percent">50%</div>
<div id="end">End</div>
</div>
</div>
@@ -44,8 +44,8 @@ body {
padding: 10px;
}
#hundred {
height: 100%;
#percent {
height: 50%;
background: purple;
border: 1px solid black;
padding: 10px;
@@ -93,20 +93,40 @@ describe('dom style', () => {
});
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);
describe('without prefix and suffix', () => {
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);
});
});
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);
describe('with prefix and suffix', () => {
test('normal', () => {
const result = topRightBottomLeft(document.body, 'border', 'width');
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, 'border', 'width');
expect(result.t).toBe(0);
expect(result.r).toBe(0);
expect(result.b).toBe(0);
expect(result.l).toBe(0);
});
});
});
});
@@ -1,4 +1,4 @@
import { equal, equalTRBL, equalWH, equalXY } from 'support/utils/equal';
import { equal, equalTRBL, equalWH, equalXY, equalBCRWH } from 'support/utils/equal';
describe('equal', () => {
test('equal', () => {
@@ -33,4 +33,13 @@ describe('equal', () => {
expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 0 })).toBe(true);
expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 1 })).toBe(false);
});
test('equalBCRWH', () => {
const bodyBCR = document.body.getBoundingClientRect();
expect(equalBCRWH(bodyBCR, bodyBCR)).toBe(true);
expect(equalBCRWH(bodyBCR, { ...bodyBCR, height: 5 })).toBe(false);
expect(equalBCRWH({ ...bodyBCR, height: 4.1 }, { ...bodyBCR, height: 4.12 })).toBe(false);
expect(equalBCRWH({ ...bodyBCR, height: 4.1 }, { ...bodyBCR, height: 4.12 }, true)).toBe(true);
});
});