Skip to content

fix(number input): prevent from clearing on intermediate decimal entry (Angular)#6115

Open
Copilot wants to merge 14 commits intomainfrom
copilot/fix-input-field-decimal-separator
Open

fix(number input): prevent from clearing on intermediate decimal entry (Angular)#6115
Copilot wants to merge 14 commits intomainfrom
copilot/fix-input-field-decimal-separator

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 21, 2026

When typing a decimal separator in a type="number" input (e.g. 1. or 1,), the Angular binding was calling writeValue("") — because event.target.value returns "" for incomplete numbers — causing the field to clear or the cursor to reset.

Root cause

The browser sets event.target.value = "" when a number input is in an intermediate state (e.g. "1."). For dot (.), validity.badInput is set to true. For comma (,), some browsers/locales (e.g. Firefox) fire an input event with value="" but leave badInput === false. In both cases, the Angular writeValue call was resetting the displayed value while the user was still typing.

Changes

  • src/utils/form-components.ts — In handleFrameworkEventAngular, propagateChange is now always called first (so Angular forms can always react to intermediate values for validation), and writeValue is skipped only when the target is a number input in an intermediate state. The lastValue parameter has been removed as it is no longer needed:
// Always call propagateChange first so Angular forms can react to intermediate values
component.propagateChange(value);

if (
    type === 'number' &&
    event.type === 'input' &&
    (event.target?.validity?.badInput ||
        (value === '' && event.inputType === 'insertText'))
) {
    // Skip writeValue for intermediate number input states (e.g. "1." or "1,")
    return;
}

component.writeValue(value);

This guards against resetting the displayed value during intermediate input while still allowing:

  • propagateChange to always fire so validation logic can respond even for intermediate states
  • completed valid values (e.g. 1.5) to write back normally
  • intentionally cleared fields (via backspace/delete) to write back normally
  • change events (fired on blur) to always propagate and write back the final value

The date/time input handling (writeValue skipped for null/undefined values) is also consolidated here at runtime rather than via build-time injection.

Vue is not affected by this issue and requires no change.

  • src/utils/form-components.spec.ts (new) — Unit tests for both handlers covering: valid number input, intermediate decimal with badInput=true (dot), intermediate decimal with badInput=false and insertText (comma in some browsers), field clearing via backspace, number change event on blur, date/time inputs with null and undefined values, and non-number input types.

💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 21, 2026

🦋 Changeset detected

Latest commit: 9e5849d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@db-ux/ngx-core-components Patch
@db-ux/core-foundations Patch
@db-ux/core-components Patch
@db-ux/react-core-components Patch
@db-ux/v-core-components Patch
@db-ux/wc-core-components Patch
@db-ux/core-stylelint Patch
@db-ux/core-migration Patch
@db-ux/agent-cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

… state

Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix cursor jump and input clearing in number field fix: prevent number input from clearing on intermediate decimal entry (Angular/Vue) Feb 21, 2026
Copilot AI requested a review from mfranzke February 21, 2026 06:26
@mfranzke
Copy link
Copy Markdown
Collaborator

@copilot propagating/emiting is perfectly fine, as we might even want to react on incorrect values and handle their validation. So Vue is not even affected by this, but only Angular, and we need to run your code exclusively in front of component.writeValue(event.target[modelValue]); and not the propagation. Please adapt this change, the test, the description in this PR and revert your changes within package-lock.json as those are unrelated.

…e-lock

Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot propagating/emiting is perfectly fine, as we might even want to react on incorrect values and handle their validation. So Vue is not even affected by this, but only Angular, and we need to ru...

Done in fee6312:

  • Angular: propagateChange is always called; only writeValue is guarded by the badInput check
  • Vue: Change reverted — Vue is not affected
  • Tests: Updated to reflect that propagateChange IS called but writeValue is NOT called during intermediate decimal input
  • package-lock.json: Reverted

Copilot AI changed the title fix: prevent number input from clearing on intermediate decimal entry (Angular/Vue) fix: prevent number input from clearing on intermediate decimal entry (Angular) Feb 21, 2026
@mfranzke mfranzke marked this pull request as ready for review February 21, 2026 18:22
@mfranzke mfranzke enabled auto-merge (squash) February 21, 2026 18:22
@mfranzke mfranzke moved this from 🏗 In progress to 🎁 Ready for review in UX Engineering Team Backlog Feb 21, 2026
@mfranzke mfranzke requested review from Copilot and removed request for mfranzke February 21, 2026 18:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an Angular-specific UX issue where type="number" inputs can get cleared/reset during intermediate decimal entry (e.g. 1. / 1,) due to event.target.value === "" while validity.badInput === true.

