Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@
"@changesets/cli": "^2.26.1",
"@playwright/test": "^1.33.0",
"@types/jest": "^29.5.1",
"@types/node": "^20.0.0",
"@types/react": "^18.2.5",
"@types/node": "^20.1.3",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.0.0",
"cross-env": "^7.0.3",
"cypress": "^12.11.0",
"cypress": "^12.12.0",
"eslint-config-dflex": "workspace:*",
"istanbul-lib-instrument": "^5.2.1",
"jest": "^29.5.0",
Expand All @@ -66,10 +66,5 @@
"typescript": "^5.0.4",
"vite": "^4.3.5",
"vite-plugin-replace": "^0.1.1"
},
"size-limit": [
{
"path": "packages/dflex-core-instance/dist/dflex-core.js"
}
]
}
}
218 changes: 129 additions & 89 deletions packages/dflex-core-instance/src/Container/DFlexScrollContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
getParentElm,
PointBool,
Threshold,
getCachedComputedStyle,
getDimensionTypeByAxis,
eventDebounce,
BoxNum,
getElmPos,
getElmOverflow,
BoxBool,
} from "@dflex/utils";

import type { ThresholdPercentages, AbstractBox } from "@dflex/utils";
Expand All @@ -31,23 +34,20 @@ export type DFlexSerializedScroll = {

const OVERFLOW_REGEX = /(auto|scroll|overlay)/;

function isStaticallyPositioned(DOM: Element): boolean {
const computedStyle = getCachedComputedStyle(DOM);
const position = computedStyle.getPropertyValue("position");
return position === "static";
}

function getScrollContainer(baseDOMElm: HTMLElement): [HTMLElement, boolean] {
let hasDocumentAsContainer = false;

const { position: baseELmPosition } = getCachedComputedStyle(baseDOMElm);
const baseELmPosition = getElmPos(baseDOMElm);

const excludeStaticParents = baseELmPosition === "absolute";

const scrollContainerDOM = getParentElm(baseDOMElm, (parentDOM) => {
const { overflowX, overflowY } = getCachedComputedStyle(parentDOM);
const overflowX = getElmOverflow(baseDOMElm, "overflow-x");
const overflowY = getElmOverflow(baseDOMElm, "overflow-y");

const parentRect = parentDOM.getBoundingClientRect();

if (excludeStaticParents && isStaticallyPositioned(parentDOM)) {
if (excludeStaticParents && getElmPos(parentDOM) === "static") {
return false;
}

Expand Down Expand Up @@ -100,14 +100,23 @@ const OUTER_THRESHOLD: ThresholdPercentages = {
vertical: 25,
};

class DFlexScrollContainer {
private _innerThresholdInViewport: Threshold | null;

private _outerThresholdInViewport: Threshold | null;

private _threshold_inner_key: string;
// Note: (maybe TODO) It can be customized by the user later.
const INNER_THRESHOLD: ThresholdPercentages = {
horizontal: 10,
vertical: 10,
};

private _threshold_outer_key: string;
class DFlexScrollContainer {
private _thresholdInViewport: {
inner: {
threshold: Threshold | null;
key: string;
};
outer: {
threshold: Threshold | null;
key: string;
};
};

private _SK: string;

Expand Down Expand Up @@ -152,16 +161,24 @@ class DFlexScrollContainer {
scrollEventCallback: ScrollEventCallback
) {
this._SK = SK;
this._threshold_inner_key = `scroll_inner_${SK}`;
this._threshold_outer_key = `scroll_outer_${SK}`;

this._thresholdInViewport = {
inner: {
threshold: null,
key: `scroll_inner_${SK}`,
},
outer: {
threshold: null,
key: `scroll_outer_${SK}`,
},
};

this._listenerDatasetKey = `dflexScrollListener_${SK}`;

this.hasOverflow = new PointBool(false, false);
this.totalScrollRect = new BoxRect(0, 0, 0, 0);
this.visibleScrollRect = new BoxRect(0, 0, 0, 0);
this.allowDynamicVisibility = false;
this._innerThresholdInViewport = null;
this._outerThresholdInViewport = null;
this._scrollEventCallback = null;

const [containerDOM, isDocumentContainer] = getScrollContainer(firstELmDOM);
Expand All @@ -181,19 +198,33 @@ class DFlexScrollContainer {
this.allowDynamicVisibility
) {
this._scrollEventCallback = scrollEventCallback;
this._outerThresholdInViewport = new Threshold(OUTER_THRESHOLD);
this._outerThresholdInViewport.setMainThreshold(
this._threshold_outer_key,
this.visibleScrollRect,
false
);

this._initializeThreshold("outer", OUTER_THRESHOLD);
}

this._attachResizeAndScrollListeners();

if (this.totalScrollRect.top > 0 || this.totalScrollRect.left > 0) {
this._updateScrollPosition(0, 0, true);
}

if (__DEV__) {
Object.seal(this);
}
}

private _initializeThreshold(
type: "inner" | "outer",
thresholdValue: ThresholdPercentages
): void {
const threshold = new Threshold(thresholdValue);
const instance = this._thresholdInViewport[type];
instance.threshold = threshold;
threshold.setMainThreshold(
instance.key,
this.visibleScrollRect,
type === "inner"
);
}

private _updateOverflowStatus(): void {
Expand Down Expand Up @@ -252,8 +283,8 @@ class DFlexScrollContainer {
if (this._isDocumentContainer) {
// For document container, the visible area is the entire client viewport
this.visibleScrollRect.setByPointAndDimensions(
0,
0,
scrollTop,
scrollLeft,
clientHeight,
clientWidth
);
Expand Down Expand Up @@ -308,27 +339,23 @@ class DFlexScrollContainer {
* @returns
*/
hasScrollableArea(axis: Axis, direction: Direction): boolean {
if (!this.hasOverflow[axis]) {
return false;
if (__DEV__) {
if (!this.hasOverflow[axis]) {
throw new Error(
`Cannot call hasScrollableArea when there is no overflow in the ${axis} direction.`
);
}
}

const scrollRect =
axis === "x" ? this.totalScrollRect.width : this.totalScrollRect.height;

const visibleRect =
axis === "x"
? this.visibleScrollRect.width
: this.visibleScrollRect.height;
const scrollRect = this.totalScrollRect[getDimensionTypeByAxis(axis)];
const visibleRect = this.visibleScrollRect[getDimensionTypeByAxis(axis)];

if (direction === 1) {
return scrollRect - visibleRect > 0;
}

if (direction === -1) {
return visibleRect > 0;
}

return false;
// direction === -1;
return visibleRect > 0;
}

private _updateDOMDataset(
Expand Down Expand Up @@ -358,7 +385,19 @@ class DFlexScrollContainer {
}
});

private _throttledResizeHandler = eventDebounce(this._updateScrollRect);
private _throttledResizeHandler = eventDebounce(() => {
this._updateScrollRect();
this._updateOverflowStatus();

// If it's not initialized yet. Leave it as it is.
if (this._thresholdInViewport.outer) {
this._initializeThreshold("outer", OUTER_THRESHOLD);

if (this._thresholdInViewport.inner) {
this._initializeThreshold("inner", INNER_THRESHOLD);
}
}
});

private _attachResizeAndScrollListeners(isAttachListener = true): void {
/**
Expand Down Expand Up @@ -403,9 +442,9 @@ class DFlexScrollContainer {
}

private _clearInnerThreshold(): void {
if (this._innerThresholdInViewport) {
this._innerThresholdInViewport.destroy();
this._innerThresholdInViewport = null;
if (this._thresholdInViewport.inner.threshold) {
this._thresholdInViewport.inner.threshold.destroy();
this._thresholdInViewport.inner.threshold = null;
}
}

Expand All @@ -416,39 +455,23 @@ class DFlexScrollContainer {
* Note: this method is called when dragged is triggered so it gives the user
* more flexibility to choose the threshold in relation to the dragged element.
*
* @param threshold
*/
setInnerThreshold(threshold: ThresholdPercentages) {
this._clearInnerThreshold();

this._innerThresholdInViewport = new Threshold(threshold);

this._innerThresholdInViewport.setMainThreshold(
this._threshold_inner_key,
this.visibleScrollRect,
true
);
}
setInnerThreshold() {
if (__DEV__) {
if (!this._thresholdInViewport.outer.threshold) {
throw new Error(
"setInnerThreshold: Cannot set inner threshold when the outer threshold is not set."
);
}
}

isOutThreshold(
axis: Axis,
direction: Direction,
startingPos: number,
endingPos: number
): boolean {
const adjustToViewport =
axis === "y" ? this.totalScrollRect.top : this.totalScrollRect.left;
// If it's already exist. It means one of the siblings has been triggered
// previously and there's not need to initialize it again.
if (this._thresholdInViewport.inner.threshold) {
return;
}

return (
this.hasOverflow[axis] &&
this._innerThresholdInViewport!.isOutThresholdByDirection(
axis,
direction,
this._threshold_inner_key,
startingPos - adjustToViewport,
endingPos - adjustToViewport
)
);
this._initializeThreshold("inner", INNER_THRESHOLD);
}

/**
Expand Down Expand Up @@ -483,27 +506,44 @@ class DFlexScrollContainer {
return [viewportTop, viewportLeft];
}

isElementInViewport(
isElmOutViewport(
topPos: number,
leftPos: number,
height: number,
width: number
): boolean {
width: number,
isInner: boolean
): [boolean, BoxBool] {
const instance = isInner
? this._thresholdInViewport.inner
: this._thresholdInViewport.outer;

if (__DEV__) {
if (!instance) {
throw new Error(
"_thresholdInViewport is not initialized. Please call setInnerThreshold() method before using isElmOutViewport."
);
}
}

const [viewportTop, viewportLeft] = this.getElmViewportPosition(
topPos,
leftPos
);

const isOutThreshold =
this._outerThresholdInViewport!.isShallowOutThreshold(
this._threshold_outer_key,
viewportTop,
viewportLeft + width,
viewportTop + height,
viewportLeft
);
const top = viewportTop;
const right = viewportLeft + width;
const bottom = viewportTop + height;
const left = viewportLeft;

const targetBox = new BoxNum(top, right, bottom, left);

const { threshold, key } = instance;

const isOutThreshold = threshold!.isOutThreshold(key, targetBox, isInner);

const preservedBoxResult = threshold!.isOut[key];

return !isOutThreshold;
return [isOutThreshold, preservedBoxResult];
}

private _getVisibleScreen(): Dimensions {
Expand Down
Loading