diff --git a/.changeset/nasty-clouds-win.md b/.changeset/nasty-clouds-win.md new file mode 100644 index 0000000000..89b0eff9bf --- /dev/null +++ b/.changeset/nasty-clouds-win.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/web-elements": patch +--- + +feat: add wheel event handling and corresponding tests for x-foldview-ng diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts index 6dcebe9dec..2e2cfcba33 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts @@ -22,7 +22,7 @@ export class XFoldviewSlotNgTouchEventsHandler constructor(dom: XFoldviewSlotNg) { this.#dom = dom; - this.#dom.addEventListener('touchmove', this.#scroller, { + this.#dom.addEventListener('touchmove', this.#handleTouch, { passive: false, }); @@ -32,6 +32,9 @@ export class XFoldviewSlotNgTouchEventsHandler this.#dom.addEventListener('touchend', this.#touchEnd, { passive: true, }); + this.#dom.addEventListener('wheel', this.#handleWheel, { + passive: false, + }); } #getTheMostScrollableKid(delta: number) { @@ -61,8 +64,11 @@ export class XFoldviewSlotNgTouchEventsHandler scrollableKid.scrollTop = targetKidScrollDistance; } - #scroller = (event: TouchEvent) => { + #handleTouch = (event: TouchEvent) => { const parentElement = this.#getParentElement(); + if (!parentElement) { + return; + } const touch = event.touches.item(0)!; const { pageY, pageX } = touch; const deltaY = this.#previousPageY! - pageY; @@ -73,35 +79,41 @@ export class XFoldviewSlotNgTouchEventsHandler if (this.#scrollingVertically === false) { return; } - const scrollableKidY = this.#getTheMostScrollableKid(deltaY); - if ( - parentElement - ) { - if (event.cancelable) { - event.preventDefault(); - } - if ( - (parentElement[isHeaderShowing] && deltaY > 0 - || (deltaY < 0 && !scrollableKidY)) - // deltaY > 0: swipe up (folding header) - // scroll the foldview if its scrollable - || (!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.#currentScrollingElement = scrollableKidY; - this.#scrollKid(scrollableKidY, deltaY); - } + if (event.cancelable) { + event.preventDefault(); } + this.#handleScrollDelta(deltaY, parentElement); this.#previousPageY = pageY; - this.#deltaY = deltaY; + }; + + #handleWheel = (event: WheelEvent) => { + const parentElement = this.#getParentElement(); + if (!parentElement) { + return; + } + if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) { + return; + } + const pathElements = event.composedPath().filter(( + element, + ): element is Element => + element instanceof Element && this.#dom.contains(element) + ); + const { clientX, clientY } = event; + const pointElements = document.elementsFromPoint(clientX, clientY).filter( + e => this.#dom.contains(e), + ); + this.#elements = [...new Set([...pathElements, ...pointElements])]; + this.#parentScrollTop = parentElement.scrollTop; + if (this.#elements) { + for (const element of this.#elements) { + this.#childrenElemsntsScrollTop.set(element, element.scrollTop); + } + } + if (event.cancelable) { + event.preventDefault(); + } + this.#handleScrollDelta(event.deltaY, parentElement); }; #getParentElement(): XFoldviewNg | void { @@ -142,4 +154,31 @@ export class XFoldviewSlotNgTouchEventsHandler }); } }; + + #handleScrollDelta( + deltaY: number, + parentElement: XFoldviewNg, + ) { + const scrollableKidY = this.#getTheMostScrollableKid(deltaY); + if ( + (parentElement[isHeaderShowing] && deltaY > 0 + || (deltaY < 0 && !scrollableKidY)) + // deltaY > 0: swipe up (folding header) + // scroll the foldview if its scrollable + || (!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.#currentScrollingElement = scrollableKidY; + this.#scrollKid(scrollableKidY, deltaY); + } + this.#deltaY = deltaY; + } } diff --git a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html new file mode 100644 index 0000000000..7c720bf9ee --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html @@ -0,0 +1,67 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html new file mode 100644 index 0000000000..669e7bc870 --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html @@ -0,0 +1,69 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts b/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts new file mode 100644 index 0000000000..83d16a45c4 --- /dev/null +++ b/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts @@ -0,0 +1,93 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { test, expect } from '@lynx-js/playwright-fixtures'; +import type { Page } from '@playwright/test'; + +const wait = async (ms: number) => { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +const goto = async (page: Page, fixtureName: string) => { + await page.goto(`/tests/fixtures/${fixtureName}.html`, { + waitUntil: 'load', + }); + await page.evaluate(() => document.fonts.ready); +}; + +test.describe('x-foldview-ng wheel', () => { + test('x-foldview-ng/wheel-parent-first', async ({ page, browserName }, { + title, + }) => { + test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit'); + await goto(page, title); + const foldview = page.locator('#foldview'); + const scrollview = page.locator('#inner-scroll'); + await page.locator('#inner-scroll').hover(); + + await foldview.evaluate((dom: HTMLElement) => { + dom.scrollTop = 0; + }); + await scrollview.evaluate((dom: HTMLElement) => { + dom.scrollTop = 0; + }); + + const foldviewInitial = await foldview.evaluate((dom: HTMLElement) => + dom.scrollTop + ); + const scrollViewInitial = await scrollview.evaluate((dom: HTMLElement) => + dom.scrollTop + ); + + await page.mouse.wheel(0, 120); + await wait(200); + + expect( + await foldview.evaluate((dom: HTMLElement) => dom.scrollTop), + 'wheel-outer-scrolls-first', + ).toBeGreaterThan(foldviewInitial); + expect( + await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop), + 'wheel-inner-not-scrolled-before-header', + ).toBe(scrollViewInitial); + }); + + test('x-foldview-ng/wheel-smooth-continue', async ({ page, browserName }, { + title, + }) => { + test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit'); + await goto(page, title); + const foldview = page.locator('#foldview'); + const scrollview = page.locator('#inner-scroll'); + await page.locator('#inner-scroll').hover(); + + await foldview.evaluate((dom: HTMLElement) => { + dom.scrollTop = 0; + }); + await scrollview.evaluate((dom: HTMLElement) => { + dom.scrollTop = 0; + }); + + await foldview.evaluate((dom: HTMLElement) => { + dom.scrollTop = dom.scrollHeight; + }); + await wait(100); + + const foldviewBeforeInner = await foldview.evaluate((dom: HTMLElement) => + dom.scrollTop + ); + await page.mouse.wheel(0, 200); + await wait(200); + + expect( + await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop), + 'wheel-continues-to-inner-scroll', + ).toBeGreaterThan(0); + expect( + await foldview.evaluate((dom: HTMLElement) => dom.scrollTop), + 'wheel-outer-stays-at-end', + ).toBeGreaterThanOrEqual(foldviewBeforeInner); + }); +});