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
9 changes: 9 additions & 0 deletions .changeset/shaggy-badgers-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@lynx-js/web-elements": patch
---

fix: improve x-foldview-ng

- support fling for touch event driven scrolling
- allow the height of `x-foldview-slot-ng` + `x-foldview-toolbar-ng` > `x-foldview-ng`
- do not prevent horizontal gesture. After this commit we only allow one direction gesture for one touch (start -> end)
2 changes: 2 additions & 0 deletions .cspell/lynx.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ disexposure
viewpager
scrolltoupper
scrolltolower
foldview
layoutchange
1 change: 0 additions & 1 deletion cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@
"PAPI", // Lynx's Element PAPI
"parseable", // https://pnpm.io/cli/list
"debugids", // https://getsentry.github.io/debugids/
"layoutchange", // the "layoutchange" event of Lynx
],
// Glob
"ignorePaths": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
import type { AttributeReactiveClass } from '@lynx-js/web-elements-reactive';
import type { XFoldviewHeaderNg } from './XFoldviewHeaderNg.js';
import type { XFoldviewNg } from './XFoldviewNg.js';
import { scrollableLength, type XFoldviewNg } from './XFoldviewNg.js';

export class XFoldviewHeaderNgFeatures
implements InstanceType<AttributeReactiveClass<typeof XFoldviewHeaderNg>>
Expand All @@ -28,7 +28,7 @@ export class XFoldviewHeaderNgFeatures
const headerHeight = resize!.contentRect.height;
if (offsetTop < headerHeight) {
slot.style.top = headerHeight - offsetTop + 'px';
parentElement.__scrollableLength = headerHeight - offsetTop;
parentElement[scrollableLength] = headerHeight - offsetTop;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,32 @@ import { LynxExposure } from '../common/Exposure.js';
import { XFoldviewNgEvents } from './XFoldviewNgEvents.js';
import { scrollContainerDom } from '../common/constants.js';

export const scrollableLength = Symbol('scrollableLength');
export const isHeaderShowing = Symbol('isHeaderShowing');

@Component<typeof XFoldviewNg>('x-foldview-ng', [
LynxExposure,
XFoldviewNgEvents,
])
export class XFoldviewNg extends HTMLElement {
static readonly notToFilterFalseAttributes = new Set(['scroll-enable']);
__scrollableLength: number = 0;
get __headershowing() {
[scrollableLength]: number = 0;
get [isHeaderShowing]() {
// This behavior cannot be reproduced in the current test, but can be reproduced in Android WebView
return Math.abs(this.scrollTop - this.__scrollableLength) > 1;
return this[scrollableLength] - this.scrollTop >= 1;
}

override get scrollTop() {
return super.scrollTop;
}

override set scrollTop(value: number) {
if (value > this[scrollableLength]) {
value = this[scrollableLength];
} else if (value < 0) {
value = 0;
}
super.scrollTop = value;
}

setFoldExpanded(params: { offset: string; smooth: boolean }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import {
type AttributeReactiveClass,
registerAttributeHandler,
registerEventEnableStatusChangeHandler,
} from '@lynx-js/web-elements-reactive';
import { commonComponentEventSetting } from '../common/commonEventInitConfiguration.js';
import type { XFoldviewNg } from './XFoldviewNg.js';
import { scrollableLength, type XFoldviewNg } from './XFoldviewNg.js';

export class XFoldviewNgEvents
implements InstanceType<AttributeReactiveClass<typeof XFoldviewNg>>
Expand All @@ -18,9 +19,6 @@ export class XFoldviewNgEvents
#pervScroll = 0;
constructor(dom: XFoldviewNg) {
this.#dom = dom;
this.#dom.addEventListener('scroll', this.#handleScroll, {
passive: true,
});
}
static observedAttributes = ['granularity'];

Expand All @@ -30,6 +28,17 @@ export class XFoldviewNgEvents
else this.#granularity = 0.01;
}

@registerEventEnableStatusChangeHandler('offset')
#enableOffsetEvent(enable: boolean) {
if (enable) {
this.#dom.addEventListener('scroll', this.#handleScroll, {
passive: true,
});
} else {
this.#dom.removeEventListener('scroll', this.#handleScroll);
}
}

