diff --git a/.changeset/shaggy-badgers-fix.md b/.changeset/shaggy-badgers-fix.md new file mode 100644 index 0000000000..cb2d604eac --- /dev/null +++ b/.changeset/shaggy-badgers-fix.md @@ -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) diff --git a/.cspell/lynx.txt b/.cspell/lynx.txt index 8dc497ff5a..4c037824af 100644 --- a/.cspell/lynx.txt +++ b/.cspell/lynx.txt @@ -39,3 +39,5 @@ disexposure viewpager scrolltoupper scrolltolower +foldview +layoutchange diff --git a/cspell.jsonc b/cspell.jsonc index aaa43f77a0..c7c206752a 100644 --- a/cspell.jsonc +++ b/cspell.jsonc @@ -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": [ diff --git a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewHeaderNgFeatures.ts b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewHeaderNgFeatures.ts index 57f6b899e6..8ca24f5ec3 100644 --- a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewHeaderNgFeatures.ts +++ b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewHeaderNgFeatures.ts @@ -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> @@ -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; } } } diff --git a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNg.ts b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNg.ts index eb7bf0cf2f..1204021c0d 100644 --- a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNg.ts +++ b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNg.ts @@ -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('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 }) { diff --git a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNgEvents.ts b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNgEvents.ts index 454bbe3650..7175d8d7f0 100644 --- a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNgEvents.ts +++ b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewNgEvents.ts @@ -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> @@ -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']; @@ -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); @@ -46,7 +55,7 @@ export class XFoldviewNgEvents ...commonComponentEventSetting, detail: { offset: curentScrollTop, - height: this.#dom.__scrollableLength, + height: this.#dom[scrollableLength], }, }), ); diff --git a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts index 8fc6a053dd..b93641e9f0 100644 --- a/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts +++ b/packages/web-platform/web-elements/src/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts @@ -4,7 +4,11 @@ // 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 @@ -12,10 +16,12 @@ export class XFoldviewSlotNgTouchEventsHandler { #parentScrollTop: number = 0; #childrenElemsntsScrollTop: WeakMap = new WeakMap(); - #childrenElemsntsScrollLeft: WeakMap = 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) { @@ -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; } @@ -56,16 +58,12 @@ 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) => { @@ -73,37 +71,50 @@ export class XFoldviewSlotNgTouchEventsHandler 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 { @@ -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) @@ -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', + }); } }; } diff --git a/packages/web-platform/web-tests/tests/web-elements.spec.ts b/packages/web-platform/web-tests/tests/web-elements.spec.ts index 2985aa0dcd..6a0f14191a 100644 --- a/packages/web-platform/web-tests/tests/web-elements.spec.ts +++ b/packages/web-platform/web-tests/tests/web-elements.spec.ts @@ -806,6 +806,22 @@ test.describe('web-elements test suite', () => { }); }); test.describe('x-foldview-ng', () => { + test('x-foldview-ng/basic-fling', async ({ page, browserName, context }, { + title, + }) => { + test.skip(browserName !== 'chromium', 'using chromium only cdp methods'); + const cdpSession = await context.newCDPSession(page); + await gotoWebComponentPage(page, title); + await diffScreenShot(page, title, 'initial'); + await swipe(cdpSession, { + x: 100, + y: 500, + xDistance: 0, + yDistance: -250, + steps: 10, + }); + await diffScreenShot(page, title, 'fling-works'); + }); test('x-foldview-ng/size-controled-by-parent-flex-cross-axis', async ({ page, browserName, @@ -891,8 +907,8 @@ test.describe('web-elements test suite', () => { x: 100, y: 400, xDistance: 0, - yDistance: -250, - speed: 300, + yDistance: -200, + steps: 30, }); let currentScrollviewScrolledPosition = await scrollview.evaluate(( dom: HTMLElement, @@ -956,6 +972,29 @@ test.describe('web-elements test suite', () => { 'swipe-back-to-show-header', ).toBeLessThan(200); }); + + test( + 'x-foldview-ng/size-toolbar-and-slot-size-lager', + async ({ page, browserName, context }, { + title, + }) => { + test.skip( + browserName !== 'chromium', + 'using chromium only cdp methods', + ); + const cdpSession = await context.newCDPSession(page); + await gotoWebComponentPage(page, title); + await diffScreenShot(page, title, 'initial'); + await swipe(cdpSession, { + x: 100, + y: 500, + xDistance: 0, + yDistance: -600, + speed: 200, + }); + await diffScreenShot(page, title, 'fully-swiped'); + }, + ); test('x-foldview-ng/swipe-with-x-scroll-view', async ({ page, browserName, @@ -1006,7 +1045,7 @@ test.describe('web-elements test suite', () => { y: 250, xDistance: 0, yDistance: -50, - speed: 200, + steps: 20, }); await wait(100); let scrollEvents = await events.jsonValue(); @@ -1016,7 +1055,7 @@ test.describe('web-elements test suite', () => { y: 250, xDistance: 0, yDistance: -100, - speed: 200, + steps: 20, }); await wait(100); scrollEvents = await events.jsonValue(); @@ -1108,8 +1147,8 @@ test.describe('web-elements test suite', () => { x: 100, y: 400, xDistance: 0, - yDistance: -250, - speed: 300, + yDistance: -200, + steps: 30, }); let currentScrollviewScrolledPosition = await scrollview.evaluate(( dom: HTMLElement, diff --git a/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/fling-works-chromium-linux.png b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/fling-works-chromium-linux.png new file mode 100644 index 0000000000..d56d99f6e9 Binary files /dev/null and b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/fling-works-chromium-linux.png differ diff --git a/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/initial-chromium-linux.png b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/initial-chromium-linux.png new file mode 100644 index 0000000000..35d0f5eb3e Binary files /dev/null and b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-fling/initial-chromium-linux.png differ diff --git a/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/fully-swiped-chromium-linux.png b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/fully-swiped-chromium-linux.png new file mode 100644 index 0000000000..67ef483bcc Binary files /dev/null and b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/fully-swiped-chromium-linux.png differ diff --git a/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/initial-chromium-linux.png b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/initial-chromium-linux.png new file mode 100644 index 0000000000..35d0f5eb3e Binary files /dev/null and b/packages/web-platform/web-tests/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-toolbar-and-slot-size-lager/initial-chromium-linux.png differ diff --git a/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/basic-fling.html b/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/basic-fling.html new file mode 100644 index 0000000000..6259602a27 --- /dev/null +++ b/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/basic-fling.html @@ -0,0 +1,68 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/size-toolbar-and-slot-size-lager.html b/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/size-toolbar-and-slot-size-lager.html new file mode 100644 index 0000000000..bed6fe41ad --- /dev/null +++ b/packages/web-platform/web-tests/tests/web-elements/x-foldview-ng/size-toolbar-and-slot-size-lager.html @@ -0,0 +1,68 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +