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

OSS::PhoneNumberInput: better support for keyboard shortcuts #394

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions addon/components/o-s-s/currency-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface OSSCurrencyInputArgs {
}

const NUMERIC_ONLY = /^\d$/i;
const NOT_NUMERIC_FLOAT = /[^0-9,.]/g;
export const NUMERIC_FLOAT = /[^0-9,.]/g;
export const PLATFORM_CURRENCIES: Currency[] = [
{ code: 'USD', symbol: '$' },
{ code: 'EUR', symbol: '€' },
Expand Down Expand Up @@ -122,7 +122,7 @@ export default class OSSCurrencyInput extends Component<OSSCurrencyInputArgs> {
handlePaste(event: ClipboardEvent): void {
event.preventDefault();

const paste = (event.clipboardData?.getData('text') ?? '').replace(NOT_NUMERIC_FLOAT, '');
const paste = (event.clipboardData?.getData('text') ?? '').replace(NUMERIC_FLOAT, '');
const target = event.target as HTMLInputElement;
const initialSelectionStart = target.selectionStart ?? 0;
const finalSelectionPosition = initialSelectionStart + paste.length;
Expand Down
8 changes: 6 additions & 2 deletions addon/components/o-s-s/infinite-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';
import { action } from '@ember/object';

import { guidFor } from '@ember/object/internals';
import { inject as service } from '@ember/service';

import type { IntlService } from 'ember-intl';

interface InfiniteSelectArgs {
searchEnabled: boolean;
Expand All @@ -29,6 +31,8 @@ type InfinityItem = {
const DEFAULT_ITEM_LABEL = 'name';

export default class OSSInfiniteSelect extends Component<InfiniteSelectArgs> {
@service declare intl: IntlService;

@tracked _searchKeyword: string = '';
@tracked _focusElement: number = 0;

Expand All @@ -54,7 +58,7 @@ export default class OSSInfiniteSelect extends Component<InfiniteSelectArgs> {
}

get searchPlaceholder(): string {
return this.args.searchPlaceholder ?? 'Search...';
return this.args.searchPlaceholder ?? this.intl.t('oss-components.infinite-select.search.placeholder');
}

get itemLabel(): string {
Expand Down
30 changes: 22 additions & 8 deletions addon/components/o-s-s/phone-number-input.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
<div class="phone-number-container fx-1" ...attributes>
<div class="phone-number-input {{if this.countrySelectorShown 'phone-number-input--active'}} fx-row fx-1 fx-xalign-center">
<div
class="phone-number-input
{{if this.countrySelectorShown 'phone-number-input--active'}}
fx-row fx-1 fx-xalign-center"
>
<div class="country-selector fx-row" role="button" {{on "click" this.toggleCountrySelector}}>
<div class="fflag fflag-{{this.selectedCountry.id}} ff-sm ff-round"></div>
<OSS::Icon @icon="fa-chevron-{{if this.countrySelectorShown 'up' 'down'}}" />
</div>
<div class="fx-1 fx-row upf-input" {{on "click" this.focusInput}}>
<span class="fx-row fx-xalign-center phone-prefix">{{@prefix}}</span>
<Input class="fx-1" type="tel" @value={{@number}} placeholder={{this.placeholder}}
{{on "keydown" this.onlyNumeric}} {{on "blur" this.onlyNumeric}} {{did-insert this.registerInputElement}} />
<Input
class="fx-1"
type="tel"
@value={{@number}}
placeholder={{this.placeholder}}
{{on "keydown" this.onlyNumeric}}
{{on "paste" this.handlePaste}}
{{on "blur" this.onlyNumeric}}
{{did-insert this.registerInputElement}}
/>
</div>

</div>
Expand All @@ -19,10 +31,12 @@
{{/if}}

{{#if this.countrySelectorShown}}
<OSS::InfiniteSelect @items={{this.filteredCountries}} @onSearch={{this.onSearch}}
@onSelect={{this.onSelect}}
@searchPlaceholder="Search"
{{on-click-outside this.hideCountrySelector}}>
<OSS::InfiniteSelect
@items={{this.filteredCountries}}
@onSearch={{this.onSearch}}
@onSelect={{this.onSelect}}
{{on-click-outside this.hideCountrySelector}}
>
<:option as |country|>
<div class="fx-row fx-xalign-center {{if (eq this.selectedCountry country) 'row-selected'}}">
<div class="fflag fflag-{{country.id}} ff-sm ff-rounded"></div>
Expand All @@ -35,4 +49,4 @@
</:option>
</OSS::InfiniteSelect>
{{/if}}
</div>
</div>
33 changes: 26 additions & 7 deletions addon/components/o-s-s/phone-number-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
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 { countries, type CountryData } from '@upfluence/oss-components/utils/country-codes';
import { NUMERIC_FLOAT } from './currency-input';

interface OSSPhoneNumberInputArgs {
prefix: string;
number: string;
Expand Down Expand Up @@ -69,12 +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 = authorizedInputs.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)) &&
['v', 'a', 'z', 'c'].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 @@ -83,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(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: 55 additions & 1 deletion tests/integration/components/o-s-s/phone-number-input-test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
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';
import { setupIntl } from 'ember-intl/test-support';

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

test('it renders', async function (assert) {
this.onChange = () => {};
Expand Down Expand Up @@ -122,6 +124,58 @@ module('Integration | Component | o-s-s/phone-number', function (hooks) {
assert.ok(this.onValidation.calledWithExactly(false));
assert.dom('.font-color-error-500').exists();
});

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('Error management', function () {
Expand Down
2 changes: 2 additions & 0 deletions translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ oss-components:
description: Try adjusting your search to find what you’re looking for.
empty: Nothing to see here.
empty_img_alt: Empty content
search:
placeholder: Search...
password-input:
validators:
uppercase: Uppercase
Expand Down
Loading