#handleScroll = () => {
const curentScrollTop = this.#dom.scrollTop;
const scrollLength = Math.abs(this.#pervScroll - curentScrollTop);
Expand All @@ -46,7 +55,7 @@ export class XFoldviewNgEvents
...commonComponentEventSetting,
detail: {
offset: curentScrollTop,
height: this.#dom.__scrollableLength,
height: this.#dom[scrollableLength],
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
// LICENSE file in the root directory of this source tree.
*/
import type { AttributeReactiveClass } from '@lynx-js/web-elements-reactive';
import type { XFoldviewNg } from './XFoldviewNg.js';
import {
isHeaderShowing,
scrollableLength,
type XFoldviewNg,
} from './XFoldviewNg.js';
import type { XFoldviewSlotNg } from './XFoldviewSlotNg.js';
import { isChromium } from '../common/constants.js';
export class XFoldviewSlotNgTouchEventsHandler
implements InstanceType<AttributeReactiveClass<typeof XFoldviewSlotNg>>
{
#parentScrollTop: number = 0;
#childrenElemsntsScrollTop: WeakMap<Element, number> = new WeakMap();
#childrenElemsntsScrollLeft: WeakMap<Element, number> = new WeakMap();
#elements?: Element[];
#previousPageY: number = 0;
#previousPageX: number = 0;
#scrollingVertically: boolean | null = null;
#currentScrollingElement?: Element;
#deltaY: number = 0;
#dom: XFoldviewSlotNg;
static observedAttributes = [];
constructor(dom: XFoldviewSlotNg) {
Expand All @@ -25,29 +31,25 @@ export class XFoldviewSlotNgTouchEventsHandler
passive: false,
});

this.#dom.addEventListener('touchstart', this.#initPreviousScreen, {
this.#dom.addEventListener('touchstart', this.#touchStart, {
passive: true,
});
this.#dom.addEventListener('touchcancel', this.#initPreviousScreen, {
this.#dom.addEventListener('touchend', this.#touchEnd, {
passive: true,
});
}

#getTheMostScrollableKid(delta: number, isVertical: boolean) {
#getTheMostScrollableKid(delta: number) {
const scrollableKid = this.#elements?.find((element) => {
if (
(isVertical && element.scrollHeight > element.clientHeight)
|| (!isVertical && element.scrollWidth > element.clientWidth)
element.scrollHeight > element.clientHeight
) {
const couldScrollNear = delta < 0
&& (isVertical ? element.scrollTop !== 0 : element.scrollLeft !== 0);
&& element.scrollTop !== 0;
const couldScrollFar = delta > 0
&& Math.abs(
isVertical
? (element.scrollHeight - element.clientHeight
- element.scrollTop)
: (element.scrollWidth - element.clientWidth
- element.scrollLeft),
element.scrollHeight - element.clientHeight
- element.scrollTop,
) > 1;
return couldScrollNear || couldScrollFar;
}
Expand All @@ -56,54 +58,63 @@ export class XFoldviewSlotNgTouchEventsHandler
return scrollableKid;
}

#scrollKid(scrollableKid: Element, delta: number, isVertical: boolean) {
let targetKidScrollDistance = (isVertical
? this.#childrenElemsntsScrollTop
: this.#childrenElemsntsScrollLeft)
.get(scrollableKid) ?? 0;
#scrollKid(scrollableKid: Element, delta: number) {
let targetKidScrollDistance =
this.#childrenElemsntsScrollTop.get(scrollableKid) ?? 0;
targetKidScrollDistance += delta;
this.#childrenElemsntsScrollTop.set(scrollableKid, targetKidScrollDistance);
isVertical
? (scrollableKid.scrollTop = targetKidScrollDistance)
: (scrollableKid.scrollLeft = targetKidScrollDistance);
scrollableKid.scrollTop = targetKidScrollDistance;
}

