Skip to content
Merged
1 change: 1 addition & 0 deletions app/components/new-signup/input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
placeholder="Darth"
aria-labelledby="signup-form-label"
{{on "input" this.inputFieldChanged}}
{{on "keydown" this.handleKeydown}}
data-test-signup-form-input
/>
{{#if @error}}
Expand Down
27 changes: 25 additions & 2 deletions app/components/new-signup/input.js
Copy link
Member

Choose a reason for hiding this comment

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

Should we also add sanitization during the signup form submit for first and last name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's no need to add sanitization on submit because it is guaranteed that no spaces will be present at submission time (already sanitized on input change).

Copy link
Member

@MayankBansal12 MayankBansal12 Dec 16, 2025

Choose a reason for hiding this comment

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

  • what if user changes directly changes in HTML using inspect element?
  • are there going to be any changes in backend API for this? how will API handle if spaces are included in names?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Adding sanitization to the from button will be redundant. The user who bypasses the onChange input sanitization by changing the value via browser dev tools will still ultimately receive an HTTP 400 Bad Request error response from the backend.

  2. No, there will be no changes in the backend API. It is already handling the input validation by using validateGenerateUsernameQuery middleware.

Copy link
Contributor

Choose a reason for hiding this comment

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

@azeemuddinaziz can you please reply to these question?

Copy link
Member

Choose a reason for hiding this comment

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

@MayankBansal12 and @Suvidh-kaushik great observation, let me drill down this

In Ember (especially with tracked state + one-way data flow), direct DOM mutation can desync the input value from the tracked property backing that input.

What can go wrong
• Cursor jumps to the end while typing
• Value reverts on re-render
• Input appears to accept spaces briefly and then snaps back
• Fails when used with @value={{this.someTrackedProp}}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@iamitprakash Thanks for detailed explanation.

  1. I added a keydown event that preventDefaults spaces. This completely stops the cursor jumping and desync issues since the space never hits the DOM.
  2. I still need the direct DOM manipulation for the paste edge case, where user directly pastes in a text which have spaces in it. I've updated it to only run if the input actually contains a space, so it won't interfere with normal updates.
I have added the test cases for the keydown implementation. image Screenshot 2025-12-19 at 3 30 39 PM

- I have updated the test coverage in PR description as well.

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,31 @@ export default class SignupComponent extends Component {
return LABEL_TEXT[currentStep];
}

@action inputFieldChanged({ target: { value } }) {
@action inputFieldChanged(event) {
Copy link
Member

Choose a reason for hiding this comment

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

I do see issue

When you remove whitespace and calculate the new cursor position, you're using:

javascriptconst newCursorPosition = Math.min(
  nonWhitespaceBeforeCursor,
  sanitizedInput.length,
);

But nonWhitespaceBeforeCursor is the count of non-whitespace characters, not accounting for the actual position in the sanitized string. This can cause the cursor to jump incorrectly.
The Bug Scenario:
If the user types: "abc def" with cursor after 'd' (position 5):

textBeforeCursor = "abc d"
nonWhitespaceBeforeCursor = 4 (correct)
But the sanitization happens on the entire input asynchronously

The real issue is that you're setting the value and cursor position synchronously, but the value update might not be immediate, causing cursor position issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. The user cannot type "Space" in the input field.
  2. The DOM manipulation is only necessary if a user pastes text in the input directly.
  3. I still didn't completely understood the bug, because the final cursor position seems to be calculated correct.

const { onChange, currentStep } = this.args;
onChange(currentStep, value);

const rawValue = event.target.value;

if (/\s/.test(rawValue)) {
const cursorPosition = event.target.selectionStart;
const sanitizedInput = rawValue.replace(/\s/g, '');

const textBeforeCursor = rawValue.substring(0, cursorPosition);
const spacesBeforeCursor = (textBeforeCursor.match(/\s/g) || []).length;
const newCursorPosition = cursorPosition - spacesBeforeCursor;

event.target.value = sanitizedInput;
event.target.setSelectionRange(newCursorPosition, newCursorPosition);

onChange(currentStep, sanitizedInput);
} else {
onChange(currentStep, rawValue);
}
}

@action handleKeydown(event) {
if (/\s/.test(event.key)) {
event.preventDefault();
}
}
}
181 changes: 180 additions & 1 deletion tests/integration/components/new-signup/input-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'website-www/tests/helpers';
import { render } from '@ember/test-helpers';
import { render, triggerEvent, typeIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { NEW_SIGNUP_STEPS } from 'website-www/constants/new-signup';

Expand Down Expand Up @@ -136,4 +136,183 @@ module('Integration | Component | new-signup/input', function (hooks) {

assert.dom('[data-test-button="signup"]').isDisabled();
});

module('whitespace handling', function (hooks) {
hooks.beforeEach(function () {
this.setProperties({
currentStep: 'firstName',
onChange: (step, value) => {
this.inputValue = value;
},
onClick: () => {},
});
});

test('should prevent single space when typing', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');

await typeIn(input, 'John Doe');

assert
.dom(input)
.hasValue('JohnDoe', 'Single space should be prevented when typing');
});

test('should remove multiple consecutive spaces when typing', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');

await typeIn(input, 'John Doe');

assert
.dom(input)
.hasValue('JohnDoe', 'Multiple consecutive spaces should be removed');
});

test('should remove single space when pasting', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');
input.value = 'John Doe';
await triggerEvent(input, 'input');

assert.strictEqual(
this.inputValue,
'JohnDoe',
'Single space should be removed when pasting',
);
assert.dom(input).hasValue('JohnDoe');
});

test('should remove leading and trailing spaces when pasting', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');
input.value = ' John Doe ';
await triggerEvent(input, 'input');

assert.strictEqual(
this.inputValue,
'JohnDoe',
'Leading and trailing spaces should be removed',
);
assert.dom(input).hasValue('JohnDoe');
});

test('should handle input with only spaces', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');
input.value = ' ';
await triggerEvent(input, 'input');

assert.strictEqual(
this.inputValue,
'',
'Input with only spaces should result in empty string',
);
assert.dom(input).hasValue('');
});

test('should remove mixed whitespace characters when pasting', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');
input.value = 'John\t\nDoe';
await triggerEvent(input, 'input');

assert.strictEqual(
this.inputValue,
'JohnDoe',
'Tabs and newlines should be removed',
);
assert.dom(input).hasValue('JohnDoe');
});

test('should accept text without whitespace', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');

await typeIn(input, 'JohnDoe');

assert.strictEqual(
this.inputValue,
'JohnDoe',
'Text without whitespace should be accepted as-is',
);
assert.dom(input).hasValue('JohnDoe');
});

test('should handle combination of typing and pasting with whitespace', async function (assert) {
await render(hbs`
<NewSignup::Input
@currentStep={{this.currentStep}}
@onChange={{this.onChange}}
@onClick={{this.onClick}}
/>
`);

const input = this.element.querySelector('[data-test-signup-form-input]');

// First type some text
await typeIn(input, 'John ');

// Then paste text with spaces
input.value = input.value + ' Doe Smith';
await triggerEvent(input, 'input');

assert.strictEqual(
this.inputValue,
'JohnDoeSmith',
'Combination of typing and pasting should remove all spaces',
);
assert.dom(input).hasValue('JohnDoeSmith');
});
});
});