From 2b9843f509a273171c9056492ffbd012f843cb7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:30:45 +0000 Subject: [PATCH 1/5] Propagate x-input number type to inner input Co-authored-by: PupilTong <12288479+PupilTong@users.noreply.github.com> --- .../src/elements/XInput/InputBaseAttributes.ts | 1 + .../web-elements/tests/web-elements.spec.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts index 812dc2fc85..f81525957d 100644 --- a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts @@ -79,6 +79,7 @@ export class InputBaseAttributes inputMode = 'numeric'; } else if (attributeValue === 'number') { inputMode = 'decimal'; + inputType = 'number'; } else if (attributeValue === 'email') { inputMode = 'email'; } else if (attributeValue === 'tel') { diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts b/packages/web-platform/web-elements/tests/web-elements.spec.ts index e08433cc3c..ea1bbce51d 100644 --- a/packages/web-platform/web-elements/tests/web-elements.spec.ts +++ b/packages/web-platform/web-elements/tests/web-elements.spec.ts @@ -2623,6 +2623,16 @@ test.describe('web-elements test suite', () => { }, ); + test( + 'attribute-type-number-inner-input-type', + async ({ page }, { titlePath }) => { + const title = getTitle(titlePath); + await gotoWebComponentPage(page, title); + const inputType = await page.locator('input').getAttribute('type'); + expect(inputType).toBe('number'); + }, + ); + test( 'attribute-type-tel', async ({ page }, { titlePath, title: simpleTitle }) => { From c6841fde454dedfacb50c9e359cb9e03a6849dc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 05:51:28 +0000 Subject: [PATCH 2/5] Co-authored-by: PupilTong <12288479+PupilTong@users.noreply.github.com> --- ...ttribute-type-number-inner-input-type.html | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-input/attribute-type-number-inner-input-type.html diff --git a/packages/web-platform/web-elements/tests/fixtures/x-input/attribute-type-number-inner-input-type.html b/packages/web-platform/web-elements/tests/fixtures/x-input/attribute-type-number-inner-input-type.html new file mode 100644 index 0000000000..098e6a32f2 --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-input/attribute-type-number-inner-input-type.html @@ -0,0 +1,31 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + From 72245ac8026e4424e19192a3a759e56760c581fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 06:33:57 +0000 Subject: [PATCH 3/5] chore: add changeset for x-input fix Co-authored-by: PupilTong <12288479+PupilTong@users.noreply.github.com> --- .changeset/tidy-houses-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-houses-smash.md diff --git a/.changeset/tidy-houses-smash.md b/.changeset/tidy-houses-smash.md new file mode 100644 index 0000000000..957dab4662 --- /dev/null +++ b/.changeset/tidy-houses-smash.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/web-elements": patch +--- + +Fix x-input number type forwarding to the inner input element. From edf2f9c8e81e76d1346212e9f386a2e5bf3ffc12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:20:55 +0000 Subject: [PATCH 4/5] fix: filter invalid number input Co-authored-by: PupilTong <12288479+PupilTong@users.noreply.github.com> --- .github/lynx-stack.instructions.md | 1 + .../elements/XInput/InputBaseAttributes.ts | 24 ++++++++++++++++++- .../web-elements/tests/web-elements.spec.ts | 6 ++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/lynx-stack.instructions.md b/.github/lynx-stack.instructions.md index 4dac45fc1f..fe22e83ca5 100644 --- a/.github/lynx-stack.instructions.md +++ b/.github/lynx-stack.instructions.md @@ -4,3 +4,4 @@ applyTo: "packages/web-platform/web-elements/**/*" When updating web element APIs, add targeted Playwright tests in packages/web-platform/web-elements/tests/web-elements.spec.ts and keep changes minimal. Ensure Playwright browsers are installed (pnpm exec playwright install --with-deps ) before running web-elements tests. +For x-input type="number" in web-elements, keep inner input type as text, set inputmode="decimal", and rely on input-filter regex to allow one dot with digits. diff --git a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts index f81525957d..c0abf890d6 100644 --- a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts @@ -11,6 +11,7 @@ import { } from '../../element-reactive/index.js'; type InputType = 'text' | 'number' | 'digit' | 'password' | 'tel' | 'email'; +const numberInputFilter = '[^0-9.]|\\.(?=.*\\.)'; /** * shared by x-input and x-input-ng */ @@ -26,6 +27,7 @@ export class InputBaseAttributes 'spell-check', ]; #dom: HTMLElement; + #numberInputFilterOverride: string | null | undefined; #getInputElement = genDomGetter(() => this.#dom.shadowRoot!, '#input'); @@ -62,6 +64,7 @@ export class InputBaseAttributes @registerAttributeHandler('type', true) _handleType(value: string | null) { const attributeValue = value as InputType; + const existingInputFilter = this.#dom.getAttribute('input-filter'); // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode let inputMode: | 'text' @@ -79,7 +82,12 @@ export class InputBaseAttributes inputMode = 'numeric'; } else if (attributeValue === 'number') { inputMode = 'decimal'; - inputType = 'number'; + if (existingInputFilter !== numberInputFilter) { + if (this.#numberInputFilterOverride === undefined) { + this.#numberInputFilterOverride = existingInputFilter; + } + this.#dom.setAttribute('input-filter', numberInputFilter); + } } else if (attributeValue === 'email') { inputMode = 'email'; } else if (attributeValue === 'tel') { @@ -87,6 +95,20 @@ export class InputBaseAttributes } else { inputType = attributeValue; } + if ( + attributeValue !== 'number' + && this.#numberInputFilterOverride !== undefined + ) { + if (this.#numberInputFilterOverride === null) { + this.#dom.removeAttribute('input-filter'); + } else { + this.#dom.setAttribute( + 'input-filter', + this.#numberInputFilterOverride, + ); + } + this.#numberInputFilterOverride = undefined; + } this.#setInputmode(inputMode); this.#setType(inputType); } diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts b/packages/web-platform/web-elements/tests/web-elements.spec.ts index ea1bbce51d..6297057638 100644 --- a/packages/web-platform/web-elements/tests/web-elements.spec.ts +++ b/packages/web-platform/web-elements/tests/web-elements.spec.ts @@ -2629,7 +2629,11 @@ test.describe('web-elements test suite', () => { const title = getTitle(titlePath); await gotoWebComponentPage(page, title); const inputType = await page.locator('input').getAttribute('type'); - expect(inputType).toBe('number'); + expect(inputType).toBe('text'); + const inputFilter = await page + .locator('x-input') + .getAttribute('input-filter'); + expect(inputFilter).toBe('[^0-9.]|\\.(?=.*\\.)'); }, ); From 05b982d3b029d930bba3113bdfbd4e5dcbe46161 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:29:44 +0000 Subject: [PATCH 5/5] fix: internal number filtering for x-input Co-authored-by: PupilTong <12288479+PupilTong@users.noreply.github.com> --- .github/lynx-stack.instructions.md | 2 +- .../elements/XInput/InputBaseAttributes.ts | 23 ------------------- .../src/elements/XInput/XInputEvents.ts | 23 ++++++++++++------- .../web-elements/tests/web-elements.spec.ts | 8 ++----- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/.github/lynx-stack.instructions.md b/.github/lynx-stack.instructions.md index fe22e83ca5..90879de3a4 100644 --- a/.github/lynx-stack.instructions.md +++ b/.github/lynx-stack.instructions.md @@ -4,4 +4,4 @@ applyTo: "packages/web-platform/web-elements/**/*" When updating web element APIs, add targeted Playwright tests in packages/web-platform/web-elements/tests/web-elements.spec.ts and keep changes minimal. Ensure Playwright browsers are installed (pnpm exec playwright install --with-deps ) before running web-elements tests. -For x-input type="number" in web-elements, keep inner input type as text, set inputmode="decimal", and rely on input-filter regex to allow one dot with digits. +For x-input type="number" in web-elements, keep inner input type as text, set inputmode="decimal", and filter number input internally without setting input-filter explicitly. diff --git a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts index c0abf890d6..812dc2fc85 100644 --- a/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XInput/InputBaseAttributes.ts @@ -11,7 +11,6 @@ import { } from '../../element-reactive/index.js'; type InputType = 'text' | 'number' | 'digit' | 'password' | 'tel' | 'email'; -const numberInputFilter = '[^0-9.]|\\.(?=.*\\.)'; /** * shared by x-input and x-input-ng */ @@ -27,7 +26,6 @@ export class InputBaseAttributes 'spell-check', ]; #dom: HTMLElement; - #numberInputFilterOverride: string | null | undefined; #getInputElement = genDomGetter(() => this.#dom.shadowRoot!, '#input'); @@ -64,7 +62,6 @@ export class InputBaseAttributes @registerAttributeHandler('type', true) _handleType(value: string | null) { const attributeValue = value as InputType; - const existingInputFilter = this.#dom.getAttribute('input-filter'); // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode let inputMode: | 'text' @@ -82,12 +79,6 @@ export class InputBaseAttributes inputMode = 'numeric'; } else if (attributeValue === 'number') { inputMode = 'decimal'; - if (existingInputFilter !== numberInputFilter) { - if (this.#numberInputFilterOverride === undefined) { - this.#numberInputFilterOverride = existingInputFilter; - } - this.#dom.setAttribute('input-filter', numberInputFilter); - } } else if (attributeValue === 'email') { inputMode = 'email'; } else if (attributeValue === 'tel') { @@ -95,20 +86,6 @@ export class InputBaseAttributes } else { inputType = attributeValue; } - if ( - attributeValue !== 'number' - && this.#numberInputFilterOverride !== undefined - ) { - if (this.#numberInputFilterOverride === null) { - this.#dom.removeAttribute('input-filter'); - } else { - this.#dom.setAttribute( - 'input-filter', - this.#numberInputFilterOverride, - ); - } - this.#numberInputFilterOverride = undefined; - } this.#setInputmode(inputMode); this.#setType(inputType); } diff --git a/packages/web-platform/web-elements/src/elements/XInput/XInputEvents.ts b/packages/web-platform/web-elements/src/elements/XInput/XInputEvents.ts index ab7a738587..80f5c48854 100644 --- a/packages/web-platform/web-elements/src/elements/XInput/XInputEvents.ts +++ b/packages/web-platform/web-elements/src/elements/XInput/XInputEvents.ts @@ -19,6 +19,7 @@ export class XInputEvents #dom: HTMLElement; #sendComposingInput = false; + #numberInputFilter = /[^0-9.]|\.(?=.*\.)/g; #getInputElement = genDomGetter( () => this.#dom.shadowRoot!, @@ -75,10 +76,7 @@ export class XInputEvents #teleportInput = (event: InputEvent) => { const input = this.#getInputElement(); - const inputFilter = this.#dom.getAttribute('input-filter'); - const filterValue = inputFilter - ? input.value.replace(new RegExp(inputFilter, 'g'), '') - : input.value; + const filterValue = this.#filterInputValue(input.value); const isComposing = event.isComposing; input.value = filterValue; if (isComposing && !this.#sendComposingInput) return; @@ -101,10 +99,7 @@ export class XInputEvents #teleportCompositionendInput = () => { const input = this.#getInputElement(); - const inputFilter = this.#dom.getAttribute('input-filter'); - const filterValue = inputFilter - ? input.value.replace(new RegExp(inputFilter, 'g'), '') - : input.value; + const filterValue = this.#filterInputValue(input.value); input.value = filterValue; // if #sendComposingInput set true, #teleportInput will send detail if (!this.#sendComposingInput) { @@ -126,6 +121,18 @@ export class XInputEvents } }; + #filterInputValue(value: string) { + let filterValue = value; + if (this.#dom.getAttribute('type') === 'number') { + filterValue = filterValue.replace(this.#numberInputFilter, ''); + } + const inputFilter = this.#dom.getAttribute('input-filter'); + if (inputFilter) { + filterValue = filterValue.replace(new RegExp(inputFilter, 'g'), ''); + } + return filterValue; + } + @registerEventEnableStatusChangeHandler('selection') _handleEnableSelectionEvent(status: boolean) { if (status) { diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts b/packages/web-platform/web-elements/tests/web-elements.spec.ts index 6297057638..8a74efb0be 100644 --- a/packages/web-platform/web-elements/tests/web-elements.spec.ts +++ b/packages/web-platform/web-elements/tests/web-elements.spec.ts @@ -2630,10 +2630,6 @@ test.describe('web-elements test suite', () => { await gotoWebComponentPage(page, title); const inputType = await page.locator('input').getAttribute('type'); expect(inputType).toBe('text'); - const inputFilter = await page - .locator('x-input') - .getAttribute('input-filter'); - expect(inputFilter).toBe('[^0-9.]|\\.(?=.*\\.)'); }, ); @@ -2825,9 +2821,9 @@ test.describe('web-elements test suite', () => { return detail; }); await page.mouse.click(100, 25); - await page.keyboard.type('2.'); + await page.keyboard.type('1.2a'); await wait(200); - expect((await confirmValue.jsonValue()).value).toBe('2.'); + expect((await confirmValue.jsonValue()).value).toBe('1.2'); }, );