Changes:

  • Update Angular framework event handler to skip writeValue for number inputs in badInput intermediate states while still calling propagateChange.
  • Add unit tests covering Angular/Vue handlers for valid number input, intermediate badInput, cleared values, and non-number inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/components/src/utils/form-components.ts Avoids writing back empty values during intermediate number input states in Angular.
packages/components/src/utils/form-components.spec.ts Adds Vitest unit coverage for Angular/Vue framework event handlers (note: ensure these tests are actually executed by CI).

@mfranzke mfranzke changed the title fix: prevent number input from clearing on intermediate decimal entry (Angular) fix(number input): prevent from clearing on intermediate decimal entry (Angular) Feb 23, 2026
@nmerget nmerget moved this from 🎁 Ready for review to 👀 Actively In Review in UX Engineering Team Backlog Feb 23, 2026
@nmerget nmerget self-requested a review February 23, 2026 07:30
@nmerget nmerget moved this from 👀 Actively In Review to 🎶 Waiting for feedback in UX Engineering Team Backlog Feb 23, 2026
auto-merge was automatically disabled February 24, 2026 08:46

Head branch was pushed to by a user without write access

Copilot AI requested a review from mfranzke February 24, 2026 08:47
@nmerget nmerget linked an issue Feb 25, 2026 that may be closed by this pull request
7 tasks
@nmerget nmerget moved this from 🎶 Waiting for feedback to 🎁 Ready for review in UX Engineering Team Backlog Feb 25, 2026
github-actions bot and others added 3 commits February 25, 2026 09:17
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@nmerget nmerget moved this from 🎁 Ready for review to 🏗 In progress in UX Engineering Team Backlog Feb 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 14 comments.

