diff --git a/.changeset/plenty-moose-jump.md b/.changeset/plenty-moose-jump.md
new file mode 100644
index 0000000000..de10bd1398
--- /dev/null
+++ b/.changeset/plenty-moose-jump.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/web-elements": patch
+---
+
+feat: x-input and x-textarea support bindselection event, the returned type structure is `{ selectionStart: number; selectionEnd: number }`.
diff --git a/packages/web-platform/web-elements/src/XInput/XInputEvents.ts b/packages/web-platform/web-elements/src/XInput/XInputEvents.ts
index bad9fbaf82..610ca90593 100644
--- a/packages/web-platform/web-elements/src/XInput/XInputEvents.ts
+++ b/packages/web-platform/web-elements/src/XInput/XInputEvents.ts
@@ -108,6 +108,37 @@ export class XInputEvents
}
};
+ @registerEventEnableStatusChangeHandler('selection')
+ #handleEnableSelectionEvent(status: boolean) {
+ if (status) {
+ this.#getInputElement().addEventListener(
+ 'select',
+ this.#selectEvent,
+ {
+ passive: true,
+ },
+ );
+ } else {
+ this.#getInputElement().removeEventListener(
+ 'select',
+ this.#selectEvent,
+ );
+ }
+ }
+
+ #selectEvent = () => {
+ const input = this.#getInputElement();
+ this.#dom.dispatchEvent(
+ new CustomEvent('selection', {
+ ...commonComponentEventSetting,
+ detail: {
+ selectionStart: input.selectionStart,
+ selectionEnd: input.selectionEnd,
+ },
+ }),
+ );
+ };
+
#blockHtmlEvent = (event: InputEvent) => {
if (
event.target === this.#getInputElement()
diff --git a/packages/web-platform/web-elements/src/XTextarea/XTextareaEvents.ts b/packages/web-platform/web-elements/src/XTextarea/XTextareaEvents.ts
index 72378e5739..5c3c29bb5c 100644
--- a/packages/web-platform/web-elements/src/XTextarea/XTextareaEvents.ts
+++ b/packages/web-platform/web-elements/src/XTextarea/XTextareaEvents.ts
@@ -108,6 +108,37 @@ export class XTextareaEvents
}
};
+ @registerEventEnableStatusChangeHandler('selection')
+ #handleEnableSelectionEvent(status: boolean) {
+ if (status) {
+ this.#getTextareaElement().addEventListener(
+ 'select',
+ this.#selectEvent,
+ {
+ passive: true,
+ },
+ );
+ } else {
+ this.#getTextareaElement().removeEventListener(
+ 'select',
+ this.#selectEvent,
+ );
+ }
+ }
+
+ #selectEvent = () => {
+ const input = this.#getTextareaElement();
+ this.#dom.dispatchEvent(
+ new CustomEvent('selection', {
+ ...commonComponentEventSetting,
+ detail: {
+ selectionStart: input.selectionStart,
+ selectionEnd: input.selectionEnd,
+ },
+ }),
+ );
+ };
+
#blockHtmlEvent = (event: FocusEvent | InputEvent) => {
if (
event.target === this.#getTextareaElement()
diff --git a/packages/web-platform/web-tests/tests/react.spec.ts b/packages/web-platform/web-tests/tests/react.spec.ts
index ec8e5b39f3..a9ed8de5ea 100644
--- a/packages/web-platform/web-tests/tests/react.spec.ts
+++ b/packages/web-platform/web-tests/tests/react.spec.ts
@@ -2237,6 +2237,21 @@ test.describe('reactlynx3 tests', () => {
expect(selectionEnd).toBe(true);
},
);
+ test(
+ 'basic-element-x-input-bindselection',
+ async ({ page }, { title }) => {
+ await goto(page, title);
+ await wait(200);
+ await page.evaluate(() => {
+ const inputDom = document.querySelector('lynx-view')?.shadowRoot
+ ?.querySelector('x-input')?.shadowRoot?.querySelector('input');
+ inputDom?.focus();
+ inputDom?.setSelectionRange(2, 5);
+ });
+ const result = await page.locator('.result').first().innerText();
+ expect(result).toBe('2-5');
+ },
+ );
});
test.describe('x-overlay-ng', () => {
test('basic-element-x-overlay-ng-demo', async ({ page }, { title }) => {
@@ -3535,6 +3550,23 @@ test.describe('reactlynx3 tests', () => {
expect(selectionEnd).toBe(true);
},
);
+ test(
+ 'basic-element-x-textarea-bindselection',
+ async ({ page }, { title }) => {
+ await goto(page, title);
+ await wait(200);
+ await page.evaluate(() => {
+ const textareaDom = document.querySelector('lynx-view')?.shadowRoot
+ ?.querySelector('x-textarea')?.shadowRoot?.querySelector(
+ 'textarea',
+ );
+ textareaDom?.focus();
+ textareaDom?.setSelectionRange(2, 5);
+ });
+ const result = await page.locator('.result').first().innerText();
+ expect(result).toBe('2-5');
+ },
+ );
});
test.describe('x-audio-tt', () => {
test('basic-element-x-audio-tt-play', async ({ page }, { title }) => {
diff --git a/packages/web-platform/web-tests/tests/react/basic-element-x-input-bindselection/index.jsx b/packages/web-platform/web-tests/tests/react/basic-element-x-input-bindselection/index.jsx
new file mode 100644
index 0000000000..87e49ef9df
--- /dev/null
+++ b/packages/web-platform/web-tests/tests/react/basic-element-x-input-bindselection/index.jsx
@@ -0,0 +1,28 @@
+// Copyright 2023 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 { root, useState } from '@lynx-js/react';
+
+function App() {
+ const value = 'bindselection';
+ const [result, setResult] = useState();
+
+ const onSelection = ({ detail: { selectionStart, selectionEnd } }) => {
+ setResult(`${selectionStart}-${selectionEnd}`);
+ };
+
+ return (
+
+
+
+ {result}
+
+
+ );
+}
+
+root.render();
diff --git a/packages/web-platform/web-tests/tests/react/basic-element-x-textarea-bindselection/index.jsx b/packages/web-platform/web-tests/tests/react/basic-element-x-textarea-bindselection/index.jsx
new file mode 100644
index 0000000000..93e5b8212a
--- /dev/null
+++ b/packages/web-platform/web-tests/tests/react/basic-element-x-textarea-bindselection/index.jsx
@@ -0,0 +1,28 @@
+// Copyright 2023 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 { root, useState } from '@lynx-js/react';
+
+function App() {
+ const value = 'bindselection';
+ const [result, setResult] = useState();
+
+ const onSelection = ({ detail: { selectionStart, selectionEnd } }) => {
+ setResult(`${selectionStart}-${selectionEnd}`);
+ };
+
+ return (
+
+
+
+ {result}
+
+
+ );
+}
+
+root.render();