Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated: PhoneNumberInput component now handles paste events #477

Merged
merged 1 commit into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addon/components/o-s-s/currency-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface OSSCurrencyInputArgs {
}

const NUMERIC_ONLY = /^\d$/i;
const NOT_NUMERIC_FLOAT = /[^0-9,.]/g;
export const NOT_NUMERIC_FLOAT = /[^\d,.]/g;
export const PLATFORM_CURRENCIES: Currency[] = [
{ code: 'USD', symbol: '$' },
{ code: 'EUR', symbol: '€' },
Expand Down
1 change: 1 addition & 0 deletions addon/components/o-s-s/phone-number-input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
name="telephone"
placeholder={{this.placeholder}}
{{on "keydown" this.onlyNumeric}}
{{on "paste" this.handlePaste}}
{{on "blur" this.onlyNumeric}}
{{did-insert this.registerInputElement}}
/>
Expand Down
34 changes: 27 additions & 7 deletions addon/components/o-s-s/phone-number-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { countries, type CountryData } from '@upfluence/oss-components/utils/country-codes';
import type IntlService from 'ember-intl/services/intl';
import { NOT_NUMERIC_FLOAT } from './currency-input';

interface OSSPhoneNumberInputArgs {
prefix: string;
Expand All @@ -17,6 +18,9 @@ interface OSSPhoneNumberInputArgs {
validates?(isPassing: boolean): void;
}

const AUTHORIZED_KEYS = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift'];
const AUTHORIZED_COMBO_KEYS = ['v', 'a', 'z', 'c', 'x'];

export default class OSSPhoneNumberInput extends Component<OSSPhoneNumberInputArgs> {
@service declare intl: IntlService;

Expand Down Expand Up @@ -67,13 +71,14 @@ export default class OSSPhoneNumberInput extends Component<OSSPhoneNumberInputAr

@action
onlyNumeric(event: KeyboardEvent | FocusEvent): void {
const authorizedInputs = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift'];

if (
event instanceof FocusEvent ||
/^[0-9]$/i.test(event.key) ||
authorizedInputs.find((key: string) => key === event.key)
) {
const isAuthorizedKey = AUTHORIZED_KEYS.find((key: string) => key === (event as KeyboardEvent).key);
const isSupportedCombo =
event instanceof KeyboardEvent &&
((event as KeyboardEvent).metaKey ||
((navigator as any).userAgentData?.platform === 'Windows' && event.ctrlKey)) &&
AUTHORIZED_COMBO_KEYS.includes(event.key);

if (event instanceof FocusEvent || /^[0-9]$/i.test(event.key) || isSupportedCombo || isAuthorizedKey) {
this.args.onChange('+' + this.selectedCountry.countryCallingCodes[0], this.args.number);
} else {
event.preventDefault();
Expand All @@ -82,6 +87,21 @@ export default class OSSPhoneNumberInput extends Component<OSSPhoneNumberInputAr
this.validateInput();
}

@action
handlePaste(event: ClipboardEvent): void {
event.preventDefault();

const paste = (event.clipboardData?.getData('text') ?? '').replace(NOT_NUMERIC_FLOAT, '');
const target = event.target as HTMLInputElement;
const initialSelectionStart = target.selectionStart ?? 0;
const finalSelectionPosition = initialSelectionStart + paste.length;

target.setRangeText(paste, initialSelectionStart, target.selectionEnd ?? initialSelectionStart);
target.setSelectionRange(finalSelectionPosition, finalSelectionPosition);

this.args.onChange('+' + this.selectedCountry.countryCallingCodes[0], target.value);
}

@action
onSearch(keyword: any): void {
this.filteredCountries = this._countries.filter((country: any) => {
Expand Down
56 changes: 54 additions & 2 deletions tests/integration/components/o-s-s/phone-number-input-test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { hbs } from 'ember-cli-htmlbars';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, setupOnerror, triggerKeyEvent } from '@ember/test-helpers';
import { render, setupOnerror, triggerEvent, triggerKeyEvent } from '@ember/test-helpers';
import click from '@ember/test-helpers/dom/click';
import sinon from 'sinon';
import findAll from '@ember/test-helpers/dom/find-all';
import typeIn from '@ember/test-helpers/dom/type-in';
import settled from '@ember/test-helpers/settled';

module('Integration | Component | o-s-s/phone-number', function (hooks) {
module('Integration | Component | o-s-s/phone-number-input', function (hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(function () {
Expand Down Expand Up @@ -115,6 +115,58 @@ module('Integration | Component | o-s-s/phone-number', function (hooks) {
});
});

module('When the paste event is received', function (hooks) {
hooks.beforeEach(function () {
this.onChange = () => {};
this.onValidation = sinon.spy();
this.number = '1234567890';
});

test('The value stored in the clipboard is inserted in the input', async function (assert) {
await render(
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
);
assert.dom('input').hasValue('1234567890');
await triggerEvent('input', 'paste', {
clipboardData: {
getData: sinon.stub().returns('123')
}
});

assert.dom('input').hasValue('1234567890123');
});

test('The non-numeric characters are escaped', async function (assert) {
await render(
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
);
assert.dom('input').hasValue('1234567890');
await triggerEvent('input', 'paste', {
clipboardData: {
getData: sinon.stub().returns('1withletter0')
}
});

assert.dom('input').hasValue('123456789010');
});

test('When selection is applied, it replaces the selection', async function (assert) {
await render(
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
);
assert.dom('input').hasValue('1234567890');
let input = document.querySelector('input.ember-text-field') as HTMLInputElement;
input.setSelectionRange(4, 6);
await triggerEvent('input', 'paste', {
clipboardData: {
getData: sinon.stub().returns('0')
}
});

assert.dom('input').hasValue('123407890');
});
});

module('@hasError parameter', () => {
test('A red border is displayed if the parameter is true', async function (assert) {
await render(hbs`<OSS::PhoneNumberInput @prefix="" @number="" @hasError={{true}}
Expand Down
Loading