Comment on lines +40 to +54
it('calls propagateChange but skips writeValue when number input has badInput (intermediate state like "1.")', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: '.',
inputType: 'insertText',
target: { type: 'number', value: '1.' }
};
handleFrameworkEventAngular(component, event, 'value', '1');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description and implementation don't match the actual code in form-components.ts. The test claims to check for badInput (intermediate state like "1."), but the actual implementation in form-components.ts checks event.data === '.' instead of validity.badInput. The test event object doesn't include event.type = 'input' which is required by the implementation logic at line 23. This test would pass but doesn't actually validate the real implementation behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +70
it('calls propagateChange but skips writeValue when number input value is empty after insertText (e.g. comma "," in browsers where badInput stays false)', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: ',',
inputType: 'insertText',
target: { type: 'number', value: '' }
};
handleFrameworkEventAngular(component, event, 'value', '1');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description and implementation don't match the actual code in form-components.ts. The test claims to check for the comma scenario, but the actual implementation checks event.data === ',' instead of checking for badInput === false with empty value. The test event object doesn't include event.type = 'input' which is required by the implementation logic at line 23. This test would pass but doesn't actually validate the real implementation behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +81
it('calls propagateChange and writeValue when number input is cleared via backspace (deleteContentBackward)', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = createNumberEvent('', false, 'deleteContentBackward');
handleFrameworkEventAngular(component, event);
expect(component.propagateChange).toHaveBeenCalledWith('');
expect(component.writeValue).toHaveBeenCalledWith('');
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test event is missing event.type = 'input' which is required for the implementation to check at line 23 of form-components.ts. Without this property, the implementation would skip the number input handling entirely and proceed to call both propagateChange and writeValue, making this test pass for the wrong reason.

Copilot uses AI. Check for mistakes.
if (event.type === 'input') {
if (
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential runtime error: When lastValue is undefined (not provided), calling lastValue.toString() will throw a TypeError. The implementation should check if lastValue exists before calling .toString() on it, or handle the optional parameter properly.

Suggested change
(lastValue.toString().includes('.') &&
(lastValue != null &&
lastValue.toString().includes('.') &&

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +33
if (
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
// or content was deleted but last number had a `.`
return;
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation doesn't match the approach described in the PR description. The PR description states the fix checks validity.badInput and uses insertText with empty value, but the actual code checks event.data instead. Using event.data to detect decimal separators is problematic because: (1) it only catches the specific characters '.', ',' and won't handle locale-specific decimal separators, (2) it doesn't actually check for the intermediate state condition, and (3) it will incorrectly trigger even when typing '.' or ',' in a valid context. The approach described in the PR description (checking validity.badInput and event.target.value === '' && event.inputType === 'insertText') would be more robust.

Suggested change
if (
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
// or content was deleted but last number had a `.`
return;
}
const target = event.target as HTMLInputElement | null;
if (
target?.validity?.badInput &&
target.value === '' &&
event.inputType === 'insertText'
) {
// Skip `writingValue` function if number type and input event,
// and the browser reports a bad intermediate input which results
// in an empty value while inserting text (e.g. locale-specific
// decimal separator handling).
return;
}
if (
lastValue?.toString().includes('.') &&
event.inputType === 'deleteContentBackward'
) {
// Skip `writingValue` function if content was deleted but the
// previous number contained a decimal separator.
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +30
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic only handles '.' and ',' decimal separators but doesn't account for other valid intermediate states in number inputs, such as typing 'e' or 'E' for scientific notation (e.g., "1e"), typing '+' or '-' for signs, or typing multiple decimals. These will also result in event.target.value === '' but won't be caught by event.data === '.' || event.data === ',', potentially causing the same clearing issue this fix is trying to address.

Suggested change
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
['.', ',', 'e', 'E', '+', '-'].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.`, `,`, `e`, `E`, `+` or `-` was typed

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +31
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
// or content was deleted but last number had a `.`
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deletion handling logic at line 26-27 has a flaw: it returns early (skipping propagateChange and writeValue) when deleting content from a number that previously had a decimal point, even if the deletion results in a valid number. For example, if the user has "1.5" and deletes the "." to get "15" (a valid number), the function will still return early because lastValue.toString().includes('.') is true. This prevents the valid result from being propagated to Angular forms. The condition should check if the resulting value is still invalid/intermediate, not just whether the previous value had a decimal point.

Suggested change
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
// or content was deleted but last number had a `.`
(value?.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
// or content was deleted but resulting number still has a `.`

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +30
if (
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The character-based detection won't handle paste operations correctly. If a user pastes "1." into a number input, event.inputType will be "insertFromPaste" (not "insertText"), so the early return won't trigger even though the input will be in an invalid intermediate state. Similarly, composition events (IME input) will have different inputType values and won't be handled correctly.

Suggested change
if (
['.', ','].includes(event.data) ||
(lastValue.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` was typed
const isInsertOperation =
typeof event.inputType === 'string' &&
event.inputType.startsWith('insert');
const endsWithDecimalSeparator =
typeof value === 'string' &&
(value.endsWith('.') || value.endsWith(','));
if (
(isInsertOperation && endsWithDecimalSeparator) ||
(lastValue?.toString().includes('.') &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and a value ending with `.` or `,` was produced (by typing,
// pasting or composition)

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +40
component.propagateChange(value);
component.writeValue(value);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation contradicts the PR description's claim that "propagateChange is always called (so Angular forms can react to intermediate values for validation)". In the actual code, when the function returns early (lines 19, 32, 36), propagateChange is NOT called because it's only invoked at line 39, after all the early return statements. This means Angular forms won't receive updates during intermediate states, preventing validation logic from running on partial input, which could be important for certain use cases.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9e5849d. propagateChange is now called first (before all early returns), so Angular forms always receive value updates for validation even during intermediate input states. Key changes:

  • propagateChange moved to top of function — always executed
  • writeValue is skipped only for: date/time with null/undefined value, or number inputs in intermediate state (badInput=true or insertText with empty value)
  • Removed the event.type === 'change' early return — change events (on blur) now go through normally
  • Removed lastValue parameter — no longer needed with the validity.badInput approach

@mfranzke mfranzke removed their assignment Mar 2, 2026
@mfranzke mfranzke moved this from 🏗 In progress to 🎶 Waiting for feedback in UX Engineering Team Backlog Mar 2, 2026
@mfranzke mfranzke removed their request for review March 2, 2026 17:30
@github-actions github-actions bot added the 📕documentation Improvements or additions to documentation label Mar 3, 2026
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ventAngular

Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
Copilot AI requested a review from mfranzke March 13, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🏘components 📕documentation Improvements or additions to documentation

Projects

Status: No status
Status: 🎶 Waiting for feedback

Development

Successfully merging this pull request may close these issues.

DBInput/DBTextarea: Can't set value to undefined Cursor jumps to beginning and input is cleared when entering decimal separator in number field

5 participants