diff --git a/.changeset/calm-needles-rule.md b/.changeset/calm-needles-rule.md
new file mode 100644
index 0000000000..da0d254795
--- /dev/null
+++ b/.changeset/calm-needles-rule.md
@@ -0,0 +1,6 @@
+---
+"@lynx-js/web-constants": patch
+"@lynx-js/web-mainthread-apis": patch
+---
+
+feat: add main-thread API: __QuerySelector
diff --git a/packages/web-platform/web-constants/src/types/MainThreadGlobalThis.ts b/packages/web-platform/web-constants/src/types/MainThreadGlobalThis.ts
index 44b3d7de2a..e2e0ed8db1 100644
--- a/packages/web-platform/web-constants/src/types/MainThreadGlobalThis.ts
+++ b/packages/web-platform/web-constants/src/types/MainThreadGlobalThis.ts
@@ -319,12 +319,17 @@ export type QueryComponentPAPI = (
}) => void,
) => null;
export type InvokeUIMethodPAPI = (
- element: unknown,
+ element: HTMLElement,
method: string,
params: object,
callback: (result: InvokeCallbackRes) => void,
) => void;
+export type QuerySelectorPAPI = (
+ element: HTMLElement,
+ selector: string,
+) => unknown;
+
export interface ElementPAPIs {
__ElementFromBinary: ElementFromBinaryPAPI;
@@ -390,6 +395,7 @@ export interface ElementPAPIs {
options?: FlushElementTreeOptions,
) => void;
__InvokeUIMethod: InvokeUIMethodPAPI;
+ __QuerySelector: QuerySelectorPAPI;
}
export interface MainThreadGlobalThis extends ElementPAPIs {
diff --git a/packages/web-platform/web-mainthread-apis/ts/createMainThreadGlobalThis.ts b/packages/web-platform/web-mainthread-apis/ts/createMainThreadGlobalThis.ts
index c729345ea3..ac57f1ab8f 100644
--- a/packages/web-platform/web-mainthread-apis/ts/createMainThreadGlobalThis.ts
+++ b/packages/web-platform/web-mainthread-apis/ts/createMainThreadGlobalThis.ts
@@ -62,6 +62,8 @@ import {
type QueryComponentPAPI,
lynxEntryNameAttribute,
ErrorCode,
+ type QuerySelectorPAPI,
+ type InvokeUIMethodPAPI,
} from '@lynx-js/web-constants';
import { createMainThreadLynx } from './createMainThreadLynx.js';
import {
@@ -649,6 +651,76 @@ export function createMainThreadGlobalThis(
);
};
+ const __InvokeUIMethod: InvokeUIMethodPAPI = (
+ element,
+ method,
+ params,
+ callback,
+ ) => {
+ try {
+ if (method === 'boundingClientRect') {
+ const rect = (element as HTMLElement).getBoundingClientRect();
+ callback({
+ code: ErrorCode.SUCCESS,
+ data: {
+ id: (element as HTMLElement).id,
+ width: rect.width,
+ height: rect.height,
+ left: rect.left,
+ right: rect.right,
+ top: rect.top,
+ bottom: rect.bottom,
+ },
+ });
+ return;
+ }
+ if (typeof (element as any)[method] === 'function') {
+ const data = (element as any)[method](params);
+ callback({
+ code: ErrorCode.SUCCESS,
+ data,
+ });
+ return;
+ }
+ callback({
+ code: ErrorCode.METHOD_NOT_FOUND,
+ });
+ } catch (e) {
+ console.error(
+ `[lynx-web] invokeUIMethod: apply method failed with`,
+ e,
+ element,
+ );
+ callback({
+ code: ErrorCode.PARAM_INVALID,
+ });
+ }
+ };
+
+ const __QuerySelector: QuerySelectorPAPI = (
+ element,
+ selector,
+ ) => {
+ if (!element) return null;
+ const el = (element as HTMLElement).querySelector(selector);
+ if (el) {
+ if (!(el as any).invoke) {
+ (el as any).invoke = (method: string, params: object) => {
+ return new Promise((resolve, reject) => {
+ __InvokeUIMethod(el as HTMLElement, method, params, (res) => {
+ if (res.code === ErrorCode.SUCCESS) {
+ resolve(res.data);
+ } else {
+ reject(res);
+ }
+ });
+ });
+ };
+ }
+ }
+ return el;
+ };
+
const __GetPageElement: GetPageElementPAPI = () => {
return pageElement;
};
@@ -817,46 +889,8 @@ export function createMainThreadGlobalThis(
_I18nResourceTranslation: callbacks._I18nResourceTranslation,
_AddEventListener: () => {},
renderPage: undefined,
- __InvokeUIMethod: (element, method, params, callback) => {
- try {
- if (method === 'boundingClientRect') {
- const rect = (element as HTMLElement).getBoundingClientRect();
- callback({
- code: ErrorCode.SUCCESS,
- data: {
- id: (element as HTMLElement).id,
- width: rect.width,
- height: rect.height,
- left: rect.left,
- right: rect.right,
- top: rect.top,
- bottom: rect.bottom,
- },
- });
- return;
- }
- if (typeof (element as any)[method] === 'function') {
- const data = (element as any)[method](params);
- callback({
- code: ErrorCode.SUCCESS,
- data,
- });
- return;
- }
- callback({
- code: ErrorCode.METHOD_NOT_FOUND,
- });
- } catch (e) {
- console.error(
- `[lynx-web] invokeUIMethod: apply method failed with`,
- e,
- element,
- );
- callback({
- code: ErrorCode.PARAM_INVALID,
- });
- }
- },
+ __InvokeUIMethod,
+ __QuerySelector,
};
Object.assign(mtsRealm.globalWindow, mtsGlobalThis);
Object.defineProperty(mtsRealm.globalWindow, 'renderPage', {
diff --git a/packages/web-platform/web-tests/tests/react.spec.ts b/packages/web-platform/web-tests/tests/react.spec.ts
index 865ef0e743..eb094043c1 100644
--- a/packages/web-platform/web-tests/tests/react.spec.ts
+++ b/packages/web-platform/web-tests/tests/react.spec.ts
@@ -464,6 +464,22 @@ test.describe('reactlynx3 tests', () => {
expect(scrollTopAfter).toBeGreaterThan(100);
});
+ test('basic-main-query-selector', async ({ page }, { title }) => {
+ await goto(page, title);
+ await wait(100);
+ const scrollView = page.locator('scroll-view');
+ const scrollTopBefore = await scrollView.evaluate((node) =>
+ node.scrollTop
+ );
+ expect(scrollTopBefore).toBe(0);
+ await page.locator('#tap-me').click();
+ await wait(3000);
+ const scrollTopAfter = await scrollView.evaluate((node) =>
+ node.scrollTop
+ );
+ expect(scrollTopAfter).toBeGreaterThan(100);
+ });
+
// lazy component
test(
'basic-lazy-component',
diff --git a/packages/web-platform/web-tests/tests/react/basic-main-query-selector/index.jsx b/packages/web-platform/web-tests/tests/react/basic-main-query-selector/index.jsx
new file mode 100644
index 0000000000..21655323c7
--- /dev/null
+++ b/packages/web-platform/web-tests/tests/react/basic-main-query-selector/index.jsx
@@ -0,0 +1,57 @@
+import { root } from '@lynx-js/react';
+
+export const App = () => {
+ const handleTap = () => {
+ 'main thread';
+ const scrollView = lynx.querySelector('#scroll-view');
+ scrollView?.invoke('autoScroll', {
+ rate: 120,
+ start: true,
+ });
+ };
+
+ return (
+
+
+
+ Tap me to enable auto-scroll
+
+
+
+ {Array.from({ length: 20 }).map((item, index) => (
+
+ Item {index}
+
+ ))}
+
+
+ );
+};
+
+root.render();