#scroller = (event: TouchEvent) => {
const parentElement = this.#getParentElement();
const touch = event.touches.item(0)!;
const { pageY, pageX } = touch;
const deltaY = this.#previousPageY! - pageY;
const deltaX = this.#previousPageX! - pageX;
const scrollableKidY = this.#getTheMostScrollableKid(deltaY, true);
const scrollableKidX = this.#getTheMostScrollableKid(deltaX, false);
if (this.#scrollingVertically === null) {
const deltaX = this.#previousPageX! - pageX;
this.#scrollingVertically = Math.abs(deltaY) > Math.abs(deltaX);
}
if (this.#scrollingVertically === false) {
return;
}
/**
* on chromium browsers, the y-axis js scrolling won't interrupt the pan-x gestures
* we make sure the x-axis scrolling will block the y-axis scrolling
*/
const scrollableKidY = this.#getTheMostScrollableKid(deltaY);
/**
* on chromium browsers, the y-axis js scrolling won't interrupt the pan-x gestures
* we make sure the x-axis scrolling will block the y-axis scrolling
*/
if (
deltaY && parentElement && Math.abs(deltaX / 4) < Math.abs(deltaY)
parentElement
) {
if (event.cancelable && !isChromium) {
event.preventDefault();
if (scrollableKidX) {
this.#scrollKid(scrollableKidX, deltaX, false);
}
}
if (
(parentElement.__headershowing && deltaY > 0
(parentElement[isHeaderShowing] && deltaY > 0
|| (deltaY < 0 && !scrollableKidY))
// deltaY > 0: swipe up (folding header)
// scroll the foldview if its scrollable
|| (!parentElement.__headershowing && !scrollableKidY)
|| (!parentElement[isHeaderShowing] && !scrollableKidY)
// all sub doms are scrolled
) {
parentElement.scrollBy({
top: deltaY,
behavior: 'smooth',
});
this.#parentScrollTop += deltaY;
parentElement.scrollTop = this.#parentScrollTop;
this.#currentScrollingElement = parentElement;
} else if (scrollableKidY) {
this.#scrollKid(scrollableKidY, deltaY, true);
this.#currentScrollingElement = scrollableKidY;
this.#scrollKid(scrollableKidY, deltaY);
}
}
this.#previousPageY = pageY;
this.#deltaY = deltaY;
};

#getParentElement(): XFoldviewNg | void {
Expand All @@ -113,7 +124,7 @@ export class XFoldviewSlotNgTouchEventsHandler
}
}

#initPreviousScreen = (event: TouchEvent) => {
#touchStart = (event: TouchEvent) => {
const { pageX, pageY } = event.touches.item(0)!;
this.#elements = document.elementsFromPoint(pageX, pageY).filter(e =>
this.#dom.contains(e)
Expand All @@ -123,7 +134,25 @@ export class XFoldviewSlotNgTouchEventsHandler
this.#parentScrollTop = this.#getParentElement()?.scrollTop ?? 0;
for (const element of this.#elements) {
this.#childrenElemsntsScrollTop.set(element, element.scrollTop);
this.#childrenElemsntsScrollLeft.set(element, element.scrollLeft);
}
this.#scrollingVertically = null;
this.#currentScrollingElement = undefined;
};

#touchEnd = () => {
this.#scrollingVertically = null;
if (this.#currentScrollingElement) {
const parentElement = this.#getParentElement();
if (
this.#currentScrollingElement === parentElement
&& !parentElement[isHeaderShowing]
) {
return;
}
this.#currentScrollingElement.scrollBy({
top: this.#deltaY * 4,
behavior: 'smooth',
});
}
};
}
Loading
Loading