Skip to content

Commit 6288707

Browse files
authoredFeb 26, 2025
Merge pull request #477 from upfluence/mb/DRA-2290
Updated: PhoneNumberInput component now handles paste events
2 parents 1468fc3 + a98e157 commit 6288707

File tree

4 files changed

+83
-10
lines changed

4 files changed

+83
-10
lines changed
 

‎addon/components/o-s-s/currency-input.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface OSSCurrencyInputArgs {
2222
}
2323

2424
const NUMERIC_ONLY = /^\d$/i;
25-
const NOT_NUMERIC_FLOAT = /[^0-9,.]/g;
25+
export const NOT_NUMERIC_FLOAT = /[^\d,.]/g;
2626
export const PLATFORM_CURRENCIES: Currency[] = [
2727
{ code: 'USD', symbol: '$' },
2828
{ code: 'EUR', symbol: '€' },

‎addon/components/o-s-s/phone-number-input.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
name="telephone"
1414
placeholder={{this.placeholder}}
1515
{{on "keydown" this.onlyNumeric}}
16+
{{on "paste" this.handlePaste}}
1617
{{on "blur" this.onlyNumeric}}
1718
{{did-insert this.registerInputElement}}
1819
/>

‎addon/components/o-s-s/phone-number-input.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Component from '@glimmer/component';
66
import { tracked } from '@glimmer/tracking';
77
import { countries, type CountryData } from '@upfluence/oss-components/utils/country-codes';
88
import type IntlService from 'ember-intl/services/intl';
9+
import { NOT_NUMERIC_FLOAT } from './currency-input';
910

1011
interface OSSPhoneNumberInputArgs {
1112
prefix: string;
@@ -17,6 +18,9 @@ interface OSSPhoneNumberInputArgs {
1718
validates?(isPassing: boolean): void;
1819
}
1920

21+
const AUTHORIZED_KEYS = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift'];
22+
const AUTHORIZED_COMBO_KEYS = ['v', 'a', 'z', 'c', 'x'];
23+
2024
export default class OSSPhoneNumberInput extends Component<OSSPhoneNumberInputArgs> {
2125
@service declare intl: IntlService;
2226

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

6872
@action
6973
onlyNumeric(event: KeyboardEvent | FocusEvent): void {
70-
const authorizedInputs = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift'];
71-
72-
if (
73-
event instanceof FocusEvent ||
74-
/^[0-9]$/i.test(event.key) ||
75-
authorizedInputs.find((key: string) => key === event.key)
76-
) {
74+
const isAuthorizedKey = AUTHORIZED_KEYS.find((key: string) => key === (event as KeyboardEvent).key);
75+
const isSupportedCombo =
76+
event instanceof KeyboardEvent &&
77+
((event as KeyboardEvent).metaKey ||
78+
((navigator as any).userAgentData?.platform === 'Windows' && event.ctrlKey)) &&
79+
AUTHORIZED_COMBO_KEYS.includes(event.key);
80+
81+
if (event instanceof FocusEvent || /^[0-9]$/i.test(event.key) || isSupportedCombo || isAuthorizedKey) {
7782
this.args.onChange('+' + this.selectedCountry.countryCallingCodes[0], this.args.number);
7883
} else {
7984
event.preventDefault();
@@ -82,6 +87,21 @@ export default class OSSPhoneNumberInput extends Component<OSSPhoneNumberInputAr
8287
this.validateInput();
8388
}
8489

90+
@action
91+
handlePaste(event: ClipboardEvent): void {
92+
event.preventDefault();
93+
94+
const paste = (event.clipboardData?.getData('text') ?? '').replace(NOT_NUMERIC_FLOAT, '');
95+
const target = event.target as HTMLInputElement;
96+
const initialSelectionStart = target.selectionStart ?? 0;
97+
const finalSelectionPosition = initialSelectionStart + paste.length;
98+
99+
target.setRangeText(paste, initialSelectionStart, target.selectionEnd ?? initialSelectionStart);
100+
target.setSelectionRange(finalSelectionPosition, finalSelectionPosition);
101+
102+
this.args.onChange('+' + this.selectedCountry.countryCallingCodes[0], target.value);
103+
}
104+
85105
@action
86106
onSearch(keyword: any): void {
87107
this.filteredCountries = this._countries.filter((country: any) => {

‎tests/integration/components/o-s-s/phone-number-input-test.ts

+54-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { hbs } from 'ember-cli-htmlbars';
22
import { module, test } from 'qunit';
33
import { setupRenderingTest } from 'ember-qunit';
4-
import { render, setupOnerror, triggerKeyEvent } from '@ember/test-helpers';
4+
import { render, setupOnerror, triggerEvent, triggerKeyEvent } from '@ember/test-helpers';
55
import click from '@ember/test-helpers/dom/click';
66
import sinon from 'sinon';
77
import findAll from '@ember/test-helpers/dom/find-all';
88
import typeIn from '@ember/test-helpers/dom/type-in';
99
import settled from '@ember/test-helpers/settled';
1010

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

1414
hooks.beforeEach(function () {
@@ -115,6 +115,58 @@ module('Integration | Component | o-s-s/phone-number', function (hooks) {
115115
});
116116
});
117117

118+
module('When the paste event is received', function (hooks) {
119+
hooks.beforeEach(function () {
120+
this.onChange = () => {};
121+
this.onValidation = sinon.spy();
122+
this.number = '1234567890';
123+
});
124+
125+
test('The value stored in the clipboard is inserted in the input', async function (assert) {
126+
await render(
127+
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
128+
);
129+
assert.dom('input').hasValue('1234567890');
130+
await triggerEvent('input', 'paste', {
131+
clipboardData: {
132+
getData: sinon.stub().returns('123')
133+
}
134+
});
135+
136+
assert.dom('input').hasValue('1234567890123');
137+
});
138+
139+
test('The non-numeric characters are escaped', async function (assert) {
140+
await render(
141+
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
142+
);
143+
assert.dom('input').hasValue('1234567890');
144+
await triggerEvent('input', 'paste', {
145+
clipboardData: {
146+
getData: sinon.stub().returns('1withletter0')
147+
}
148+
});
149+
150+
assert.dom('input').hasValue('123456789010');
151+
});
152+
153+
test('When selection is applied, it replaces the selection', async function (assert) {
154+
await render(
155+
hbs`<OSS::PhoneNumberInput @prefix="" @number={{this.number}} @onChange={{this.onChange}} @validates={{this.onValidation}} />`
156+
);
157+
assert.dom('input').hasValue('1234567890');
158+
let input = document.querySelector('input.ember-text-field') as HTMLInputElement;
159+
input.setSelectionRange(4, 6);
160+
await triggerEvent('input', 'paste', {
161+
clipboardData: {
162+
getData: sinon.stub().returns('0')
163+
}
164+
});
165+
166+
assert.dom('input').hasValue('123407890');
167+
});
168+
});
169+
118170
module('@hasError parameter', () => {
119171
test('A red border is displayed if the parameter is true', async function (assert) {
120172
await render(hbs`<OSS::PhoneNumberInput @prefix="" @number="" @hasError={{true}}

0 commit comments

Comments
 (0)