-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(phone-number): implement component
- Loading branch information
Showing
50 changed files
with
3,548 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,7 @@ const componentNames = [ | |
'tabs', | ||
'switch', | ||
'accordion', | ||
'phone-number', | ||
//--generator-anchor-- | ||
]; | ||
|
||
|
21 changes: 21 additions & 0 deletions
21
packages/ods/react/tests/_app/src/components/ods-phone-number.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react-dom/client'; | ||
import { OdsPhoneNumber } from 'ods-components-react'; | ||
|
||
const PhoneNumber = () => { | ||
function onOdsChange() { | ||
console.log('React phone number odsChange'); | ||
} | ||
|
||
return ( | ||
<> | ||
<OdsPhoneNumber name="ods-phone-number" | ||
onOdsChange={ onOdsChange } /> | ||
|
||
<OdsPhoneNumber isDisabled | ||
name="ods-phone-number-disabled" | ||
onOdsChange={ onOdsChange } /> | ||
</> | ||
); | ||
}; | ||
|
||
export default PhoneNumber; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import type { Page } from 'puppeteer'; | ||
import { goToComponentPage, setupBrowser } from '../setup'; | ||
|
||
describe('ods-phone-number react', () => { | ||
const setup = setupBrowser(); | ||
let page: Page; | ||
|
||
beforeAll(async () => { | ||
page = setup().page; | ||
}); | ||
|
||
beforeEach(async () => { | ||
await goToComponentPage(page, 'ods-phone-number'); | ||
}); | ||
|
||
it('render the component correctly', async () => { | ||
const elem = await page.$('ods-phone-number'); | ||
const boundingBox = await elem?.boundingBox(); | ||
|
||
expect(boundingBox?.height).toBeGreaterThan(0); | ||
expect(boundingBox?.width).toBeGreaterThan(0); | ||
}); | ||
|
||
it('trigger the odsChange handler on type', async () => { | ||
const elem = await page.$('ods-phone-number:not([is-disabled]) >>> input'); | ||
let consoleLog = ''; | ||
page.on('console', (consoleObj) => { | ||
consoleLog = consoleObj.text(); | ||
}); | ||
|
||
await elem?.type('a'); | ||
// Small delay to ensure page console event has been resolved | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
||
expect(consoleLog).toBe('React phone number odsChange'); | ||
}); | ||
|
||
it('does not trigger the odsChange handler on type if disabled', async () => { | ||
const elem = await page.$('ods-phone-number[is-disabled] >>> input'); | ||
let consoleLog = ''; | ||
page.on('console', (consoleObj) => { | ||
consoleLog = consoleObj.text(); | ||
}); | ||
|
||
await elem?.type('a'); | ||
// Small delay to ensure page console event has been resolved | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
||
expect(consoleLog).toBe(''); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Local Stencil command generates external ods component build at the root of the project | ||
# Excluding them is a temporary solution to avoid pushing generated files | ||
# But the issue may cause main build (ods-component package) to fails, as it detects multiples occurences | ||
# of the same component and thus you have to delete all those generated dir manually | ||
*/src/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# How-to generate flag spritesheet | ||
|
||
For now, we use https://github.com/flarik/css-sprite-flags. | ||
|
||
You will need to add those last 7 that are missing in the repo css: | ||
|
||
```scss | ||
'wf': -0px -360px, | ||
'ws': -24px -360px, | ||
'ye': -48px -360px, | ||
'yt': -72px -360px, | ||
'za': -96px -360px, | ||
'zm': -120px -360px, | ||
'zw': -144px -360px, | ||
``` | ||
|
||
If we need to update the flag list to a custom one, please describe precisely | ||
here the workflow used to generate such files. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "@ovhcloud/ods-component-phone-number", | ||
"version": "17.1.0", | ||
"private": true, | ||
"description": "ODS PhoneNumber component", | ||
"main": "dist/index.cjs.js", | ||
"collection": "dist/collection/collection-manifest.json", | ||
"scripts": { | ||
"clean": "rimraf .stencil coverage dist docs-api www", | ||
"doc": "typedoc --pretty --plugin ../../../scripts/typedoc-plugin-decorator.js && node ../../../scripts/generate-typedoc-md.js", | ||
"lint:scss": "stylelint 'src/components/**/*.scss'", | ||
"lint:ts": "eslint '{src,tests}/**/*.{js,ts,tsx}'", | ||
"start": "stencil build --dev --watch --serve", | ||
"test:e2e": "stencil test --e2e --config stencil.config.ts", | ||
"test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --runInBand --config stencil.config.ts", | ||
"test:spec": "stencil test --spec --config stencil.config.ts --coverage", | ||
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --coverage" | ||
} | ||
} |
Binary file added
BIN
+62.8 KB
packages/ods/src/components/phone-number/src/assets/sprite-flags-24x24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
247 changes: 247 additions & 0 deletions
247
packages/ods/src/components/phone-number/src/assets/sprite-flags-24x24.scss
Large diffs are not rendered by default.
Oops, something went wrong.
41 changes: 41 additions & 0 deletions
41
...ges/ods/src/components/phone-number/src/components/ods-phone-number/ods-phone-number.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
@import '../../assets/sprite-flags-24x24'; | ||
|
||
:host(.ods-phone-number) { | ||
display: inline-flex; | ||
} | ||
|
||
.ods-phone-number { | ||
&__iso-codes { | ||
min-width: 64px; | ||
|
||
&::part(select) { | ||
border-top-right-radius: 0; | ||
border-bottom-right-radius: 0; | ||
} | ||
|
||
&::part(flag) { | ||
display: inline-block; | ||
background-image: $sprite-flags-background-image; | ||
background-repeat: no-repeat; | ||
width: 24px; | ||
height: 24px; | ||
} | ||
|
||
@each $iso-code, $background-position in $sprite-flags-background-position { | ||
&::part(flag-#{$iso-code}) { | ||
background-position: $background-position; | ||
} | ||
} | ||
} | ||
|
||
&__input { | ||
&--sibling { | ||
margin-left: -1px; // to merge input and select borders | ||
|
||
&::part(input) { | ||
border-top-left-radius: 0; | ||
border-bottom-left-radius: 0; | ||
} | ||
} | ||
} | ||
} |
192 changes: 192 additions & 0 deletions
192
...ages/ods/src/components/phone-number/src/components/ods-phone-number/ods-phone-number.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { AttachInternals, Component, Event, type EventEmitter, type FunctionalComponent, Host, Method, Prop, Watch, h } from '@stencil/core'; | ||
import { PhoneNumberUtil } from 'google-libphonenumber'; | ||
import { type OdsInput, type OdsInputValueChangeEvent } from '../../../../input/src'; | ||
import { type OdsSelectCustomRendererData, type OdsSelectEventChange } from '../../../../select/src'; | ||
import { type OdsPhoneNumberCountryIsoCode } from '../../constants/phone-number-country-iso-code'; | ||
import { type OdsPhoneNumberCountryPreset } from '../../constants/phone-number-country-preset'; | ||
import { type OdsPhoneNumberLocale } from '../../constants/phone-number-locale'; | ||
import { type TranslatedCountryMap, formatPhoneNumber, getCurrentIsoCode, getCurrentLocale, getNationalPhoneNumberExample, getTranslatedCountryMap, getValidityState, isValidPhoneNumber, parseCountries, setFormValue, sortCountriesByName } from '../../controller/ods-phone-number'; | ||
import { type OdsPhoneNumberValueChangeEventDetail } from '../../interfaces/events'; | ||
|
||
@Component({ | ||
formAssociated: true, | ||
shadow: true, | ||
styleUrl: 'ods-phone-number.scss', | ||
tag: 'ods-phone-number', | ||
}) | ||
export class OdsPhoneNumber { | ||
private hasCountries: boolean = false; | ||
private i18nCountriesMap?: TranslatedCountryMap; | ||
private inputElement?: OdsInput; | ||
private parsedCountryCodes: OdsPhoneNumberCountryIsoCode[] = []; | ||
private phoneUtils = PhoneNumberUtil.getInstance(); | ||
|
||
@AttachInternals() private internals!: ElementInternals; | ||
|
||
@Prop({ reflect: true }) public ariaLabel: HTMLElement['ariaLabel'] = null; | ||
@Prop({ reflect: true }) public ariaLabelledby?: string; | ||
@Prop({ reflect: true }) public countries?: OdsPhoneNumberCountryIsoCode[] | OdsPhoneNumberCountryPreset | string; | ||
@Prop({ reflect: true }) public defaultValue?: string; | ||
@Prop({ mutable: true, reflect: true }) public hasError: boolean = false; | ||
@Prop({ reflect: true }) public isClearable: boolean = false; | ||
@Prop({ reflect: true }) public isDisabled: boolean = false; | ||
@Prop({ reflect: true }) public isLoading: boolean = false; | ||
@Prop({ reflect: true }) public isReadonly: boolean = false; | ||
@Prop({ reflect: true }) public isRequired: boolean = false; | ||
@Prop({ mutable: true, reflect: true }) public isoCode?: OdsPhoneNumberCountryIsoCode; | ||
@Prop({ mutable: true, reflect: true }) public locale?: OdsPhoneNumberLocale; | ||
@Prop({ reflect: true }) public name!: string; | ||
@Prop({ reflect: true }) public pattern?: string; | ||
@Prop({ mutable: true, reflect: true }) public value: string | null = null; | ||
|
||
@Event() odsBlur!: EventEmitter<void>; | ||
@Event() odsChange!: EventEmitter<OdsPhoneNumberValueChangeEventDetail>; | ||
@Event() odsClear!: EventEmitter<void>; | ||
@Event() odsFocus!: EventEmitter<void>; | ||
@Event() odsReset!: EventEmitter<void>; | ||
|
||
@Method() | ||
async clear(): Promise<void> { | ||
return this.inputElement?.clear(); | ||
} | ||
|
||
@Method() | ||
async getValidity(): Promise<ValidityState | undefined> { | ||
const inputValidity = await this.inputElement?.getValidity(); | ||
return getValidityState(this.hasError, inputValidity); | ||
} | ||
|
||
@Method() | ||
async reset(): Promise<void> { | ||
return this.inputElement?.reset(); | ||
} | ||
|
||
@Watch('countries') | ||
onCountriesChange(): void { | ||
this.parsedCountryCodes = parseCountries(this.countries, this.phoneUtils) || []; | ||
this.hasCountries = !!this.parsedCountryCodes?.length; | ||
} | ||
|
||
@Watch('isoCode') | ||
onIsoCodeChange(): void { | ||
this.value = ''; | ||
this.hasError = false; | ||
} | ||
|
||
@Watch('locale') | ||
onLocaleChange(locale: OdsPhoneNumberLocale): void { | ||
this.i18nCountriesMap = getTranslatedCountryMap(locale, this.phoneUtils); | ||
this.parsedCountryCodes = sortCountriesByName(this.parsedCountryCodes, this.i18nCountriesMap); | ||
} | ||
|
||
componentWillLoad(): void { | ||
this.onCountriesChange(); | ||
this.isoCode = getCurrentIsoCode(this.isoCode, this.parsedCountryCodes[0]); | ||
this.locale = getCurrentLocale(this.locale); | ||
this.onLocaleChange(this.locale); | ||
|
||
if (this.value) { | ||
this.onInputChange(new CustomEvent('', { | ||
detail: { | ||
name: this.name, | ||
value: this.value, | ||
}, | ||
})); | ||
} | ||
} | ||
|
||
async formResetCallback(): Promise<void> { | ||
await this.reset(); | ||
} | ||
|
||
private getPlaceholder(): string { | ||
return getNationalPhoneNumberExample(this.isoCode, this.phoneUtils); | ||
} | ||
|
||
private onInputChange(event: OdsInputValueChangeEvent): void { | ||
event.stopImmediatePropagation(); | ||
|
||
this.value = event.detail.value?.toString() ?? null; | ||
this.hasError = !isValidPhoneNumber(this.value, this.isoCode, this.phoneUtils); | ||
|
||
const formattedValue = formatPhoneNumber(this.value, this.hasError, this.isoCode, this.phoneUtils); | ||
setFormValue(this.internals, formattedValue); | ||
|
||
this.odsChange.emit({ | ||
isoCode: this.isoCode, | ||
name: this.name, | ||
validity: getValidityState(this.hasError, event.detail.validity), | ||
value: formattedValue, | ||
}); | ||
} | ||
|
||
private onSelectChange(event: OdsSelectEventChange): void { | ||
this.isoCode = event.detail.value as OdsPhoneNumberCountryIsoCode; | ||
} | ||
|
||
render(): FunctionalComponent { | ||
return ( | ||
<Host class="ods-phone-number"> | ||
{ | ||
this.hasCountries && | ||
<ods-select | ||
borderRounded="left" | ||
class="ods-phone-number__iso-codes" | ||
customRenderer={{ | ||
item: (data: OdsSelectCustomRendererData) => `<span part="flag flag-${data.value}"></span>`, | ||
option: (data: OdsSelectCustomRendererData) => ` | ||
<div style="display: grid; grid-template-columns: max-content 1fr; column-gap: 8px; white-space: nowrap;"> | ||
<span part="flag flag-${data.value}"></span> | ||
<span>${data.name} (+${data.phoneCode})</span> | ||
</div> | ||
`, | ||
}} | ||
dropdownWidth="auto" | ||
hasError={ this.hasError } | ||
isDisabled={ this.isDisabled } | ||
isReadonly={ this.isReadonly } | ||
name="iso-code" | ||
onOdsChange={ (e: OdsSelectEventChange) => this.onSelectChange(e) } | ||
part="select" | ||
value={ this.isoCode }> | ||
{ | ||
this.parsedCountryCodes.map((isoCode) => { | ||
const i18nCountry = this.i18nCountriesMap?.get(isoCode); | ||
return ( | ||
<option | ||
data-name={ i18nCountry?.name } | ||
data-phone-code={ i18nCountry?.phoneCode } | ||
value={ isoCode }> | ||
</option> | ||
); | ||
}) | ||
} | ||
</ods-select> | ||
} | ||
|
||
<ods-input | ||
ariaLabel={ this.ariaLabel } | ||
ariaLabelledby={ this.ariaLabelledby } | ||
class={{ | ||
'ods-phone-number__input': true, | ||
'ods-phone-number__input--sibling': this.hasCountries, | ||
}} | ||
defaultValue={ this.defaultValue } | ||
hasError={ this.hasError } | ||
isClearable={ this.isClearable } | ||
isDisabled={ this.isDisabled } | ||
isLoading={ this.isLoading } | ||
isReadonly={ this.isReadonly } | ||
isRequired={ this.isRequired } | ||
name={ this.name } | ||
onOdsChange={ (e: OdsInputValueChangeEvent) => this.onInputChange(e) } | ||
exportparts="input" | ||
pattern={ this.pattern } | ||
placeholder={ this.getPlaceholder() } | ||
ref={ (el?: unknown): OdsInput => this.inputElement = el as OdsInput } | ||
value={ this.value }> | ||
</ods-input> | ||
</Host> | ||
); | ||
} | ||
} |
Oops, something went wrong.