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

Add phone input component #1860

Merged
merged 20 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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: 2 additions & 0 deletions packages/boxel-ui/addon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"@embroider/addon-shim": "^1.8.9",
"@floating-ui/dom": "^1.6.3",
"@glint/template": "1.3.0",
"awesome-phonenumber": "^7.2.0",
"classnames": "^2.3.2",
"countries-list": "^3.1.1",
"dayjs": "^1.11.7",
"ember-basic-dropdown": "^8.0.0",
"ember-css-url": "^1.0.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/boxel-ui/addon/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import Modal from './components/modal/index.gts';
import BoxelMultiSelect, {
BoxelMultiSelectBasic,
} from './components/multi-select/index.gts';
import PhoneInput from './components/phone-input/index.gts';
import Pill from './components/pill/index.gts';
import ProgressBar from './components/progress-bar/index.gts';
import ProgressRadial from './components/progress-radial/index.gts';
Expand Down Expand Up @@ -92,6 +93,7 @@ export {
Menu,
Message,
Modal,
PhoneInput,
Pill,
ProgressBar,
ProgressRadial,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const Select: TemplateOnlyComponent<SelectAccessorySignature> =
@onChange={{@onChange}}
@onBlur={{@onBlur}}
@matchTriggerWidth={{@matchTriggerWidth}}
@selectedItemComponent={{@selectedItemComponent}}
data-test-boxel-input-group-select-accessory-trigger
...attributes
as |item|
Expand Down
171 changes: 171 additions & 0 deletions packages/boxel-ui/addon/src/components/phone-input/index.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import {
getCountryCodeForRegionCode,
getExample,
getSupportedRegionCodes,
parsePhoneNumber,
} from 'awesome-phonenumber';
import { type TCountryCode, countries, getEmojiFlag } from 'countries-list';
import { debounce } from 'lodash';

import { type InputValidationState } from '../input/index.gts';
import BoxelInputGroup from '../input-group/index.gts';

interface Signature {
Args: {
countryCode: string;
onCountryCodeChange: (code: string) => void;
onInput: (value: string) => void;
value: string;
};
Blocks: {
default: [];
};
Element: HTMLElement;
}

interface CountryInfo {
callingCode?: string;
code: string;
example?: {
callingCode: string;
nationalNumber: string;
};
flag?: string;
name?: string;
}

const getCountryInfo = (countryCode: string): CountryInfo | undefined => {
let example = getExample(countryCode);
let callingCode = getCountryCodeForRegionCode(countryCode);

let c = countries[countryCode as TCountryCode];
if (c === undefined) {
//here some country code may not be found due to the discrepancy between countries-list and libphonenumber-js library
//Only scenario where this is true is the usage of "AC"
//Most countries consider "AC" Ascension Island as part of "SH" Saint Helena
return;
}
return {
code: countryCode,
callingCode: callingCode.toString(),
name: c ? c.name : undefined,
flag: getEmojiFlag(countryCode as TCountryCode),
example: example
? {
callingCode: callingCode.toString(),
nationalNumber: example.number?.international ?? '',
}
: undefined,
};
};

class PhoneInput extends Component<Signature> {
@tracked items: Array<CountryInfo> = [];
@tracked selectedItem: CountryInfo = getCountryInfo('US')!;
@tracked validationState: InputValidationState = 'initial';
@tracked input: string = this.args.value ?? '';

@action onSelectItem(item: CountryInfo): void {
this.selectedItem = item;
if (this.args.onCountryCodeChange) {
this.args.onCountryCodeChange(item.callingCode ?? '');
}
if (this.input.length > 0) {
const parsedPhoneNumber = parsePhoneNumber(this.input, {
regionCode: this.selectedItem.code,
});
this.validationState = parsedPhoneNumber.valid ? 'valid' : 'invalid';
}
}

constructor(owner: unknown, args: Signature['Args']) {
super(owner, args);
this.items = getSupportedRegionCodes()
.map((code) => {
return getCountryInfo(code);
})
.filter((c) => c !== undefined) as CountryInfo[];

if (this.args.countryCode) {
this.selectedItem = this.items.find(
(item) => item.callingCode === this.args.countryCode,
)!;
}
}

get placeholder(): string | undefined {
if (this.selectedItem) {
return this.selectedItem.example?.nationalNumber;
}
return undefined;
}

@action onInput(v: string): void {
this.debouncedInput(v);
}

private debouncedInput = debounce((input: string) => {
this.input = input;

if (input === '') {
this.validationState = 'initial';
return;
}

const parsedPhoneNumber = parsePhoneNumber(input, {
regionCode: this.selectedItem.code,
});
this.validationState = parsedPhoneNumber.valid ? 'valid' : 'invalid';
//save when the state is valid
if (this.validationState === 'valid') {
this.args.onInput(this.input);
}
}, 300);

<template>
<BoxelInputGroup
@placeholder={{this.placeholder}}
@state={{this.validationState}}
@onInput={{this.onInput}}
@value={{this.input}}
>
<:before as |Accessories|>
<Accessories.Select
@placeholder={{this.placeholder}}
@selected={{this.selectedItem}}
@onChange={{this.onSelectItem}}
@options={{this.items}}
@selectedItemComponent={{PhoneSelectedItem}}
@searchEnabled={{true}}
@searchField='name'
@matchTriggerWidth={{false}}
aria-label='Select an country calling code'
as |item|
>
<div>{{item.flag}} {{item.name}} +{{item.callingCode}}</div>
</Accessories.Select>
</:before>
</BoxelInputGroup>
</template>
}

export interface SelectedItemSignature {
Args: {
option: any;
};
Element: HTMLDivElement;
}

class PhoneSelectedItem extends Component<SelectedItemSignature> {
<template>
<div>
{{@option.flag}}
+{{@option.callingCode}}
</div>
</template>
}

export default PhoneInput;
39 changes: 39 additions & 0 deletions packages/boxel-ui/addon/src/components/phone-input/usage.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import FreestyleUsage from 'ember-freestyle/components/freestyle/usage';

import PhoneInput from './index.gts';

export default class PhoneInputUsage extends Component {
@tracked value = '';
@tracked countryCode = '';

@action onInput(value: string): void {
this.value = value;
}

@action onCountryCodeChange(code: string): void {
this.countryCode = code;
}

<template>
<FreestyleUsage @name='PhoneInput'>
<:description>
<p>
PhoneInput is a component that allows users to input phone numbers
with a dropdown select of country code and validation of the inputted
numbers
</p>
</:description>
<:example>
<PhoneInput
@value={{this.value}}
@countryCode={{this.countryCode}}
@onInput={{this.onInput}}
@onCountryCodeChange={{this.onCountryCodeChange}}
/>
</:example>
</FreestyleUsage>
</template>
}
2 changes: 2 additions & 0 deletions packages/boxel-ui/addon/src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import MenuUsage from './components/menu/usage.gts';
import MessageUsage from './components/message/usage.gts';
import ModalUsage from './components/modal/usage.gts';
import MultiSelectUsage from './components/multi-select/usage.gts';
import PhoneInputUsage from './components/phone-input/usage.gts';
import PillUsage from './components/pill/usage.gts';
import ProgressBarUsage from './components/progress-bar/usage.gts';
import ProgressRadialUsage from './components/progress-radial/usage.gts';
Expand Down Expand Up @@ -64,6 +65,7 @@ export const ALL_USAGE_COMPONENTS = [
['Message', MessageUsage],
['Modal', ModalUsage],
['MultiSelect', MultiSelectUsage],
['PhoneInput', PhoneInputUsage],
['Pill', PillUsage],
['ProgressBar', ProgressBarUsage],
['ProgressRadial', ProgressRadialUsage],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"data": {
"type": "card",
"attributes": {
"contactPhone": {
"value": "01355889283",
"type": {
"index": 2,
"label": "Work"
}
},
"title": null,
"description": null,
"thumbnailURL": null
},
"meta": {
"adoptsFrom": {
"module": "../phone-number",
"name": "CardWithContactPhoneNumber"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
"primaryEmail": "[email protected]",
"secondaryEmail": "[email protected]",
"phoneMobile": {
"type": "office",
"country": 1,
"area": 415,
"number": 123456
"phoneNumber": {
"number": "1158524828",
"countryCode": "60"
},
"type": {
"index": 0,
"label": "Mobile"
}
},
"phoneOffice": {
"type": null,
"country": null,
"area": null,
"number": null
"phoneNumber": {
"number": null,
"countryCode": null
},
"type": {
"index": null,
"label": null
}
},
"socialLinks": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@
"[email protected]"
],
"phone": {
"type": null,
"country": null,
"area": null,
"number": null
"number": "1159292211",
"countryCode": "60"
},
"percentage": 100.159393,
"currency": {
Expand Down Expand Up @@ -105,4 +103,4 @@
}
}
}
}
}
6 changes: 3 additions & 3 deletions packages/experiments-realm/crm/contact.gts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import StringField from 'https://cardstack.com/base/string';
import { PhoneField } from '../phone';
import { ContactPhoneNumber } from '../phone-number';
import { EmailField } from '../email';
import { ContactLinkField } from '../fields/contact-link';
import {
Expand Down Expand Up @@ -701,8 +701,8 @@ export class Contact extends CardDef {
@field department = contains(StringField);
@field primaryEmail = contains(EmailField);
@field secondaryEmail = contains(EmailField);
@field phoneMobile = contains(PhoneField);
@field phoneOffice = contains(PhoneField);
@field phoneMobile = contains(ContactPhoneNumber);
@field phoneOffice = contains(ContactPhoneNumber);
@field socialLinks = containsMany(SocialLinkField);
@field statusTag = contains(StatusTagField); //this is an empty field that gets computed in subclasses

Expand Down
2 changes: 1 addition & 1 deletion packages/experiments-realm/experiments_fields_preview.gts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FeaturedImageField } from './fields/featured-image';
import { ContactLinkField } from './fields/contact-link';
import { EmailField } from './email';
import { PhoneField } from './phone';
import { PhoneField } from './phone-number';
import { UrlField } from './url';
import { WebsiteField } from './website';
import { Address as AddressField } from './address';
Expand Down
Loading
Loading