├── .nvmrc ├── website ├── README.md ├── .storybook │ ├── styles │ │ ├── styles.scss │ │ ├── _base.scss │ │ └── _story.scss │ ├── addons.js │ ├── stories │ │ ├── 4.CustomCSS │ │ │ ├── styles.scss │ │ │ └── CustomCSS.stories.js │ │ ├── 2.Props │ │ │ └── Props.stories.js │ │ ├── 6.GeoIP │ │ │ └── GeoIP.stories.js │ │ ├── 5.CustomStyle │ │ │ └── CustomStyle.stories.js │ │ ├── 1.GettingStarted │ │ │ └── GettingStarted.stories.js │ │ └── 3.Playground │ │ │ └── Playground.stories.js │ ├── helpers │ │ └── helpers.js │ ├── config.js │ └── webpack.config.js └── package.json ├── .releaserc ├── .coveralls.yml ├── .packwatch.json ├── .eslintignore ├── src ├── flags.png ├── flags@2x.png ├── index.js ├── components │ ├── __tests__ │ │ ├── AllCountries.test.ts │ │ ├── IntlTelInput.test.js │ │ ├── RootModal.test.tsx │ │ ├── FlagBox.test.tsx │ │ ├── CountryList.test.tsx │ │ ├── RootModal.test.js │ │ ├── FlagDropDown.test.tsx │ │ ├── TelInput.test.tsx │ │ ├── IntlTelInput.test.tsx │ │ ├── utils.test.js │ │ ├── FlagDropDown.test.js │ │ └── TelInput.test.js │ ├── constants.js │ ├── RootModal.d.ts │ ├── constants.d.ts │ ├── AllCountries.d.ts │ ├── FlagBox.d.ts │ ├── RootModal.js │ ├── CountryList.d.ts │ ├── TelInput.d.ts │ ├── FlagBox.js │ ├── FlagDropDown.d.ts │ ├── TelInput.js │ ├── utils.d.ts │ ├── FlagDropDown.js │ ├── CountryList.js │ ├── utils.js │ ├── AllCountries.js │ └── IntlTelInput.d.ts ├── index.d.ts ├── types.d.ts ├── intlTelInput.scss └── sprite.scss ├── .commitlintrc.js ├── .npmignore ├── config ├── jest │ ├── transform.js │ ├── setupTestFramework.js │ ├── fileTransform.js │ ├── cssTransform.js │ └── setup.js ├── .eslintrc ├── paths.js ├── env.js └── webpack.config.prod.js ├── .yarnrc.yml ├── .editorconfig ├── tsconfig.test.json ├── tsconfig.json ├── .babelrc ├── .gitignore ├── jest.config.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── main.yml ├── LICENSE ├── README.md ├── package.json ├── .eslintrc.js └── CHANGELOG.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.22.4 2 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # website 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"] 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: h4mSYiVLxUBNL77JwRz2ohH8Of8e4qSuc 2 | -------------------------------------------------------------------------------- /website/.storybook/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | @import "story"; 3 | -------------------------------------------------------------------------------- /.packwatch.json: -------------------------------------------------------------------------------- 1 | {"limit":"107.2 kB","packageSize":"107.2 kB","unpackedSize":"241.3 kB"} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/* 3 | package.json 4 | webpack.*.js 5 | coverage/* 6 | -------------------------------------------------------------------------------- /src/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patw0929/react-intl-tel-input/HEAD/src/flags.png -------------------------------------------------------------------------------- /src/flags@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patw0929/react-intl-tel-input/HEAD/src/flags@2x.png -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore all 2 | 3 | * 4 | 5 | # override above and include followings 6 | 7 | !dist/**/* 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import IntlTelInput from './components/IntlTelInput' 2 | 3 | export default IntlTelInput 4 | -------------------------------------------------------------------------------- /config/jest/transform.js: -------------------------------------------------------------------------------- 1 | const babelJest = require('babel-jest') 2 | 3 | module.exports = babelJest.createTransformer() 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | yarnPath: .yarn/releases/yarn-berry.cjs 3 | npmPublishRegistry: "https://registry.npmjs.com/" 4 | -------------------------------------------------------------------------------- /src/components/__tests__/AllCountries.test.ts: -------------------------------------------------------------------------------- 1 | import AllCountries from '../AllCountries' 2 | 3 | console.log(AllCountries.getCountries()) 4 | -------------------------------------------------------------------------------- /website/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-options/register' 2 | import '@storybook/addon-knobs/register' 3 | import '@storybook/addon-actions/register' 4 | -------------------------------------------------------------------------------- /website/.storybook/stories/4.CustomCSS/styles.scss: -------------------------------------------------------------------------------- 1 | .tel-wrapper { 2 | transform: rotate(-3deg); 3 | transform-origin: 0 0; 4 | } 5 | 6 | .tel-input { 7 | box-shadow: 0 0 20px red; 8 | } 9 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-var": 0, 4 | "comma-dangle": 0, 5 | "quote-props": 0, 6 | "object-shorthand": 0, 7 | "no-multiple-empty-lines": 0, 8 | "no-trailing-spaces": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /website/.storybook/styles/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | body { 7 | color: #222; 8 | line-height: 1.2; 9 | font-size: 13px; 10 | margin: 16px; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import IntlTelInput, { 2 | IntlTelInputProps, 3 | IntlTelInputState, 4 | } from './components/IntlTelInput' 5 | 6 | export default IntlTelInput 7 | export { IntlTelInputProps, IntlTelInputState } 8 | export { CountryData } from './types' 9 | -------------------------------------------------------------------------------- /src/components/constants.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const KEYS = { 3 | UP: 38, 4 | DOWN: 40, 5 | ENTER: 13, 6 | ESC: 27, 7 | PLUS: 43, 8 | A: 65, 9 | Z: 90, 10 | SPACE: 32, 11 | TAB: 9, 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/components/RootModal.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface RootModalProps {} 4 | 5 | export interface RootModalState {} 6 | 7 | export default class RootModal extends React.Component< 8 | RootModalProps, 9 | RootModalState 10 | > { 11 | modalTarget: HTMLDivElement | null 12 | } 13 | -------------------------------------------------------------------------------- /src/components/constants.d.ts: -------------------------------------------------------------------------------- 1 | interface KeysStatic { 2 | UP: 38 3 | DOWN: 40 4 | ENTER: 13 5 | ESC: 27 6 | PLUS: 43 7 | A: 65 8 | Z: 90 9 | SPACE: 32 10 | TAB: 9 11 | } 12 | 13 | declare const KEYS: KeysStatic 14 | 15 | // eslint-disable-next-line import/prefer-default-export 16 | export { KEYS } 17 | -------------------------------------------------------------------------------- /config/jest/setupTestFramework.js: -------------------------------------------------------------------------------- 1 | /* global jasmine:false */ 2 | 3 | if (process.env.CI) { 4 | const jasmineReporters = require('jasmine-reporters') // eslint-disable-line global-require 5 | const junitReporter = new jasmineReporters.JUnitXmlReporter({ 6 | savePath: 'testresults', 7 | consolidateAll: false, 8 | }) 9 | 10 | jasmine.getEnv().addReporter(junitReporter) 11 | } 12 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface CountryData { 2 | /** Country name. */ 3 | name?: string 4 | /** ISO 3166-1 alpha-2 code. */ 5 | iso2?: string 6 | /** International dial code. */ 7 | dialCode?: string 8 | /** Order (if >1 country with same dial code). */ 9 | priority?: number 10 | /** Area codes (if >1 country with same dial code). */ 11 | areaCodes?: string[] | null 12 | } 13 | -------------------------------------------------------------------------------- /website/.storybook/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const lookup = callback => { 3 | const request = new XMLHttpRequest() 4 | 5 | request.addEventListener('load', () => { 6 | callback(JSON.parse(request.responseText).country_code) 7 | }) 8 | 9 | request.open('GET', 'https://api.ipdata.co/?api-key=test') 10 | request.send() 11 | } 12 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // This is a custom Jest transformer turning file imports into filenames. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | // eslint-disable-next-line prefer-template 9 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';' 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return ` 7 | const idObj = require('identity-obj-proxy'); 8 | module.exports = idObj; 9 | ` 10 | }, 11 | getCacheKey() { 12 | // eslint-disable-line no-unused-vars 13 | // The output is always the same. 14 | return 'cssTransform' 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/components/AllCountries.d.ts: -------------------------------------------------------------------------------- 1 | import { CountryData } from '../types' 2 | 3 | type ExternalCountry = [ 4 | CountryData['name'], 5 | CountryData['iso2'], 6 | CountryData['dialCode'], 7 | CountryData['priority'], 8 | CountryData['areaCodes'], 9 | ] 10 | 11 | interface AllCountriesStatic { 12 | initialize(externalCountriesList: ExternalCountry[]): void 13 | getCountries(): CountryData[] 14 | } 15 | 16 | declare const AllCountries: AllCountriesStatic 17 | 18 | export default AllCountries 19 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | // Prevent the output of any files to /dist during compilation because this is just a test only. 5 | "declaration": false, 6 | "emitDeclarationOnly": false, 7 | "noEmit": true 8 | }, 9 | "include": [ 10 | "src/**/*.d.ts", 11 | "src/**/*.test.tsx" 12 | ], 13 | "exclude": [] // Include all the .test.tsx files in the compilation to verify that the modules are exported as intended. 14 | } 15 | -------------------------------------------------------------------------------- /website/.storybook/stories/2.Props/Props.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | 5 | import IntlTelInput from '../../../../src/components/IntlTelInput' 6 | import '../../../../src/intlTelInput.scss' 7 | 8 | storiesOf('Documentation', module) 9 | .addParameters({ options: { showAddonPanel: false } }) 10 | .add( 11 | 'Props', 12 | withInfo({ inline: true, source: false })(() => ), 13 | ) 14 | -------------------------------------------------------------------------------- /src/components/FlagBox.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface FlagBoxProps { 4 | dialCode: string 5 | isoCode: string 6 | name: string 7 | onMouseOver?: (event: React.MouseEvent) => void 8 | onFocus?: (event: React.FocusEvent) => void 9 | onClick?: (event: React.MouseEvent) => void 10 | flagRef?: (instance: HTMLDivElement | null) => void 11 | innerFlagRef?: (instance: HTMLDivElement | null) => void 12 | countryClass: string 13 | } 14 | 15 | declare const FlagBox: React.FunctionComponent 16 | 17 | export default FlagBox 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "allowJs": true, 8 | "allowSyntheticDefaultImports": true, 9 | "declaration": true, 10 | "emitDeclarationOnly": true, 11 | "esModuleInterop": true, 12 | "noEmitOnError": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "outDir": "dist", 15 | "lib": [ 16 | "dom", 17 | "dom.iterable", 18 | "esnext" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*.d.ts" 23 | ], 24 | "exclude": [ 25 | "src/components/__tests__/**/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /website/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@storybook/react' 2 | import { withOptions } from '@storybook/addon-options' 3 | import { version } from '../../package.json' 4 | 5 | const req = require.context('./stories', true, /.js*/) 6 | 7 | function loadStories() { 8 | req.keys().forEach(filename => req(filename)) 9 | // eslint-disable-next-line global-require 10 | require('./styles/styles.scss') 11 | } 12 | 13 | addDecorator( 14 | withOptions({ 15 | name: `react-intl-tel-input v${version}`, 16 | url: 'https://github.com/patw0929/react-intl-tel-input', 17 | sidebarAnimations: true, 18 | }), 19 | ) 20 | 21 | configure(loadStories, module) 22 | -------------------------------------------------------------------------------- /website/.storybook/styles/_story.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | table { 3 | margin-top: 20px; 4 | border: solid 1px #DDD; 5 | border-collapse: collapse; 6 | border-spacing: 0; 7 | font: normal 14px Arial, sans-serif; 8 | } 9 | table thead th { 10 | background-color: #DDD; 11 | border: solid 1px #DDD; 12 | color: #333; 13 | padding: 10px; 14 | text-align: left; 15 | text-shadow: 1px 1px 1px #fff; 16 | } 17 | table tbody td { 18 | border: solid 1px #DDD; 19 | color: #333; 20 | padding: 10px; 21 | text-shadow: 1px 1px 1px #fff; 22 | } 23 | } 24 | 25 | #root > div > div > div { 26 | border: none !important; 27 | } 28 | 29 | #story-root { 30 | padding: 0px 40px; 31 | } 32 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "presets": [ 4 | "@babel/preset-env", 5 | "@babel/preset-react" 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-proposal-class-properties", 9 | "@babel/plugin-syntax-dynamic-import", 10 | "react-docgen" 11 | ], 12 | "env": { 13 | "test": { 14 | "plugins": ["dynamic-import-node"] 15 | }, 16 | "production": { 17 | "ignore": ["**/*.test.js"], 18 | "plugins": [ 19 | [ 20 | "transform-react-remove-prop-types", 21 | { 22 | "removeImport": true, 23 | "additionalLibraries": [ 24 | "react-style-proptype" 25 | ] 26 | } 27 | ] 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Yarn 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/releases 5 | !.yarn/plugins 6 | !.yarn/sdks 7 | !.yarn/versions 8 | .pnp.* 9 | 10 | ### SublimeText ### 11 | *.sublime-workspace 12 | 13 | ### JetBrains ### 14 | .idea 15 | *.iml 16 | 17 | ### OSX ### 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | Icon 22 | 23 | # Thumbnails 24 | ._* 25 | 26 | # Files that might appear on external disk 27 | .Spotlight-V100 28 | .Trashes 29 | 30 | ### Windows ### 31 | # Windows image file caches 32 | Thumbs.db 33 | ehthumbs.db 34 | 35 | # Folder config file 36 | Desktop.ini 37 | 38 | # Recycle Bin used on file shares 39 | $RECYCLE.BIN/ 40 | 41 | # App specific 42 | dist/ 43 | node_modules/ 44 | .tmp 45 | /src/main.js 46 | npm-debug.log 47 | yarn-error.log 48 | 49 | coverage 50 | .example 51 | -------------------------------------------------------------------------------- /src/components/__tests__/IntlTelInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | 4 | import IntlTelInput from '../IntlTelInput' 5 | 6 | describe('Style customization', () => { 7 | it('correctly applies user-supplied classes on outer container', () => { 8 | const component = shallow() 9 | const mockClass = 'mock-class-1' 10 | component.setProps({ containerClassName: mockClass }) 11 | expect(component.props().className).toMatchInlineSnapshot( 12 | `"allow-dropdown mock-class-1"`, 13 | ) 14 | const otherMockClass = 'mock-class-2' 15 | component.setProps({ containerClassName: otherMockClass }) 16 | expect(component.props().className).toMatchInlineSnapshot( 17 | `"allow-dropdown mock-class-2"`, 18 | ) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /website/.storybook/stories/6.GeoIP/GeoIP.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | import { action } from '@storybook/addon-actions' 5 | 6 | import IntlTelInput from '../../../../src/components/IntlTelInput' 7 | import '../../../../src/intlTelInput.scss' 8 | import { lookup } from '../../helpers/helpers' 9 | 10 | storiesOf('Usage', module) 11 | .addParameters({ options: { selectedPanel: 'ACTION LOGGER' } }) 12 | .add( 13 | 'Geo IP', 14 | withInfo({ inline: true, source: false, propTables: null })(() => ( 15 | 21 | )), 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/RootModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ReactDOM from 'react-dom' 4 | 5 | export default class RootModal extends Component { 6 | static propTypes = { 7 | children: PropTypes.node, 8 | } 9 | 10 | constructor(props) { 11 | super(props) 12 | 13 | this.modalTarget = document.createElement('div') 14 | this.modalTarget.className = 'intl-tel-input iti-container' 15 | } 16 | 17 | componentDidMount() { 18 | document.body.appendChild(this.modalTarget) 19 | } 20 | 21 | componentWillUnmount() { 22 | document.body.removeChild(this.modalTarget) 23 | } 24 | 25 | render() { 26 | return ReactDOM.createPortal( 27 | {this.props.children}, 28 | this.modalTarget, 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/__tests__/RootModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import RootModal from '../RootModal' 4 | 5 | const App: React.FunctionComponent = () => { 6 | const rootModalComponentRef = React.useRef(null) 7 | 8 | const init = () => { 9 | const { current: rootModalComponent } = rootModalComponentRef 10 | if (rootModalComponent == null) { 11 | return 12 | } 13 | 14 | const { modalTarget } = rootModalComponent 15 | if (modalTarget == null) { 16 | return 17 | } 18 | 19 | console.log('modalTarget', modalTarget) 20 | console.log('modalTarget.className', modalTarget.className) 21 | } 22 | 23 | React.useEffect(() => { 24 | init() 25 | }, []) 26 | 27 | return ( 28 | 29 | ) 30 | } 31 | 32 | React.createElement(App) 33 | -------------------------------------------------------------------------------- /website/.storybook/stories/5.CustomStyle/CustomStyle.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | import { action } from '@storybook/addon-actions' 5 | import { withKnobs, object } from '@storybook/addon-knobs/react' 6 | 7 | import IntlTelInput from '../../../../src/components/IntlTelInput' 8 | import '../../../../src/intlTelInput.scss' 9 | 10 | storiesOf('Usage', module) 11 | .addDecorator(withKnobs) 12 | .add( 13 | 'Custom Style', 14 | withInfo({ inline: true, source: false, propTables: null })(() => ( 15 | 21 | )), 22 | ) 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: [ 3 | 'src/**/*.js', 4 | '!**/__mocks__/**', 5 | '!**/__tests__/**', 6 | '!.storybook', 7 | ], 8 | setupFiles: ['/config/jest/setup.js'], 9 | setupTestFrameworkScriptFile: '/config/jest/setupTestFramework.js', 10 | testEnvironment: 'jsdom', 11 | testPathIgnorePatterns: ['[/\\\\](build|docs|node_modules)[/\\\\]'], 12 | testRegex: '/__tests__/.*\\.(test|spec)\\.js$', 13 | testURL: 'http://localhost', 14 | transform: { 15 | '^(?!.*\\.(js|jsx|css|scss|json)$)': 16 | '/config/jest/fileTransform.js', 17 | '^.+\\.(js|jsx)$': '/config/jest/transform.js', 18 | '^.+\\.(scss|css)$': '/config/jest/cssTransform.js', 19 | }, 20 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 21 | } 22 | -------------------------------------------------------------------------------- /website/.storybook/stories/4.CustomCSS/CustomCSS.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | import { action } from '@storybook/addon-actions' 5 | import { withKnobs, text } from '@storybook/addon-knobs/react' 6 | 7 | import IntlTelInput from '../../../../src/components/IntlTelInput' 8 | import './styles.scss' 9 | 10 | storiesOf('Usage', module) 11 | .addDecorator(withKnobs) 12 | .add( 13 | 'Custom CSS', 14 | withInfo({ inline: true, source: false, propTables: null })(() => { 15 | return ( 16 | 26 | ) 27 | }), 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/CountryList.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CountryData } from '../types' 4 | 5 | export interface CountryListProps { 6 | setFlag?: (iso2: string) => void 7 | countries?: CountryData[] 8 | inputTop?: number 9 | inputOuterHeight?: number 10 | preferredCountries?: CountryData 11 | highlightedCountry?: number 12 | changeHighlightCountry?: ( 13 | showDropdown: boolean, 14 | selectedIndex: number, 15 | ) => void 16 | showDropdown?: boolean 17 | isMobile?: boolean 18 | dropdownContainer?: string 19 | } 20 | 21 | export interface CountryListState {} 22 | 23 | export default class CountryList extends React.Component< 24 | CountryListProps, 25 | CountryListState 26 | > { 27 | listElement?: HTMLUListElement | null 28 | 29 | setDropdownPosition(): void 30 | 31 | appendListItem( 32 | countries: CountryData[], 33 | isPreferred?: boolean, 34 | ): React.ReactNode 35 | 36 | handleMouseOver: (event: React.MouseEvent) => void 37 | } 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Screenshots (if appropriate): 7 | 8 | 9 | ## Types of changes 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 14 | 15 | ## Checklist: 16 | 17 | 18 | - [ ] I have used ESLint & Prettier to follow the code style of this project. 19 | - [ ] I have updated the documentation accordingly. 20 | - [ ] I have added tests to cover my changes. 21 | - [ ] All new and existing tests passed. 22 | -------------------------------------------------------------------------------- /src/components/TelInput.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface TelInputProps { 4 | className?: string 5 | disabled?: boolean 6 | readonly?: boolean 7 | fieldName?: string 8 | fieldId?: string 9 | value?: string 10 | placeholder?: string 11 | handleInputChange: (event: React.ChangeEvent) => void 12 | handleOnBlur: (event: React.FocusEvent) => void 13 | handleOnFocus: (event: React.FocusEvent) => void 14 | autoFocus?: boolean 15 | autoComplete?: string 16 | inputProps?: React.HTMLProps 17 | refCallback: (element: HTMLInputElement | null) => void 18 | cursorPosition?: number 19 | } 20 | 21 | export interface TelInputState { 22 | hasFocus: boolean 23 | } 24 | 25 | export default class TelInput extends React.Component< 26 | TelInputProps, 27 | TelInputState 28 | > { 29 | tel?: HTMLInputElement | null 30 | 31 | refHandler: (element: HTMLInputElement | null) => void 32 | 33 | handleBlur: (event: React.FocusEvent) => void 34 | 35 | handleFocus: (event: React.FocusEvent) => void 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 Patrick Wang (patw) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Current Behavior 7 | 8 | 9 | 10 | ## Possible Solution 11 | 12 | 13 | ## Steps to Reproduce 14 | 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | Code: 21 | 22 | ```js 23 | ``` 24 | 25 | 26 | 27 | ## Environment 28 | 29 | * Version: 30 | 31 | * Browser: 32 | 33 | 34 | ## Detailed Description 35 | 36 | -------------------------------------------------------------------------------- /src/components/__tests__/FlagBox.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import FlagBox from '../FlagBox' 4 | 5 | const App = () => { 6 | const flagRef = React.useRef(null) 7 | const flagRefCallback = (instance: HTMLDivElement | null) => { 8 | flagRef.current = instance 9 | } 10 | 11 | const innerFlagRef = React.useRef(null) 12 | const innerFlagRefCallback = (instance: HTMLDivElement | null) => { 13 | innerFlagRef.current = instance 14 | } 15 | 16 | const init = () => { 17 | const { current: flag } = flagRef 18 | const { current: innerFlag } = flagRef 19 | 20 | if (flag == null || innerFlag == null) { 21 | return 22 | } 23 | 24 | console.log('flag.className', flag.className) 25 | console.log('innerFlag.className', innerFlag.className) 26 | } 27 | 28 | React.useEffect(() => { 29 | init() 30 | }, []) 31 | 32 | return ( 33 | 41 | ) 42 | } 43 | 44 | React.createElement(App) 45 | -------------------------------------------------------------------------------- /config/jest/setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom' 2 | import sinon from 'sinon' 3 | import Enzyme from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | import '@babel/polyfill' 6 | 7 | Enzyme.configure({ adapter: new Adapter() }) 8 | 9 | // localStorage 10 | class LocalStorageMock { 11 | constructor() { 12 | this.store = {} 13 | } 14 | 15 | clear() { 16 | this.store = {} 17 | } 18 | 19 | getItem(key) { 20 | return this.store[key] 21 | } 22 | 23 | setItem(key, value) { 24 | this.store[key] = value.toString() 25 | } 26 | } 27 | 28 | window.localStorage = new LocalStorageMock() 29 | window.__SERVER__ = false 30 | window.__DEVELOPMENT__ = false 31 | 32 | // Define some html to be our basic document 33 | // JSDOM will consume this and act as if we were in a browser 34 | const DEFAULT_HTML = '' 35 | 36 | // Define some variables to make it look like we're a browser 37 | // First, use JSDOM's fake DOM as the document 38 | global.document = jsdom.jsdom(DEFAULT_HTML) 39 | 40 | // Set up a mock window 41 | global.window = document.defaultView 42 | 43 | // Allow for things like window.location 44 | global.navigator = window.navigator 45 | 46 | global.XMLHttpRequest = sinon.useFakeXMLHttpRequest() 47 | -------------------------------------------------------------------------------- /src/components/FlagBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const FlagBox = ({ 5 | dialCode, 6 | isoCode, 7 | name, 8 | onMouseOver, 9 | onFocus, 10 | onClick, 11 | flagRef, 12 | innerFlagRef, 13 | countryClass, 14 | }) => ( 15 |
  • 23 |
    24 |
    25 |
    26 | 27 | {name} 28 | {`+ ${dialCode}`} 29 |
  • 30 | ) 31 | 32 | FlagBox.propTypes = { 33 | dialCode: PropTypes.string.isRequired, 34 | isoCode: PropTypes.string.isRequired, 35 | name: PropTypes.string.isRequired, 36 | onMouseOver: PropTypes.func, 37 | onFocus: PropTypes.func, 38 | onClick: PropTypes.func, 39 | flagRef: PropTypes.func, 40 | innerFlagRef: PropTypes.func, 41 | countryClass: PropTypes.string.isRequired, 42 | } 43 | 44 | FlagBox.defaultProps = { 45 | onFocus: () => {}, 46 | onMouseOver: () => {}, 47 | onClick: () => {}, 48 | } 49 | 50 | export default FlagBox 51 | -------------------------------------------------------------------------------- /website/.storybook/stories/1.GettingStarted/GettingStarted.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | 5 | import IntlTelInput from '../../../../src/components/IntlTelInput' 6 | import '../../../../src/intlTelInput.scss' 7 | 8 | storiesOf('Documentation', module) 9 | .addParameters({ options: { showAddonPanel: false } }) 10 | .add( 11 | 'Getting Started', 12 | withInfo({ inline: true, source: false, propTables: null })(() => ( 13 | 14 | )), 15 | { 16 | info: { 17 | text: ` 18 | ## Installation 19 | 20 | ~~~bash 21 | npm install react-intl-tel-input --save 22 | ~~~ 23 | 24 | or 25 | 26 | ~~~bash 27 | yarn add react-intl-tel-input 28 | ~~~ 29 | 30 | ## Basic Usage 31 | ~~~js 32 | import React from 'react'; 33 | import ReactDOM from 'react-dom'; 34 | import IntlTelInput from 'react-intl-tel-input'; 35 | import 'react-intl-tel-input/dist/main.css'; 36 | 37 | ReactDOM.render( 38 | , 43 | document.getElementById('root'), 44 | ); 45 | ~~~ 46 | `, 47 | }, 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "private": true, 4 | "packageManager": "yarn@3.0.0", 5 | "peerDependencies": { 6 | "@babel/cli": "*", 7 | "@babel/core": "*", 8 | "eslint": "*", 9 | "react": "*", 10 | "react-dom": "*" 11 | }, 12 | "dependencies": { 13 | "@storybook/addon-actions": "4.1.11", 14 | "@storybook/addon-info": "^4.1.11", 15 | "@storybook/addon-knobs": "^4.1.11", 16 | "@storybook/addon-options": "^4.1.11", 17 | "@storybook/cli": "^4.1.6", 18 | "@storybook/react": "^4.1.6", 19 | "@storybook/storybook-deployer": "^2.8.1", 20 | "babel-loader": "^8.0.4", 21 | "css-loader": "^1.0.1", 22 | "css-modules-require-hook": "^4.0.1", 23 | "eslint-loader": "^2.1.1", 24 | "file-loader": "^2.0.0", 25 | "image-webpack-loader": "^4.6.0", 26 | "mini-css-extract-plugin": "^0.4.5", 27 | "optimize-css-assets-webpack-plugin": "^5.0.1", 28 | "postcss-safe-parser": "^4.0.1", 29 | "react-hot-loader": "^1.3.0", 30 | "sass-loader": "^7.1.0", 31 | "storybook-addon-react-docgen": "^1.0.4", 32 | "style-loader": "^0.23.1", 33 | "uglifyjs-webpack-plugin": "^2.0.1", 34 | "url-loader": "^1.1.2", 35 | "webpack": "^4.27.1" 36 | }, 37 | "scripts": { 38 | "start": "start-storybook -p 4000 -c .storybook", 39 | "deploy:dryrun": "storybook-to-ghpages --dry-run", 40 | "deploy": "storybook-to-ghpages -- --ci" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/FlagDropDown.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CountryData } from '../types' 4 | 5 | import CountryList from './CountryList' 6 | 7 | export interface FlagDropDownProps { 8 | allowDropdown?: boolean 9 | dropdownContainer?: React.ElementType | string 10 | separateDialCode?: boolean 11 | dialCode?: string 12 | countryCode?: string 13 | showDropdown?: boolean 14 | clickSelectedFlag?: ( 15 | event: React.MouseEvent, 16 | ) => void 17 | handleSelectedFlagKeydown?: ( 18 | event: React.KeyboardEvent, 19 | ) => void 20 | isMobile?: boolean 21 | setFlag?: (iso2: string) => void 22 | countries?: CountryData[] 23 | inputTop?: number 24 | inputOuterHeight?: number 25 | preferredCountries?: CountryData[] 26 | highlightedCountry?: number 27 | changeHighlightCountry?: ( 28 | showDropdown: boolean, 29 | selectedIndex: number, 30 | ) => void 31 | titleTip?: string 32 | refCallback: (instance: HTMLDivElement | null) => void 33 | } 34 | 35 | export interface FlagDropDownState {} 36 | 37 | export default class FlagDropDown extends React.Component< 38 | FlagDropDownProps, 39 | FlagDropDownState 40 | > { 41 | countryList?: CountryList | null 42 | 43 | genSelectedDialCode: () => React.ReactNode 44 | 45 | genArrow: () => React.ReactNode 46 | 47 | genFlagClassName: () => string 48 | 49 | genCountryList: () => React.ReactNode 50 | } 51 | -------------------------------------------------------------------------------- /src/components/__tests__/CountryList.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import AllCountries from '../AllCountries' 4 | import CountryList from '../CountryList' 5 | 6 | const App = () => { 7 | const countryListComponentRef = React.useRef(null) 8 | 9 | const init = () => { 10 | const { current: countryListComponent } = countryListComponentRef 11 | if (countryListComponent == null) { 12 | return 13 | } 14 | 15 | console.log('countryListComponent.listElement', countryListComponent.listElement) 16 | countryListComponent.appendListItem([ 17 | { 18 | name: '', 19 | iso2: '', 20 | dialCode: '', 21 | priority: 0, 22 | areaCodes: null, 23 | } 24 | ], false) 25 | } 26 | 27 | React.useEffect(() => { 28 | init() 29 | }, []) 30 | 31 | const countries = AllCountries.getCountries() 32 | const changeHighlightCountry = (showDropdown: boolean, selectedIndex: number) => { 33 | console.log(showDropdown, selectedIndex) 34 | }; 35 | const setFlag = (iso2: string) => { 36 | console.log(iso2) 37 | } 38 | 39 | return ( 40 | 49 | ) 50 | } 51 | 52 | React.createElement(App) 53 | -------------------------------------------------------------------------------- /src/components/__tests__/RootModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import RootModal from '../RootModal' 4 | 5 | // eslint-disable-next-line func-names 6 | describe('RootModal', function() { 7 | beforeEach(() => { 8 | jest.resetModules() 9 | 10 | this.makeSubject = () => { 11 | return mount( 12 | 13 |
    foo
    14 |
    , 15 | { 16 | attachTo: document.body, 17 | }, 18 | ) 19 | } 20 | }) 21 | 22 | it('should has a div.root tag', () => { 23 | const subject = this.makeSubject() 24 | 25 | expect(subject.find('div.root').length).toBeTruthy() 26 | }) 27 | 28 | it('should has parent element which has specific className', () => { 29 | const subject = this.makeSubject() 30 | 31 | expect(subject.instance().modalTarget.classList[0]).toBe('intl-tel-input') 32 | expect(subject.instance().modalTarget.classList[1]).toBe('iti-container') 33 | }) 34 | 35 | it('should has a modalTarget in body', () => { 36 | const subject = this.makeSubject() 37 | 38 | subject.setState({ 39 | foo: 'foo', 40 | }) 41 | expect(subject.instance().modalTarget.classList[0]).toBe('intl-tel-input') 42 | expect(subject.instance().modalTarget.classList[1]).toBe('iti-container') 43 | }) 44 | 45 | it('should not has a modalTarget in body', () => { 46 | const subject = this.makeSubject() 47 | 48 | subject.unmount() 49 | expect(document.body.querySelector('.iti-container')).toBeNull() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /website/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack'); 3 | const paths = require('../../config/paths'); 4 | 5 | module.exports = { 6 | devtool: false, 7 | entry: { 8 | main: '../../src/components/IntlTelInput.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | loader: 'eslint-loader', 15 | enforce: 'pre', 16 | include: paths.appSrc, 17 | }, 18 | { 19 | test: /\.(js|jsx)$/, 20 | include: paths.appSrc, 21 | loader: 'babel-loader', 22 | options: { 23 | presets: [ 24 | "@babel/preset-env", 25 | "@babel/preset-react", 26 | 27 | { 28 | plugins: ["@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties"] 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | test: /\.scss$/, 35 | use: [ 36 | 'style-loader', 37 | 'css-loader', 38 | { 39 | loader: 'sass-loader', 40 | options: { 41 | implementation: require('sass'), 42 | sassOptions: { outputStyle: 'expanded' } 43 | } 44 | } 45 | ], 46 | }, 47 | { 48 | test: /\.(jpg|png|svg|gif)$/, 49 | loader: 'file-loader', 50 | options: { 51 | name: 'img/[name].[ext]', 52 | publicPath: './', 53 | }, 54 | }, 55 | { 56 | test: /\.css$/, 57 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 58 | }, 59 | ], 60 | }, 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /src/components/__tests__/FlagDropDown.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import FlagDropDown from '../FlagDropDown' 4 | 5 | const App = () => { 6 | const flagDropDownComponentRef = React.useRef(null) 7 | const flagDropDownElementRef = React.useRef(null) 8 | const refCallback = (instance: HTMLDivElement | null) => { 9 | flagDropDownElementRef.current = instance 10 | } 11 | 12 | const init = () => { 13 | const { current: flagDropDownComponent } = flagDropDownComponentRef 14 | if (flagDropDownComponent == null) { 15 | return 16 | } 17 | 18 | const { current: flagDropDownElement } = flagDropDownElementRef 19 | if (flagDropDownElement == null) { 20 | return 21 | } 22 | 23 | console.log('flagDropDownElement.className', flagDropDownElement.className) 24 | console.log( 25 | 'flagDropDownComponent.countryList', 26 | flagDropDownComponent.countryList, 27 | ) 28 | 29 | console.log( 30 | 'flagDropDownComponent.genArrow()', 31 | flagDropDownComponent.genArrow(), 32 | ) 33 | console.log( 34 | 'flagDropDownComponent.genCountryList()', 35 | flagDropDownComponent.genCountryList(), 36 | ) 37 | console.log( 38 | 'flagDropDownComponent.genSelectedDialCode()', 39 | flagDropDownComponent.genSelectedDialCode(), 40 | ) 41 | } 42 | 43 | React.useEffect(() => { 44 | init() 45 | }, []) 46 | 47 | return ( 48 | 54 | ) 55 | } 56 | 57 | React.createElement(App) 58 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | const appDirectory = fs.realpathSync(path.join(path.dirname(process.cwd()))) 7 | 8 | function resolveApp(relativePath) { 9 | return path.resolve(appDirectory, relativePath) 10 | } 11 | 12 | // We support resolving modules according to `NODE_PATH`. 13 | // This lets you use absolute paths in imports inside large monorepos: 14 | // https://github.com/facebookincubator/create-react-app/issues/253. 15 | 16 | // It works similar to `NODE_PATH` in Node itself: 17 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 18 | 19 | // We will export `nodePaths` as an array of absolute paths. 20 | // It will then be used by Webpack configs. 21 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 22 | 23 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 24 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 25 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 26 | 27 | // eslint-disable-next-line vars-on-top 28 | const nodePaths = (process.env.NODE_PATH || '') 29 | .split(process.platform === 'win32' ? ';' : ':') 30 | .filter(Boolean) 31 | .filter(folder => !path.isAbsolute(folder)) 32 | .map(resolveApp) 33 | 34 | // config after eject: we're in ./config/ 35 | module.exports = { 36 | appDist: resolveApp('dist'), 37 | appPackageJson: resolveApp('package.json'), 38 | appSrc: resolveApp('src'), 39 | nodePaths: nodePaths, 40 | } 41 | -------------------------------------------------------------------------------- /src/components/__tests__/TelInput.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import TelInput from '../TelInput' 4 | 5 | const App: React.FunctionComponent = () => { 6 | const telInputComponentRef = React.useRef(null) 7 | const inputElementRef = React.useRef(null) 8 | const refCallback = (instance: HTMLInputElement | null) => { 9 | inputElementRef.current = instance 10 | } 11 | 12 | const init = () => { 13 | const { current: telInputComponent } = telInputComponentRef 14 | if (telInputComponent == null) { 15 | return 16 | } 17 | 18 | const { current: inputElement } = inputElementRef 19 | if (inputElement == null) { 20 | return 21 | } 22 | 23 | inputElement.focus() 24 | console.log('inputElement.focus()') 25 | console.log('telInputComponent.state.hasFocus', telInputComponent.state.hasFocus) 26 | console.log('telInputComponent.tel', telInputComponent.tel) 27 | } 28 | 29 | React.useEffect(() => { 30 | init() 31 | }, []) 32 | 33 | const handleInputChange = (event: React.ChangeEvent) => { 34 | console.log('handleInputChange', event.target.value) 35 | } 36 | 37 | const handleOnFocus = (event: React.FocusEvent) => { 38 | console.log('handleOnFocus', event.target.value) 39 | } 40 | 41 | const handleOnBlur = (event: React.FocusEvent) => { 42 | console.log('handleOnBlur', event.target.value) 43 | } 44 | 45 | return ( 46 | 53 | ) 54 | } 55 | 56 | React.createElement(App) 57 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, arrow-parens, prefer-template */ 2 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 3 | // injected into the application via DefinePlugin in Webpack configuration. 4 | 5 | const REACT_APP = /^REACT_APP_/i 6 | 7 | /* 8 | * Get Global Objects in different running environments 9 | */ 10 | const getGlobalObject = () => ` 11 | (function(){ 12 | if(typeof window !== "undefined" && window) 13 | return window; 14 | else if(typeof self !== "undefined" && self) 15 | return self; 16 | else 17 | return this; 18 | })() 19 | ` 20 | 21 | const getClientEnvironment = publicUrl => { 22 | const NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development') 23 | const DEVELOPMENT = NODE_ENV === JSON.stringify('development') 24 | const SERVER = false 25 | const CLIENT = true 26 | const BUILD_NAME = JSON.stringify(process.env.BUILD_NAME || 'dev') 27 | 28 | const processEnv = Object.keys(process.env) 29 | .filter(key => REACT_APP.test(key)) 30 | .reduce( 31 | (env, key) => { 32 | env[key] = JSON.stringify(process.env[key]) // eslint-disable-line no-param-reassign 33 | 34 | return env 35 | }, 36 | { 37 | // Useful for determining whether we’re running in production mode. 38 | // Most importantly, it switches React into the correct mode. 39 | NODE_ENV: NODE_ENV, 40 | // Useful for resolving the correct path to static assets in `public`. 41 | // For example, . 42 | // This should only be used as an escape hatch. Normally you would put 43 | // images into the `src` and `import` them in code to get their paths. 44 | PUBLIC_URL: JSON.stringify(publicUrl), 45 | BUILD_NAME: BUILD_NAME, 46 | }, 47 | ) 48 | 49 | return { 50 | 'process.env': processEnv, 51 | getGlobalObject, 52 | __SERVER__: SERVER, 53 | __CLIENT__: CLIENT, 54 | __DEVELOPMENT__: DEVELOPMENT, 55 | } 56 | } 57 | 58 | module.exports = getClientEnvironment 59 | -------------------------------------------------------------------------------- /src/components/TelInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class TelInput extends Component { 5 | static propTypes = { 6 | className: PropTypes.string, 7 | disabled: PropTypes.bool, 8 | readonly: PropTypes.bool, 9 | fieldName: PropTypes.string, 10 | fieldId: PropTypes.string, 11 | value: PropTypes.string, 12 | placeholder: PropTypes.string, 13 | handleInputChange: PropTypes.func, 14 | handleOnBlur: PropTypes.func, 15 | handleOnFocus: PropTypes.func, 16 | autoFocus: PropTypes.bool, 17 | autoComplete: PropTypes.string, 18 | inputProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types 19 | refCallback: PropTypes.func.isRequired, 20 | cursorPosition: PropTypes.number, 21 | } 22 | 23 | state = { 24 | hasFocus: false, 25 | } 26 | 27 | componentDidUpdate() { 28 | if (this.state.hasFocus) { 29 | this.tel.setSelectionRange( 30 | this.props.cursorPosition, 31 | this.props.cursorPosition, 32 | ) 33 | } 34 | } 35 | 36 | refHandler = element => { 37 | this.tel = element 38 | this.props.refCallback(element) 39 | } 40 | 41 | handleBlur = e => { 42 | this.setState({ hasFocus: false }) 43 | 44 | if (typeof this.props.handleOnBlur === 'function') { 45 | this.props.handleOnBlur(e) 46 | } 47 | } 48 | 49 | handleFocus = e => { 50 | this.setState({ hasFocus: true }) 51 | 52 | if (typeof this.props.handleOnFocus === 'function') { 53 | this.props.handleOnFocus(e) 54 | } 55 | } 56 | 57 | render() { 58 | return ( 59 | 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { CountryData } from '../types' 2 | 3 | interface UtilsStatic { 4 | arraysEqual: (a: any, b: any) => boolean 5 | 6 | shallowEquals: (a: any, b: any) => boolean 7 | 8 | trim: (str?: string) => string 9 | 10 | isNumeric: (obj: any) => obj is number 11 | 12 | retrieveLiIndex: (node?: HTMLElement) => number 13 | 14 | getNumeric: (s: string) => string 15 | 16 | startsWith: (a: string, b: string) => boolean 17 | 18 | isWindow: (obj?: any) => obj is Window 19 | 20 | getWindow: (elem: any) => Window 21 | 22 | offset: ( 23 | elem: HTMLElement, 24 | ) => { 25 | top: number 26 | left: number 27 | } 28 | 29 | getOuterHeight: (element: HTMLElement) => number 30 | 31 | getCountryData: { 32 | // utils.getCountryData([], undefined, undefined, ) <---- The last variable has not been given and is therefore, `undefined`. 33 | // ^^ ^^^^^^^^^ ^^^^^^^^^ ^ 34 | // 1 2 3 4 35 | // Declare this one first so that when the compiler infers the type returned from the invoke pattern above, 36 | // it assumes the user wants this overloadable function instead of the next one when the 4th variable is actually `undefined`. 37 | ( 38 | countries: CountryData[], 39 | countryCode?: string, 40 | ignoreOnlyCountriesOption?: boolean, 41 | allowFail?: false, 42 | errorHandler?: (failedCountryCode: string) => void, 43 | ): CountryData 44 | 45 | // Evaluate second so that when called without an `allowFail` set to `true`, the first overload is the assumed returned type. 46 | // Questionmarked parameters (i.e. optional arguments) can't be used here, because `allowFail` must be `true` in order to 47 | // obtain this function's returned type. 48 | // 49 | // Example: 50 | // utils.getCountryData([], undefined, undefined, true) <---- There's no other way to achieve optional argument 1 & 2. 51 | ( 52 | countries: CountryData[], 53 | countryCode: string | undefined, 54 | ignoreOnlyCountriesOption: boolean | undefined, 55 | allowFail: true, 56 | ): CountryData | null 57 | } 58 | 59 | findIndex: ( 60 | items: T[], 61 | predicate: (item: T) => boolean | undefined, 62 | ) => number 63 | 64 | getCursorPositionAfterFormating: ( 65 | prevBeforeCursor?: string, 66 | prev?: string, 67 | next?: string, 68 | ) => number 69 | } 70 | 71 | declare const utils: UtilsStatic 72 | 73 | export default utils 74 | -------------------------------------------------------------------------------- /src/components/__tests__/IntlTelInput.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CountryData } from '../../types' 4 | import IntlTelInput from '../IntlTelInput' 5 | 6 | interface AppProps {} 7 | interface AppState { 8 | value: string 9 | fullNumber: string 10 | iso2: string 11 | } 12 | 13 | class App extends React.Component { 14 | intlTelInput: React.RefObject = React.createRef() 15 | 16 | state: AppState = { 17 | value: '', 18 | fullNumber: '', 19 | iso2: '', 20 | } 21 | 22 | handlePhoneNumberChange = ( 23 | isValid: boolean, 24 | value: string, 25 | seletedCountryData: CountryData, 26 | fullNumber: string, 27 | extension: string, 28 | ) => { 29 | console.log('Details:', { 30 | isValid, 31 | value, 32 | fullNumber, 33 | extension, 34 | }) 35 | const { iso2 = '' } = seletedCountryData 36 | 37 | this.setState({ 38 | value, 39 | fullNumber, 40 | iso2, 41 | }) 42 | } 43 | 44 | handlePhoneNumberBlur = ( 45 | isValid: boolean, 46 | value: string, 47 | seletedCountryData: CountryData, 48 | fullNumber: string, 49 | extension: string, 50 | event: React.FocusEvent, 51 | ) => { 52 | console.log('Blur event', event) 53 | console.log('Native event type:', event.type) 54 | console.log('Details:', { 55 | isValid, 56 | value, 57 | seletedCountryData, 58 | fullNumber, 59 | extension, 60 | event, 61 | }) 62 | } 63 | 64 | handlePhoneNumberFocus = ( 65 | isValid: boolean, 66 | value: string, 67 | seletedCountryData: CountryData, 68 | fullNumber: string, 69 | extension: string, 70 | event: React.FocusEvent, 71 | ) => { 72 | console.log('Focus event') 73 | console.log('Native event type:', event.type) 74 | console.log('Details:', { 75 | isValid, 76 | value, 77 | seletedCountryData, 78 | fullNumber, 79 | extension, 80 | event, 81 | }) 82 | } 83 | 84 | render() { 85 | const { value, fullNumber, iso2 } = this.state 86 | 87 | return ( 88 |
    89 | 96 |
    97 | Full number: 98 | {fullNumber} 99 |
    100 |
    101 | ISO-2: 102 | {iso2} 103 |
    104 |
    105 | ) 106 | } 107 | } 108 | 109 | React.createElement(App) 110 | -------------------------------------------------------------------------------- /website/.storybook/stories/3.Playground/Playground.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { action } from '@storybook/addon-actions' 3 | import { storiesOf } from '@storybook/react' 4 | import { withInfo } from '@storybook/addon-info' 5 | import { withKnobs, text, boolean, array } from '@storybook/addon-knobs/react' 6 | 7 | import IntlTelInput from '../../../../src/components/IntlTelInput' 8 | import '../../../../src/intlTelInput.scss' 9 | 10 | const { defaultProps } = IntlTelInput 11 | 12 | storiesOf('Documentation', module) 13 | .addDecorator(withKnobs) 14 | .add( 15 | 'Playground', 16 | withInfo({ inline: true, source: false, propTables: null })(() => ( 17 | 71 | )), 72 | ) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Intl-Tel-Input 2 | 3 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 4 | [![CICD](https://github.com/patw0929/react-intl-tel-input/actions/workflows/main.yml/badge.svg)](https://github.com/patw0929/react-intl-tel-input/actions/workflows/main.yml) 5 | [![npm version](https://badge.fury.io/js/react-intl-tel-input.svg)](http://badge.fury.io/js/react-intl-tel-input) 6 | [![Coverage Status](https://coveralls.io/repos/github/patw0929/react-intl-tel-input/badge.svg?branch=master)](https://coveralls.io/github/patw0929/react-intl-tel-input?branch=master) 7 | [![npm](https://img.shields.io/npm/l/express.svg?maxAge=2592000)]() 8 | 9 | [![NPM](https://nodei.co/npm/react-intl-tel-input.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-intl-tel-input/) 10 | 11 | Rewrite [International Telephone Input](https://github.com/jackocnr/intl-tel-input) in React.js. 12 | 13 | 14 | ## Collaborators Wanted! 15 | 16 | Due to the long commuting time, I do not have much time to maintain this project often. 😣 17 | 18 | So if anybody else is willing to take on the work of bug fixes, integrating pull requests, etc, 19 | please let me know. 🙌 20 | 21 | I hope we can maintain the project together, and make this project better! 💪 22 | 23 | ## Demo & Examples 24 | 25 | Live demo: [patw0929.github.io/react-intl-tel-input](https://patw0929.github.io/react-intl-tel-input/) 26 | 27 | To build the examples locally, run: 28 | 29 | ```bash 30 | yarn 31 | yarn website:start 32 | ``` 33 | 34 | Then open [`localhost:3000`](http://localhost:3000) in a browser. 35 | 36 | 37 | ## Installation 38 | 39 | ```bash 40 | yarn add react-intl-tel-input 41 | ``` 42 | 43 | 44 | ### TypeScript 45 | 46 | `react-intl-tel-input` ships with official type declarations out of the box. 47 | 48 | 49 | ## Usage 50 | 51 | ```javascript 52 | import IntlTelInput from 'react-intl-tel-input'; 53 | import 'react-intl-tel-input/dist/main.css'; 54 | 55 | 59 | ``` 60 | 61 | ### Properties 62 | 63 | Please see the [Demo Page](https://patw0929.github.io/react-intl-tel-input/) 64 | 65 | 66 | ## Development (`src` and the build process) 67 | 68 | To build, watch and serve the examples (which will also watch the component source), run `yarn website:start`. 69 | 70 | You can prepare a distribution build using `yarn build`. 71 | 72 | ## Contributing 73 | 74 | Any kind of contribution including proposals, doc improvements, enhancements, bug fixes are always welcome. 75 | 76 | To contribute to `react-intl-tel-input`, clone this repo locally and commit your code on a separate branch. Please write tests for your code, and run the linter before opening a pull-request: 77 | 78 | ```bash 79 | yarn test # if you are enhancing the JavaScript modules 80 | yarn test:ts # if you are enhancing the TypeScript type declarations 81 | yarn lint 82 | ``` 83 | 84 | Also, please let us know if you encounter any issue by filing an [issue](https://github.com/patw0929/react-intl-tel-input/issues). 85 | 86 | ## Inspired by 87 | 88 | [International Telephone Input](https://github.com/jackocnr/intl-tel-input) - [@jackocnr](https://github.com/jackocnr) 89 | 90 | 91 | ## License 92 | 93 | MIT 94 | 95 | Copyright (c) 2015-2019 patw. 96 | -------------------------------------------------------------------------------- /src/components/FlagDropDown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import CountryList from './CountryList' 5 | import RootModal from './RootModal' 6 | 7 | export default class FlagDropDown extends Component { 8 | static propTypes = { 9 | allowDropdown: PropTypes.bool, 10 | dropdownContainer: PropTypes.string, 11 | separateDialCode: PropTypes.bool, 12 | dialCode: PropTypes.string, 13 | countryCode: PropTypes.string, 14 | showDropdown: PropTypes.bool, 15 | clickSelectedFlag: PropTypes.func, 16 | handleSelectedFlagKeydown: PropTypes.func, 17 | isMobile: PropTypes.bool, 18 | setFlag: PropTypes.func, 19 | countries: PropTypes.arrayOf(PropTypes.object), 20 | inputTop: PropTypes.number, 21 | inputOuterHeight: PropTypes.number, 22 | preferredCountries: PropTypes.arrayOf(PropTypes.object), 23 | highlightedCountry: PropTypes.number, 24 | changeHighlightCountry: PropTypes.func, 25 | titleTip: PropTypes.string, 26 | refCallback: PropTypes.func.isRequired, 27 | } 28 | 29 | genSelectedDialCode = () => { 30 | const { separateDialCode, dialCode } = this.props 31 | 32 | return separateDialCode ? ( 33 |
    {dialCode}
    34 | ) : null 35 | } 36 | 37 | genArrow = () => { 38 | const { allowDropdown, showDropdown } = this.props 39 | const arrowClasses = classNames('arrow', showDropdown ? 'up' : 'down') 40 | 41 | return allowDropdown ?
    : null 42 | } 43 | 44 | genFlagClassName = () => 45 | classNames('iti-flag', { 46 | [this.props.countryCode]: !!this.props.countryCode, 47 | }) 48 | 49 | genCountryList = () => { 50 | const { 51 | dropdownContainer, 52 | showDropdown, 53 | isMobile, 54 | allowDropdown, 55 | setFlag, 56 | countries, 57 | inputTop, 58 | inputOuterHeight, 59 | preferredCountries, 60 | highlightedCountry, 61 | changeHighlightCountry, 62 | } = this.props 63 | 64 | return ( 65 | { 67 | this.countryList = countryList 68 | }} 69 | dropdownContainer={dropdownContainer} 70 | isMobile={isMobile} 71 | showDropdown={allowDropdown && showDropdown} 72 | setFlag={setFlag} 73 | countries={countries} 74 | inputTop={inputTop} 75 | inputOuterHeight={inputOuterHeight} 76 | preferredCountries={preferredCountries} 77 | highlightedCountry={highlightedCountry} 78 | changeHighlightCountry={changeHighlightCountry} 79 | /> 80 | ) 81 | } 82 | 83 | render() { 84 | const { 85 | refCallback, 86 | allowDropdown, 87 | clickSelectedFlag, 88 | handleSelectedFlagKeydown, 89 | titleTip, 90 | dropdownContainer, 91 | showDropdown, 92 | } = this.props 93 | 94 | return ( 95 |
    96 |
    103 |
    104 | {this.genSelectedDialCode()} 105 | {this.genArrow()} 106 |
    107 | {dropdownContainer && showDropdown ? ( 108 | {this.genCountryList()} 109 | ) : ( 110 | this.genCountryList() 111 | )} 112 |
    113 | ) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/CountryList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import utils from './utils' 5 | 6 | import FlagBox from './FlagBox' 7 | 8 | export default class CountryList extends Component { 9 | static propTypes = { 10 | setFlag: PropTypes.func, 11 | countries: PropTypes.arrayOf(PropTypes.object), 12 | inputTop: PropTypes.number, 13 | inputOuterHeight: PropTypes.number, 14 | preferredCountries: PropTypes.arrayOf(PropTypes.object), 15 | highlightedCountry: PropTypes.number, 16 | changeHighlightCountry: PropTypes.func, 17 | showDropdown: PropTypes.bool, 18 | isMobile: PropTypes.bool, 19 | } 20 | 21 | shouldComponentUpdate(nextProps) { 22 | const shouldUpdate = !utils.shallowEquals(this.props, nextProps) 23 | 24 | if (shouldUpdate && nextProps.showDropdown) { 25 | this.listElement.classList.add('v-hide') 26 | this.setDropdownPosition() 27 | } 28 | 29 | return shouldUpdate 30 | } 31 | 32 | setDropdownPosition = () => { 33 | this.listElement.classList.remove('hide') 34 | const inputTop = this.props.inputTop 35 | const windowTop = 36 | window.pageYOffset !== undefined 37 | ? window.pageYOffset 38 | : ( 39 | document.documentElement || 40 | document.body.parentNode || 41 | document.body 42 | ).scrollTop 43 | const windowHeight = 44 | window.innerHeight || 45 | document.documentElement.clientHeight || 46 | document.body.clientHeight 47 | const inputOuterHeight = this.props.inputOuterHeight 48 | const countryListOuterHeight = utils.getOuterHeight(this.listElement) 49 | const dropdownFitsBelow = 50 | inputTop + inputOuterHeight + countryListOuterHeight < 51 | windowTop + windowHeight 52 | const dropdownFitsAbove = inputTop - countryListOuterHeight > windowTop 53 | 54 | // dropdownHeight - 1 for border 55 | const cssTop = 56 | !dropdownFitsBelow && dropdownFitsAbove 57 | ? `-${countryListOuterHeight - 1}px` 58 | : '' 59 | 60 | this.listElement.style.top = cssTop 61 | this.listElement.classList.remove('v-hide') 62 | } 63 | 64 | appendListItem = (countries, isPreferred = false) => { 65 | const preferredCountriesCount = this.props.preferredCountries.length 66 | 67 | return countries.map((country, index) => { 68 | const actualIndex = isPreferred ? index : index + preferredCountriesCount 69 | const countryClassObj = { 70 | country: true, 71 | highlight: this.props.highlightedCountry === actualIndex, 72 | preferred: isPreferred, 73 | } 74 | const countryClass = classNames(countryClassObj) 75 | const onMouseOverOrFocus = this.props.isMobile 76 | ? () => {} 77 | : this.handleMouseOver 78 | const keyPrefix = isPreferred ? 'pref-' : '' 79 | 80 | return ( 81 | this.props.setFlag(country.iso2)} 88 | onFocus={onMouseOverOrFocus} 89 | flagRef={selectedFlag => { 90 | this.selectedFlag = selectedFlag 91 | }} 92 | innerFlagRef={selectedFlagInner => { 93 | this.selectedFlagInner = selectedFlagInner 94 | }} 95 | countryClass={countryClass} 96 | /> 97 | ) 98 | }) 99 | } 100 | 101 | handleMouseOver = e => { 102 | if (e.currentTarget.getAttribute('class').indexOf('country') > -1) { 103 | const selectedIndex = utils.retrieveLiIndex(e.currentTarget) 104 | 105 | this.props.changeHighlightCountry(true, selectedIndex) 106 | } 107 | } 108 | 109 | render() { 110 | const { preferredCountries, countries, showDropdown } = this.props 111 | const className = classNames('country-list', { 112 | hide: !showDropdown, 113 | }) 114 | 115 | const preferredOptions = this.appendListItem(preferredCountries, true) 116 | const allOptions = this.appendListItem(countries) 117 | const divider =
    118 | 119 | return ( 120 |
      { 122 | this.listElement = listElement 123 | }} 124 | className={className} 125 | > 126 | {preferredOptions} 127 | {preferredCountries.length > 0 ? divider : null} 128 | {allOptions} 129 |
    130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-intl-tel-input", 3 | "version": "0.0.0", 4 | "description": "Telephone input component. Rewrite intl-tel-input in React.js.", 5 | "author": "patw", 6 | "workspaces": [ 7 | "website" 8 | ], 9 | "contributors": [ 10 | { 11 | "name": "Marc Cataford", 12 | "email": "mcat@riseup.net", 13 | "url": "https://mcataford.github.io" 14 | } 15 | ], 16 | "keywords": [ 17 | "react", 18 | "react-component", 19 | "tel", 20 | "telephone", 21 | "intl-tel-input", 22 | "international-telephone-input", 23 | "phonenumber" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/patw0929/react-intl-tel-input" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/patw0929/react-intl-tel-input/issues" 31 | }, 32 | "main": "dist/index.js", 33 | "types": "dist/index.d.ts", 34 | "peerDependencies": { 35 | "react": ">15.4.2 <17.0.0", 36 | "react-dom": ">15.4.2 <17.0.0" 37 | }, 38 | "files": [ 39 | "dist/**/*" 40 | ], 41 | "dependencies": { 42 | "classnames": "^2.2.5", 43 | "libphonenumber-js-utils": "^8.10.5", 44 | "prop-types": "^15.6.1", 45 | "react-style-proptype": "^3.0.0", 46 | "underscore.deferred": "^0.4.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.8.4", 50 | "@babel/core": "^7.2.0", 51 | "@babel/plugin-proposal-class-properties": "^7.2.1", 52 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 53 | "@babel/polyfill": "^7.0.0", 54 | "@babel/preset-env": "^7.2.0", 55 | "@babel/preset-react": "^7.0.0", 56 | "@commitlint/cli": "^8.3.5", 57 | "@commitlint/config-conventional": "^8.3.4", 58 | "@types/jquery": "^3.5.6", 59 | "@types/node": "^14.0.13", 60 | "@types/react": "^16.9.56", 61 | "@types/react-dom": "^16.9.9", 62 | "@typescript-eslint/eslint-plugin": "^4.31.1", 63 | "@typescript-eslint/parser": "^4.31.1", 64 | "babel-core": "^7.0.0-bridge.0", 65 | "babel-eslint": "^10.0.1", 66 | "babel-jest": "^23.6.0", 67 | "babel-plugin-dynamic-import-node": "^2.2.0", 68 | "babel-plugin-react-docgen": "^2.0.2", 69 | "babel-plugin-transform-react-remove-prop-types": "^0.4.21", 70 | "coveralls": "^2.11.9", 71 | "enzyme": "^3.3.0", 72 | "enzyme-adapter-react-16": "^1.5.0", 73 | "eslint": "^7.32.0", 74 | "eslint-config-airbnb": "~17.1.0", 75 | "eslint-config-airbnb-base": "~13.1.0", 76 | "eslint-config-prettier": "^6.10.0", 77 | "eslint-import-resolver-typescript": "^2.4.0", 78 | "eslint-loader": "^2.1.1", 79 | "eslint-plugin-import": "^2.23.4", 80 | "eslint-plugin-jsx-a11y": "^6.1.1", 81 | "eslint-plugin-prettier": "^3.1.2", 82 | "eslint-plugin-react": "^7.11.0", 83 | "eslint-plugin-security": "^1.3.0", 84 | "husky": "^4.2.3", 85 | "identity-obj-proxy": "^3.0.0", 86 | "jasmine-reporters": "^2.2.0", 87 | "jest": "^23.6.0", 88 | "jsdom": "^9.2.1", 89 | "lint-staged": "^11.1.1", 90 | "packwatch": "^1.0.0", 91 | "prettier": "^1.14.2", 92 | "prettier-eslint": "^9.0.1", 93 | "react": "^16.4.1", 94 | "react-dom": "^16.4.1", 95 | "rimraf": "2.5.4", 96 | "sass": "^1.37.4", 97 | "semantic-release": "^17.0.4", 98 | "sinon": "^1.17.4", 99 | "typescript": "^4.4.3" 100 | }, 101 | "scripts": { 102 | "prebuild": "yarn run clean", 103 | "build": "yarn compile:js && yarn compile:dts && yarn compile:css && yarn compile:png", 104 | "compile:js": "BABEL_ENV=production babel src -d dist", 105 | "compile:dts": "rsync -avh --include='*/' --include='*.d.ts' --exclude='*' src/ dist --prune-empty-dirs", 106 | "compile:css": "sass ./src/intlTelInput.scss ./dist/main.css", 107 | "compile:png": "cp -r -v ./src/*.png ./dist", 108 | "clean": "rimraf dist", 109 | "website:start": "yarn workspace website run start", 110 | "website:dryrun": "yarn workspace website run deploy:dryrun", 111 | "website:deploy": "yarn workspace website run deploy", 112 | "lint": "eslint src website/.storybook --ext .js,.d.ts", 113 | "coverage": "yarn test --coverage", 114 | "coverage-upload": "NODE_ENV=development cat coverage/lcov.info | yarn coveralls", 115 | "test": "jest src", 116 | "test:watch": "jest src --watchAll --coverage", 117 | "test:ts": "tsc --project ./tsconfig.test.json", 118 | "test:ts-watch": "tsc --watch --project ./tsconfig.test.json", 119 | "footprint": "yarn build && yarn packwatch", 120 | "lint:commits": "yarn commitlint --from HEAD --to HEAD --verbose" 121 | }, 122 | "lint-staged": { 123 | "*.js": [ 124 | "yarn eslint --" 125 | ], 126 | "src/**/*.js": [ 127 | "yarn run test -- --bail --findRelatedTests" 128 | ] 129 | }, 130 | "husky": { 131 | "hooks": { 132 | "pre-commit": "lint-staged", 133 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 134 | } 135 | }, 136 | "prettier": { 137 | "semi": false, 138 | "printWidth": 80, 139 | "singleQuote": true, 140 | "trailingComma": "all" 141 | }, 142 | "engines": { 143 | "node": ">=6.14.14" 144 | }, 145 | "license": "MIT" 146 | } 147 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, arrow-parens, prefer-template, comma-dangle, object-shorthand, global-require, func-names, no-else-return, vars-on-top */ 2 | const webpack = require('webpack'); 3 | const paths = require('./paths'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | const safeParser = require('postcss-safe-parser'); 8 | const getClientEnvironment = require('./env'); 9 | 10 | // Webpack uses `publicPath` to determine where the app is being served from. 11 | // In development, we always serve from the root. This makes config easier. 12 | const publicPath = ''; 13 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 14 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 15 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 16 | const publicUrl = ''; 17 | // Get environment variables to inject into our app. 18 | const env = getClientEnvironment(publicUrl); 19 | 20 | // Assert this just to be safe. 21 | // Development builds of React are slow and not intended for production. 22 | if (env['process.env'].NODE_ENV !== '"production"') { 23 | throw new Error('Production builds must have NODE_ENV=production.'); 24 | } 25 | 26 | module.exports = { 27 | mode: 'production', 28 | devtool: false, 29 | entry: { 30 | main: './src/components/IntlTelInput.js', 31 | }, 32 | output: { 33 | path: paths.appDist, 34 | pathinfo: true, 35 | filename: '[name].js', 36 | chunkFilename: '[name].bundle.js', 37 | publicPath: publicPath, 38 | library: 'IntlTelInput', 39 | libraryTarget: 'umd', 40 | globalObject: env.getGlobalObject(), 41 | }, 42 | 43 | externals: { 44 | react: { 45 | root: 'React', 46 | commonjs2: 'react', 47 | commonjs: 'react', 48 | amd: 'react', 49 | }, 50 | 'react-dom': { 51 | root: 'ReactDOM', 52 | commonjs2: 'react-dom', 53 | commonjs: 'react-dom', 54 | amd: 'react-dom', 55 | }, 56 | 'prop-types': { 57 | root: 'PropTypes', 58 | commonjs2: 'prop-types', 59 | commonjs: 'prop-types', 60 | amd: 'prop-types', 61 | }, 62 | 'libphonenumber-js-utils': { 63 | root: 'intlTelInputUtils', 64 | commonjs2: 'libphonenumber-js-utils', 65 | commonjs: 'libphonenumber-js-utils', 66 | amd: 'libphonenumber-js-utils', 67 | }, 68 | }, 69 | 70 | resolve: { 71 | modules: ['src', 'node_modules', ...paths.nodePaths], 72 | alias: { 73 | 'react-intl-tel-input': './components/IntlTelInput.js', 74 | }, 75 | }, 76 | module: { 77 | rules: [ 78 | { 79 | test: /\.(js|jsx)$/, 80 | loader: 'eslint-loader', 81 | enforce: 'pre', 82 | include: paths.appSrc, 83 | }, 84 | { 85 | exclude: [ 86 | /\.html$/, 87 | /\.(js|jsx)$/, 88 | /\.css$/, 89 | /\.scss$/, 90 | /\.json$/, 91 | /\.png$/, 92 | /\.svg$/, 93 | ], 94 | loader: 'url-loader', 95 | options: { 96 | limit: 10000, 97 | name: 'media/[name].[hash:8].[ext]', 98 | }, 99 | }, 100 | { 101 | test: /\.(js|jsx)$/, 102 | include: paths.appSrc, 103 | loader: 'babel-loader', 104 | }, 105 | { 106 | test: /\.scss$/, 107 | use: [ 108 | MiniCssExtractPlugin.loader, 109 | 'css-loader', 110 | 'sass-loader?outputStyle=expanded', 111 | ], 112 | }, 113 | { 114 | test: /\.css$/, 115 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 116 | }, 117 | { 118 | test: /\.(jpe?g|png|gif|svg)$/i, 119 | use: [ 120 | 'file-loader?name=[name].[ext]', 121 | { 122 | loader: 'image-webpack-loader', 123 | options: { 124 | pngquant: { 125 | quality: '30-40', 126 | speed: 1, 127 | }, 128 | }, 129 | }, 130 | ], 131 | }, 132 | ], 133 | }, 134 | 135 | optimization: { 136 | minimizer: [ 137 | new UglifyJsPlugin({ 138 | cache: true, 139 | parallel: true, 140 | uglifyOptions: { 141 | compress: true, 142 | ecma: 6, 143 | mangle: true, 144 | output: { 145 | comments: false, 146 | beautify: false, 147 | }, 148 | }, 149 | sourceMap: false, 150 | }), 151 | new OptimizeCssAssetsPlugin({ 152 | cssProcessorOptions: { 153 | parser: safeParser, 154 | discardComments: { 155 | removeAll: true, 156 | }, 157 | }, 158 | }), 159 | ], 160 | }, 161 | 162 | plugins: [ 163 | new webpack.LoaderOptionsPlugin({ 164 | minimize: true, 165 | debug: false, 166 | }), 167 | new webpack.DefinePlugin(env), 168 | new MiniCssExtractPlugin({ 169 | filename: 'main.css', 170 | }), 171 | ], 172 | node: { 173 | fs: 'empty', 174 | net: 'empty', 175 | tls: 'empty', 176 | }, 177 | }; 178 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | on: 3 | push: 4 | branches: 5 | master 6 | pull_request: 7 | 8 | env: 9 | NODE_VERSION: 14 10 | 11 | jobs: 12 | setup: 13 | runs-on: ubuntu-latest 14 | name: Setup 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | id: node-setup 19 | with: 20 | node-version: ${{ env.NODE_VERSION }} 21 | - uses: actions/cache@v2 22 | id: yarn-cache-restore 23 | with: 24 | path: | 25 | node_modules 26 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 27 | - name: Install dependencies 28 | if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 29 | run: yarn 30 | lint: 31 | runs-on: ubuntu-latest 32 | name: Lint 33 | needs: setup 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-node@v2 37 | id: node-setup 38 | with: 39 | node-version: ${{ env.NODE_VERSION }} 40 | - name: Yarn cache 41 | uses: actions/cache@v2 42 | id: yarn-cache-restore 43 | with: 44 | path: | 45 | node_modules 46 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 47 | - if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 48 | run: yarn 49 | - name: Lint 50 | run: | 51 | yarn lint 52 | test: 53 | runs-on: ubuntu-latest 54 | name: Test 55 | needs: setup 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions/setup-node@v2 59 | id: node-setup 60 | with: 61 | node-version: ${{ env.NODE_VERSION }} 62 | - name: Yarn cache 63 | uses: actions/cache@v2 64 | id: yarn-cache-restore 65 | with: 66 | path: | 67 | node_modules 68 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 69 | - if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 70 | run: yarn 71 | - name: Tests 72 | run: | 73 | yarn coverage 74 | - name: Coverage upload 75 | run: | 76 | yarn coverage-upload 77 | build: 78 | runs-on: ubuntu-latest 79 | name: Build 80 | needs: setup 81 | steps: 82 | - uses: actions/checkout@v2 83 | - uses: actions/setup-node@v2 84 | id: node-setup 85 | with: 86 | node-version: ${{ env.NODE_VERSION }} 87 | - name: Yarn cache 88 | uses: actions/cache@v2 89 | id: yarn-cache-restore 90 | with: 91 | path: | 92 | node_modules 93 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 94 | - if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 95 | run: yarn 96 | - run: | 97 | yarn build 98 | - name: Build Artifacts 99 | uses: actions/upload-artifact@v2 100 | with: 101 | name: build-artifacts 102 | path: dist 103 | release: 104 | runs-on: ubuntu-latest 105 | name: Release 106 | needs: 107 | - build 108 | - test 109 | - lint 110 | steps: 111 | - uses: actions/checkout@v2 112 | - uses: actions/setup-node@v2 113 | id: node-setup 114 | with: 115 | node-version: ${{ env.NODE_VERSION }} 116 | - name: Yarn cache 117 | uses: actions/cache@v2 118 | id: yarn-cache-restore 119 | with: 120 | path: | 121 | node_modules 122 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 123 | - name: Build Artifacts 124 | uses: actions/download-artifact@v2 125 | with: 126 | name: build-artifacts 127 | path: dist 128 | - if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 129 | run: yarn 130 | - name: Release (dry) 131 | if: ${{ github.ref != 'refs/heads/master' }} 132 | run: | 133 | yarn semantic-release --ci --dry-run 134 | - name: Release 135 | if: ${{ github.ref == 'refs/heads/master' }} 136 | env: 137 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 138 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 139 | run: | 140 | yarn semantic-release --ci 141 | website-deploy: 142 | runs-on: ubuntu-latest 143 | name: Website deploy 144 | needs: setup 145 | steps: 146 | - uses: actions/checkout@v2 147 | - uses: actions/setup-node@v2 148 | id: node-setup 149 | with: 150 | node-version: ${{ env.NODE_VERSION }} 151 | - name: Yarn cache 152 | uses: actions/cache@v2 153 | id: yarn-cache-restore 154 | with: 155 | path: | 156 | node_modules 157 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ env.NODE_VERSION }} 158 | - if: steps.yarn-cache-restore.outputs.cache-hit != 'true' 159 | run: yarn 160 | - name: Deploy (dry) 161 | if: ${{ github.ref != 'refs/heads/master' }} 162 | run: | 163 | yarn website:dryrun 164 | - name: Deploy 165 | if: ${{ github.ref == 'refs/heads/master' }} 166 | env: 167 | GH_TOKEN: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} 168 | run: | 169 | yarn website:deploy 170 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-airbnb', 'plugin:prettier/recommended'], 3 | plugins: ['react', 'import', 'security', 'prettier'], 4 | parser: 'babel-eslint', 5 | env: { 6 | browser: true, 7 | node: true, 8 | es6: true, 9 | jest: true, 10 | }, 11 | rules: { 12 | 'prettier/prettier': 'error', 13 | 'prefer-destructuring': 'off', 14 | 'jsx-a11y/click-events-have-key-events': 'off', 15 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 16 | 'jsx-a11y/no-autofocus': 'off', 17 | 'jsx-a11y/no-noninteractive-tabindex': 'off', 18 | 'jsx-a11y/anchor-has-content': 'off', 19 | 'jsx-a11y/no-static-element-interactions': 'off', 20 | 'react/destructuring-assignment': 'off', 21 | 'react/jsx-no-bind': 'error', 22 | 'react/no-multi-comp': 'off', 23 | 'no-restricted-syntax': [ 24 | 'error', 25 | 'DebuggerStatement', 26 | 'ForInStatement', 27 | 'WithStatement', 28 | ], 29 | 'comma-dangle': ['error', 'always-multiline'], // https://github.com/airbnb/javascript/commit/788208295469e19b806c06e01095dc8ba1b6cdc9 30 | 'no-underscore-dangle': 'off', 31 | 'react/require-default-props': 'off', 32 | 'react/jsx-curly-spacing': 'off', 33 | 'arrow-body-style': 'off', 34 | 'no-mixed-operators': [ 35 | 'error', 36 | { 37 | groups: [ 38 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 39 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 40 | ['&&', '||'], 41 | ['in', 'instanceof'], 42 | ], 43 | allowSamePrecedence: true, 44 | }, 45 | ], 46 | 'react/jsx-filename-extension': [ 47 | 'error', 48 | { extensions: ['.js', '.jsx', '.tsx'] }, 49 | ], 50 | 'react/no-string-refs': 'off', 51 | 'no-param-reassign': 'off', 52 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 53 | 'import/no-unresolved': [ 54 | 2, 55 | { ignore: ['react', 'react-dom', 'react-intl-tel-input'] }, 56 | ], 57 | 'import/extensions': 'off', 58 | 'import/no-extraneous-dependencies': [ 59 | 'error', 60 | { 61 | devDependencies: [ 62 | 'test/**', // tape, common npm pattern 63 | 'tests/**', // also common npm pattern 64 | 'spec/**', // mocha, rspec-like pattern 65 | '**/__tests__/**', // jest pattern 66 | '**/__mocks__/**', // jest pattern 67 | 'test.js', // repos with a single test file 68 | 'test-*.js', // repos with multiple top-level test files 69 | '**/*.test.js', // tests where the extension denotes that it is a test 70 | '**/webpack.config.js', // webpack config 71 | '**/webpack.config.*.js', // webpack config 72 | 'config/jest/**', 73 | 'src/testUtils/**', 74 | '*.js', 75 | ], 76 | optionalDependencies: false, 77 | }, 78 | ], 79 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 80 | }, 81 | globals: { 82 | __DEVELOPMENT__: true, 83 | __CLIENT__: true, 84 | __SERVER__: true, 85 | __DISABLE_SSR__: true, 86 | __DEVTOOLS__: true, 87 | }, 88 | overrides: [ 89 | // typescript common config 90 | { 91 | extends: [ 92 | 'eslint:recommended', 93 | 'plugin:@typescript-eslint/eslint-recommended', 94 | 'plugin:@typescript-eslint/recommended', 95 | ], 96 | files: ['**/*.d.ts', '**/*.test.ts', '**/*.test.tsx'], 97 | parser: '@typescript-eslint/parser', 98 | plugins: [ 99 | '@typescript-eslint', 100 | 'eslint-plugin-import', 101 | 'eslint-plugin-react', 102 | ], 103 | rules: { 104 | '@typescript-eslint/explicit-module-boundary-types': 'off', 105 | '@typescript-eslint/no-explicit-any': 'off', 106 | '@typescript-eslint/no-empty-interface': 'off', 107 | 'import/order': [ 108 | 'error', 109 | { 110 | alphabetize: { 111 | order: 'asc', 112 | }, 113 | groups: [['external', 'builtin'], 'parent', 'sibling'], 114 | 'newlines-between': 'always', 115 | }, 116 | ], 117 | 'react/jsx-first-prop-new-line': [1, 'multiline'], 118 | 'react/jsx-max-props-per-line': [ 119 | 1, 120 | { 121 | maximum: 1, 122 | }, 123 | ], 124 | 'react/sort-comp': [ 125 | 2, 126 | { 127 | order: [ 128 | 'static-methods', 129 | 'instance-variables', 130 | 'instance-methods', 131 | 'lifecycle', 132 | 'everything-else', 133 | 'render', 134 | ], 135 | }, 136 | ], 137 | 'spaced-comment': [ 138 | 'error', 139 | 'always', 140 | { 141 | line: { 142 | markers: ['#region', '#endregion', 'region', 'endregion'], 143 | }, 144 | }, 145 | ], 146 | }, 147 | settings: { 148 | 'import/resolver': 'eslint-import-resolver-typescript', 149 | }, 150 | }, 151 | // typescript test-only config 152 | { 153 | files: ['**/*.test.ts', '**/*.test.tsx'], 154 | rules: { 155 | 'no-use-before-define': 'off', 156 | 'no-console': 'off', // we want to be able to output results for tsc purposes 157 | }, 158 | }, 159 | ], 160 | } 161 | -------------------------------------------------------------------------------- /src/components/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom' 2 | import AllCountries from '../AllCountries' 3 | import utils from '../utils' 4 | 5 | describe('utils', () => { 6 | it('arraysEqual', () => { 7 | let a = [1, 2, 3] 8 | let b = a 9 | 10 | expect(utils.arraysEqual(a, b)).toBeTruthy() 11 | 12 | a = [1, 2, 3] 13 | b = null 14 | expect(utils.arraysEqual(a, b)).toBeFalsy() 15 | 16 | a = [1, 2, 3] 17 | b = [2, 1, 4, 5] 18 | expect(utils.arraysEqual(a, b)).toBeFalsy() 19 | 20 | a = ['1', '2', '3'] 21 | b = ['3', '1', '2'] 22 | expect(utils.arraysEqual(a, b)).toBeFalsy() 23 | }) 24 | 25 | it('shallowEquals', () => { 26 | let a = [1, 2, 3] 27 | let b = a 28 | 29 | expect(utils.shallowEquals(a, b)).toBeTruthy() 30 | 31 | a = { 32 | x: ['1', '2', '3'], 33 | y: 'abc', 34 | } 35 | b = { 36 | x: ['1', '2', '3'], 37 | y: 'abc', 38 | } 39 | expect(utils.shallowEquals(a, b)).toBeTruthy() 40 | 41 | a = { 42 | x: ['1', '2', '3'], 43 | y: ['1', '3', '4'], 44 | } 45 | b = { 46 | x: ['1', '2', '3'], 47 | y: ['4', '2'], 48 | } 49 | expect(utils.shallowEquals(a, b)).toBeFalsy() 50 | 51 | a = { 52 | a: 1, 53 | b: 2, 54 | } 55 | b = Object.create(a) 56 | b.c = 3 57 | expect(utils.shallowEquals(a, b)).toBeFalsy() 58 | }) 59 | 60 | it('trim', () => { 61 | expect(utils.trim(undefined)).toBe('') 62 | 63 | const str = ' Hello World ' 64 | 65 | expect(utils.trim(str)).toBe('Hello World') 66 | }) 67 | 68 | it('isNumeric', () => { 69 | const num = 1.2 70 | 71 | expect(utils.isNumeric(num)).toBeTruthy() 72 | }) 73 | 74 | it('retrieveLiIndex', () => { 75 | const DEFAULT_HTML = ` 76 |
      77 |
    • a
    • 78 |
    • b
    • 79 |
    80 | ` 81 | const doc = jsdom.jsdom(DEFAULT_HTML) 82 | const bListItem = doc.querySelector('.b') 83 | 84 | expect(utils.retrieveLiIndex(bListItem)).toBe(1) 85 | 86 | const otherListItem = doc.querySelector('.z') 87 | 88 | expect(utils.retrieveLiIndex(otherListItem)).toBe(-1) 89 | }) 90 | 91 | it('getNumeric', () => { 92 | const str = 'Hello 1000 World' 93 | 94 | expect(utils.getNumeric(str)).toBe('1000') 95 | }) 96 | 97 | it('startsWith', () => { 98 | const str = 'Hello World' 99 | 100 | expect(utils.startsWith(str, 'H')).toBeTruthy() 101 | }) 102 | 103 | it('isWindow', () => { 104 | expect(utils.isWindow(global.window)).toBeTruthy() 105 | }) 106 | 107 | it('getWindow', () => { 108 | expect(utils.getWindow(global.window)).toBe(global.window) 109 | }) 110 | 111 | it('getCountryData', () => { 112 | const result = { 113 | name: 'Taiwan (台灣)', 114 | iso2: 'tw', 115 | dialCode: '886', 116 | priority: 0, 117 | areaCodes: null, 118 | } 119 | 120 | expect(utils.getCountryData(AllCountries.getCountries())).toStrictEqual({}) 121 | expect( 122 | utils.getCountryData(AllCountries.getCountries(), undefined, true, true), 123 | ).toEqual(null) 124 | expect(utils.getCountryData(AllCountries.getCountries(), 'tw')).toEqual( 125 | result, 126 | ) 127 | expect( 128 | utils.getCountryData(AllCountries.getCountries(), 'zz', true, true), 129 | ).toBeNull() 130 | expect( 131 | utils.getCountryData( 132 | AllCountries.getCountries(), 133 | 'zz', 134 | false, 135 | false, 136 | country => `${country}!!`, 137 | ), 138 | ).toStrictEqual({}) 139 | }) 140 | 141 | it('findIndex', () => { 142 | let array = [] 143 | let predicate = () => true 144 | 145 | expect(utils.findIndex(array, predicate)).toEqual(-1) 146 | 147 | array = [1, 2, 3] 148 | predicate = item => item === 2 149 | 150 | expect(utils.findIndex(array, predicate)).toEqual(1) 151 | 152 | array = [1, 2, 3] 153 | predicate = item => item === 4 154 | 155 | expect(utils.findIndex(array, predicate)).toEqual(-1) 156 | }) 157 | 158 | it('getCursorPositionAfterFormating', () => { 159 | let previousStringBeforeCursor = '9123' 160 | let previousString = '912345' 161 | let nextString = '912345' 162 | 163 | expect( 164 | utils.getCursorPositionAfterFormating( 165 | previousStringBeforeCursor, 166 | previousString, 167 | nextString, 168 | ), 169 | ).toEqual(4) 170 | 171 | previousStringBeforeCursor = '0912 345' 172 | previousString = '0912 345 678' 173 | nextString = '91234678' 174 | 175 | expect( 176 | utils.getCursorPositionAfterFormating( 177 | previousStringBeforeCursor, 178 | previousString, 179 | nextString, 180 | ), 181 | ).toEqual(5) 182 | 183 | previousStringBeforeCursor = '91234' 184 | previousString = '91234678' 185 | nextString = '0912 345 678' 186 | 187 | expect( 188 | utils.getCursorPositionAfterFormating( 189 | previousStringBeforeCursor, 190 | previousString, 191 | nextString, 192 | ), 193 | ).toEqual(7) 194 | 195 | previousStringBeforeCursor = '(201) 5' 196 | previousString = '(201) 55-01' 197 | nextString = '201-5501' 198 | 199 | expect( 200 | utils.getCursorPositionAfterFormating( 201 | previousStringBeforeCursor, 202 | previousString, 203 | nextString, 204 | ), 205 | ).toEqual(5) 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /src/components/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import AllCountries from './AllCountries'; 3 | 4 | export default { 5 | arraysEqual(a, b) { 6 | if (a === b) { 7 | return true; 8 | } 9 | if (a === null || b === null) { 10 | return false; 11 | } 12 | if (a.length !== b.length) { 13 | return false; 14 | } 15 | 16 | // If you don't care about the order of the elements inside 17 | // the array, you should sort both arrays here. 18 | 19 | for (let i = 0; i < a.length; ++i) { 20 | if (a[i] !== b[i]) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | }, 27 | 28 | shallowEquals(a, b) { 29 | if (a === b) { 30 | return true; 31 | } 32 | 33 | for (const key in a) { 34 | if (a[key] !== b[key]) { 35 | if (Array.isArray(a[key]) && Array.isArray(b[key])) { 36 | if (!this.arraysEqual(a[key], b[key])) { 37 | return false; 38 | } 39 | } else { 40 | return false; 41 | } 42 | } 43 | } 44 | 45 | for (const key in b) { 46 | if (a.hasOwnProperty(key) === false) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | }, 52 | 53 | trim(str) { 54 | // Make sure we trim BOM and NBSP 55 | const rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; 56 | 57 | if (!str) { 58 | return ''; 59 | } 60 | 61 | return str.replace(rtrim, ''); 62 | }, 63 | 64 | isNumeric(obj) { 65 | return obj - parseFloat(obj) >= 0; 66 | }, 67 | 68 | retrieveLiIndex(node) { 69 | if (!node) { 70 | return -1; 71 | } 72 | 73 | const children = node.parentNode.childNodes; 74 | let num = 0; 75 | 76 | for (let i = 0, max = children.length; i < max; i++) { 77 | if (children[i] === node) { 78 | return num; 79 | } 80 | 81 | if ( 82 | children[i].nodeType === 1 && 83 | children[i].tagName.toLowerCase() === 'li' 84 | ) { 85 | num += 1; 86 | } 87 | } 88 | 89 | return -1; 90 | }, 91 | 92 | // extract the numeric digits from the given string 93 | getNumeric(s) { 94 | return s.replace(/\D/g, ''); 95 | }, 96 | 97 | // check if (uppercase) string a starts with string b 98 | startsWith(a, b) { 99 | return a.substr(0, b.length).toUpperCase() === b; 100 | }, 101 | 102 | isWindow(obj) { 103 | return obj !== null && obj === obj.window; 104 | }, 105 | 106 | getWindow(elem) { 107 | return this.isWindow(elem) ? elem : elem.nodeType === 9 && elem.defaultView; 108 | }, 109 | 110 | offset(elem) { 111 | let docElem = null; 112 | let win = null; 113 | let box = { top: 0, left: 0 }; 114 | const doc = elem && elem.ownerDocument; 115 | 116 | docElem = doc.documentElement; 117 | 118 | if (typeof elem.getBoundingClientRect !== typeof undefined) { 119 | box = elem.getBoundingClientRect(); 120 | } 121 | 122 | win = this.getWindow(doc); 123 | 124 | return { 125 | top: box.top + win.pageYOffset - docElem.clientTop, 126 | left: box.left + win.pageXOffset - docElem.clientLeft, 127 | }; 128 | }, 129 | 130 | // retrieve outerHeight of element 131 | getOuterHeight(element) { 132 | return ( 133 | element.offsetHeight + 134 | parseFloat( 135 | window.getComputedStyle(element).getPropertyValue('margin-top') 136 | ) + 137 | parseFloat( 138 | window.getComputedStyle(element).getPropertyValue('margin-bottom') 139 | ) 140 | ); 141 | }, 142 | 143 | // find the country data for the given country code 144 | // the ignoreOnlyCountriesOption is only used during init() 145 | // while parsing the onlyCountries array 146 | getCountryData( 147 | countries, 148 | countryCode, 149 | ignoreOnlyCountriesOption, 150 | allowFail, 151 | errorHandler 152 | ) { 153 | const countryList = ignoreOnlyCountriesOption 154 | ? AllCountries.getCountries() 155 | : countries; 156 | 157 | for (let i = 0; i < countryList.length; i++) { 158 | if (countryList[i].iso2 === countryCode) { 159 | return countryList[i]; 160 | } 161 | } 162 | 163 | if (allowFail) { 164 | return null; 165 | } 166 | 167 | if (typeof errorHandler === 'function') { 168 | errorHandler(countryCode); 169 | } 170 | 171 | return {}; 172 | }, 173 | 174 | findIndex(items, predicate) { 175 | let index = -1; 176 | 177 | items.some((item, i) => { 178 | if (predicate(item)) { 179 | index = i; 180 | 181 | return true; 182 | } 183 | }); 184 | 185 | return index; 186 | }, 187 | 188 | // Get the location of cursor after formatting is done on the phone number 189 | getCursorPositionAfterFormating(prevBeforeCursor, prev, next) { 190 | if (prev === next) { 191 | return prevBeforeCursor.length; 192 | } 193 | let cursorShift = 0; 194 | 195 | if (prev.length > next.length) { 196 | for ( 197 | let i = 0, j = 0; 198 | i < prevBeforeCursor.length && j < next.length; 199 | i += 1 200 | ) { 201 | if (prevBeforeCursor[i] !== next[j]) { 202 | if (isNaN(next[j]) && !isNaN(prevBeforeCursor[i])) { 203 | i -= 1; 204 | j += 1; 205 | cursorShift += 1; 206 | } else { 207 | cursorShift -= 1; 208 | } 209 | } else { 210 | j += 1; 211 | } 212 | } 213 | } else { 214 | for ( 215 | let i = 0, j = 0; 216 | i < prevBeforeCursor.length && j < next.length; 217 | j += 1 218 | ) { 219 | if (prevBeforeCursor[i] !== next[j]) { 220 | if (isNaN(prevBeforeCursor[i]) && !isNaN(next[j])) { 221 | j -= 1; 222 | i += 1; 223 | cursorShift -= 1; 224 | } else { 225 | cursorShift += 1; 226 | } 227 | } else { 228 | i += 1; 229 | } 230 | } 231 | } 232 | 233 | return prevBeforeCursor.length + cursorShift; 234 | }, 235 | }; 236 | -------------------------------------------------------------------------------- /src/intlTelInput.scss: -------------------------------------------------------------------------------- 1 | // rgba is needed for the selected flag hover state to blend in with 2 | // the border-highlighting some browsers give the input on focus 3 | $hoverColor: rgba(0, 0, 0, 0.05) !default; 4 | $greyText: #999 !default; 5 | $greyBorder: #CCC !default; 6 | 7 | $flagHeight: 15px !default; 8 | $flagWidth: 20px !default; 9 | $flagPadding: 8px !default; 10 | // this border width is used for the popup and divider, but it is also 11 | // assumed to be the border width of the input, which we do not control 12 | $borderWidth: 1px !default; 13 | 14 | $inputPadding: 6px !default; 15 | $selectedFlagWidth: $flagWidth + (2 * $flagPadding) !default; 16 | // 18px previously arrow width and padding, 6px ea. 17 | $selectedFlagArrowWidth: $flagWidth + $flagPadding + 18px !default; 18 | $selectedFlagArrowDialCodeWidth: $selectedFlagArrowWidth + $flagPadding !default; 19 | 20 | // enough space for them to click off to close 21 | $mobilePopupMargin: 30px; 22 | 23 | 24 | .intl-tel-input { 25 | // need position on the container so the selected flag can be 26 | // absolutely positioned over the input 27 | position: relative; 28 | // keep the input's default inline properties 29 | display: inline-block; 30 | 31 | // paul irish says this is ok 32 | // http://www.paulirish.com/2012/box-sizing-border-box-ftw/ 33 | * { 34 | box-sizing: border-box; 35 | -moz-box-sizing: border-box; 36 | } 37 | 38 | .hide { 39 | display: none; 40 | } 41 | // need this during init, to get the height of the dropdown 42 | .v-hide { 43 | visibility: hidden; 44 | } 45 | 46 | // specify types to increase specificity e.g. to override bootstrap v2.3 47 | input, input[type=text], input[type=tel] { 48 | position: relative; 49 | // input is bottom level, below selected flag and dropdown 50 | z-index: 0; 51 | 52 | // any vertical margin the user has on their inputs would no longer work as expected 53 | // because we wrap everything in a container div. i justify the use of !important 54 | // here because i don't think the user should ever have vertical margin here - when 55 | // the input is wrapped in a container, vertical margin messes up alignment with other 56 | // inline elements (e.g. an adjacent button) in firefox, and probably other browsers. 57 | margin-top: 0 !important; 58 | margin-bottom: 0 !important; 59 | 60 | // make space for the selected flag 61 | // Note: no !important here, as the user may want to tweak this so that the 62 | // perceived input padding matches their existing styles 63 | padding-right: $selectedFlagWidth; 64 | 65 | // any margin-right here will push the selected-flag away 66 | margin-right: 0; 67 | } 68 | 69 | .flag-container { 70 | // positioned over the top of the input 71 | position: absolute; 72 | // full height 73 | top: 0; 74 | bottom: 0; 75 | right: 0; 76 | // prevent the highlighted child from overlapping the input border 77 | padding: $borderWidth; 78 | 79 | .arrow { 80 | font-size: 6px; 81 | margin-left: 5px; 82 | 83 | &.up:after { 84 | content: '▲'; 85 | } 86 | 87 | &.down:after { 88 | content: '▼'; 89 | 90 | } 91 | } 92 | } 93 | 94 | .selected-flag { 95 | // render above the input 96 | z-index: 1; 97 | position: relative; 98 | width: $selectedFlagWidth; 99 | // this must be full-height both for the hover highlight, and to push down the 100 | // dropdown so it appears below the input 101 | height: 100%; 102 | 103 | display: flex; 104 | justify-content: center; 105 | align-items: center; 106 | } 107 | 108 | // the dropdown 109 | .country-list { 110 | position: absolute; 111 | // popup so render above everything else 112 | z-index: 2; 113 | 114 | // override default list styles 115 | list-style: none; 116 | // in case any container has text-align:center 117 | text-align: left; 118 | 119 | .divider { 120 | padding-bottom: 5px; 121 | margin-bottom: 5px; 122 | border-bottom: $borderWidth solid $greyBorder; 123 | } 124 | 125 | // place menu above the input element 126 | &.dropup { 127 | bottom: 100%; 128 | margin-bottom: (-$borderWidth); 129 | } 130 | 131 | // dropdown flags need consistent width, so wrap in a container 132 | .flag-box { 133 | display: inline-block; 134 | width: $flagWidth; 135 | } 136 | 137 | padding: 0; 138 | // margin-left to compensate for the padding on the parent 139 | margin: 0 0 0 (-$borderWidth); 140 | 141 | box-shadow: 1px 1px 4px rgba(0,0,0,0.2); 142 | background-color: white; 143 | border: $borderWidth solid $greyBorder; 144 | 145 | // don't let the contents wrap AKA the container will be as wide as the contents 146 | white-space: nowrap; 147 | // except on small screens, where we force the dropdown width to match the input 148 | @media (max-width: 500px) { 149 | white-space: normal; 150 | } 151 | 152 | max-height: 200px; 153 | overflow-y: scroll; 154 | -webkit-overflow-scrolling: touch; 155 | 156 | // each country item in dropdown (we must have separate class to differentiate from dividers) 157 | .country { 158 | // Note: decided not to use line-height here for alignment because it causes issues e.g. large font-sizes will overlap, and also looks bad if one country overflows onto 2 lines 159 | padding: 5px 10px; 160 | // the dial codes after the country names are greyed out 161 | .dial-code { 162 | color: $greyText; 163 | } 164 | } 165 | .country.highlight { 166 | background-color: $hoverColor; 167 | } 168 | 169 | // spacing between country flag, name and dial code 170 | .flag-box, .country-name, .dial-code { 171 | vertical-align: middle; 172 | } 173 | .flag-box, .country-name { 174 | margin-right: 6px; 175 | } 176 | } 177 | 178 | &.allow-dropdown { 179 | input, input[type=text], input[type=tel] { 180 | padding-right: $inputPadding; 181 | padding-left: $selectedFlagArrowWidth + $inputPadding; 182 | margin-left: 0; 183 | } 184 | .flag-container { 185 | right: auto; 186 | left: 0; 187 | width: 100%; 188 | } 189 | .selected-flag { 190 | width: $selectedFlagArrowWidth; 191 | } 192 | 193 | // hover state - show flag is clickable 194 | .flag-container:hover { 195 | cursor: pointer; 196 | .selected-flag { 197 | background-color: $hoverColor; 198 | } 199 | } 200 | // disable hover state when input is disabled 201 | input[disabled] + .flag-container:hover, input[readonly] + .flag-container:hover { 202 | cursor: default; 203 | .selected-flag { 204 | background-color: transparent; 205 | } 206 | } 207 | 208 | &.separate-dial-code { 209 | .selected-flag { 210 | // now that we have digits in this section, it needs this visual separation 211 | background-color: $hoverColor; 212 | // for vertical centering 213 | display: table; 214 | } 215 | .selected-dial-code { 216 | // for vertical centering 217 | display: table-cell; 218 | vertical-align: middle; 219 | 220 | padding-left: $flagWidth + $flagPadding; 221 | } 222 | 223 | // .iti-sdc is for Separate Dial Code, with lengths from 2-5 because shortest is "+1", longest is "+1684" 224 | $charLength: 8px; 225 | @for $i from 2 through 5 { 226 | &.iti-sdc-#{$i} { 227 | input, input[type=text], input[type=tel] { 228 | padding-left: $selectedFlagArrowDialCodeWidth + $inputPadding + ($i * $charLength); 229 | } 230 | .selected-flag { 231 | width: $selectedFlagArrowDialCodeWidth + ($i * $charLength); 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | // if dropdownContainer option is set, increase z-index to prevent display issues 239 | &.iti-container { 240 | position: absolute; 241 | top: -1000px; 242 | left: -1000px; 243 | // higher than default Bootstrap modal z-index of 1050 244 | z-index: 1060; 245 | // to keep styling consistent with .flag-container 246 | padding: $borderWidth; 247 | &:hover { 248 | cursor: pointer; 249 | } 250 | } 251 | } 252 | 253 | 254 | // overrides for mobile popup 255 | .iti-mobile .intl-tel-input { 256 | &.iti-container { 257 | top: $mobilePopupMargin; 258 | bottom: $mobilePopupMargin; 259 | left: $mobilePopupMargin; 260 | right: $mobilePopupMargin; 261 | position: fixed; 262 | } 263 | .country-list { 264 | max-height: 100%; 265 | width: 100%; 266 | -webkit-overflow-scrolling: touch; 267 | .country { 268 | padding: 10px 10px; 269 | // increase line height because dropdown copy is v likely to overflow on mobile and when it does it needs to be well spaced 270 | line-height: 1.5em; 271 | } 272 | } 273 | } 274 | 275 | 276 | 277 | @import "sprite"; 278 | 279 | $intl-tel-input-sprite-path: "./flags.png" !default; 280 | $intl-tel-input-sprite-2x-path: "./flags@2x.png" !default; 281 | 282 | .iti-flag { 283 | width: $flagWidth; 284 | height: $flagHeight; 285 | box-shadow: 0px 0px 1px 0px #888; 286 | background-image: url($intl-tel-input-sprite-path); 287 | background-repeat: no-repeat; 288 | // empty state 289 | background-color: #DBDBDB; 290 | background-position: $flagWidth 0; 291 | 292 | @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { 293 | background-image: url($intl-tel-input-sprite-2x-path); 294 | } 295 | } 296 | 297 | 298 | 299 | // hack for Nepal which is the only flag that is not square/rectangle, so it has transparency, so you can see the default grey behind it 300 | .iti-flag.np { 301 | background-color: transparent; 302 | } 303 | -------------------------------------------------------------------------------- /src/components/AllCountries.js: -------------------------------------------------------------------------------- 1 | // Tell JSHint to ignore this warning: 'character may get silently deleted by one or more browsers' 2 | // jshint -W100 3 | 4 | // Array of country objects for the flag dropdown. 5 | // Each contains a name, country code (ISO 3166-1 alpha-2) and dial code. 6 | 7 | // Originally from https://github.com/mledoze/countries 8 | // then with a couple of manual re-arrangements to be alphabetical 9 | // then changed Kazakhstan from +76 to +7 10 | // and Vatican City from +379 to +39 (see issue 50) 11 | // and Caribean Netherlands from +5997 to +599 12 | // and Curacao from +5999 to +599 13 | // Removed: Kosovo, Pitcairn Islands, South Georgia 14 | 15 | // UPDATE Sept 12th 2015 16 | // List of regions that have iso2 country codes, which I have chosen to omit: 17 | // (based on this information: https://en.wikipedia.org/wiki/List_of_country_calling_codes) 18 | // AQ - Antarctica - all different country codes depending on which 'base' 19 | // BV - Bouvet Island - no calling code 20 | // GS - South Georgia and the South Sandwich Islands - 21 | // 'inhospitable collection of islands' - same flag and calling code as Falkland Islands 22 | // HM - Heard Island and McDonald Islands - no calling code 23 | // PN - Pitcairn - tiny population (56), same calling code as New Zealand 24 | // TF - French Southern Territories - no calling code 25 | // UM - United States Minor Outlying Islands - no calling code 26 | 27 | // UPDATE the criteria of supported countries or territories (see issue 297) 28 | // Have an iso2 code: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 29 | // Have a country calling code: https://en.wikipedia.org/wiki/List_of_country_calling_codes 30 | // Have a flag 31 | // Must be supported by libphonenumber: https://github.com/googlei18n/libphonenumber 32 | 33 | // Update: converted objects to arrays to save bytes! 34 | // Update: added 'priority' for countries with the same dialCode as others 35 | // Update: added array of area codes for countries with the same dialCode as others 36 | 37 | // So each country array has the following information: 38 | // [ 39 | // Country name, 40 | // iso2 code, 41 | // International dial code, 42 | // Order (if >1 country with same dial code), 43 | // Area codes (if >1 country with same dial code) 44 | // ] 45 | const defaultCountriesData = [ 46 | ['Afghanistan (‫افغانستان‬‎)', 'af', '93'], 47 | ['Albania (Shqipëri)', 'al', '355'], 48 | ['Algeria (‫الجزائر‬‎)', 'dz', '213'], 49 | ['American Samoa', 'as', '1684'], 50 | ['Andorra', 'ad', '376'], 51 | ['Angola', 'ao', '244'], 52 | ['Anguilla', 'ai', '1264'], 53 | ['Antigua and Barbuda', 'ag', '1268'], 54 | ['Argentina', 'ar', '54'], 55 | ['Armenia (Հայաստան)', 'am', '374'], 56 | ['Aruba', 'aw', '297'], 57 | ['Australia', 'au', '61', 0], 58 | ['Austria (Österreich)', 'at', '43'], 59 | ['Azerbaijan (Azərbaycan)', 'az', '994'], 60 | ['Bahamas', 'bs', '1242'], 61 | ['Bahrain (‫البحرين‬‎)', 'bh', '973'], 62 | ['Bangladesh (বাংলাদেশ)', 'bd', '880'], 63 | ['Barbados', 'bb', '1246'], 64 | ['Belarus (Беларусь)', 'by', '375'], 65 | ['Belgium (België)', 'be', '32'], 66 | ['Belize', 'bz', '501'], 67 | ['Benin (Bénin)', 'bj', '229'], 68 | ['Bermuda', 'bm', '1441'], 69 | ['Bhutan (འབྲུག)', 'bt', '975'], 70 | ['Bolivia', 'bo', '591'], 71 | ['Bosnia and Herzegovina (Босна и Херцеговина)', 'ba', '387'], 72 | ['Botswana', 'bw', '267'], 73 | ['Brazil (Brasil)', 'br', '55'], 74 | ['British Indian Ocean Territory', 'io', '246'], 75 | ['British Virgin Islands', 'vg', '1284'], 76 | ['Brunei', 'bn', '673'], 77 | ['Bulgaria (България)', 'bg', '359'], 78 | ['Burkina Faso', 'bf', '226'], 79 | ['Burundi (Uburundi)', 'bi', '257'], 80 | ['Cambodia (កម្ពុជា)', 'kh', '855'], 81 | ['Cameroon (Cameroun)', 'cm', '237'], 82 | [ 83 | 'Canada', 84 | 'ca', 85 | '1', 86 | 1, 87 | [ 88 | '204', 89 | '226', 90 | '236', 91 | '249', 92 | '250', 93 | '289', 94 | '306', 95 | '343', 96 | '365', 97 | '367', 98 | '387', 99 | '403', 100 | '416', 101 | '418', 102 | '431', 103 | '437', 104 | '438', 105 | '450', 106 | '506', 107 | '514', 108 | '519', 109 | '548', 110 | '579', 111 | '581', 112 | '587', 113 | '604', 114 | '613', 115 | '639', 116 | '647', 117 | '672', 118 | '705', 119 | '709', 120 | '742', 121 | '778', 122 | '780', 123 | '782', 124 | '807', 125 | '819', 126 | '825', 127 | '867', 128 | '873', 129 | '902', 130 | '905', 131 | ], 132 | ], 133 | ['Cape Verde (Kabu Verdi)', 'cv', '238'], 134 | ['Caribbean Netherlands', 'bq', '599', 1], 135 | ['Cayman Islands', 'ky', '1345'], 136 | ['Central African Republic (République centrafricaine)', 'cf', '236'], 137 | ['Chad (Tchad)', 'td', '235'], 138 | ['Chile', 'cl', '56'], 139 | ['China (中国)', 'cn', '86'], 140 | ['Christmas Island', 'cx', '61', 2], 141 | ['Cocos (Keeling) Islands', 'cc', '61', 1], 142 | ['Colombia', 'co', '57'], 143 | ['Comoros (‫جزر القمر‬‎)', 'km', '269'], 144 | ['Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)', 'cd', '243'], 145 | ['Congo (Republic) (Congo-Brazzaville)', 'cg', '242'], 146 | ['Cook Islands', 'ck', '682'], 147 | ['Costa Rica', 'cr', '506'], 148 | ['Côte d’Ivoire', 'ci', '225'], 149 | ['Croatia (Hrvatska)', 'hr', '385'], 150 | ['Cuba', 'cu', '53'], 151 | ['Curaçao', 'cw', '599', 0], 152 | ['Cyprus (Κύπρος)', 'cy', '357'], 153 | ['Czech Republic (Česká republika)', 'cz', '420'], 154 | ['Denmark (Danmark)', 'dk', '45'], 155 | ['Djibouti', 'dj', '253'], 156 | ['Dominica', 'dm', '1767'], 157 | [ 158 | 'Dominican Republic (República Dominicana)', 159 | 'do', 160 | '1', 161 | 2, 162 | ['809', '829', '849'], 163 | ], 164 | ['Ecuador', 'ec', '593'], 165 | ['Egypt (‫مصر‬‎)', 'eg', '20'], 166 | ['El Salvador', 'sv', '503'], 167 | ['Equatorial Guinea (Guinea Ecuatorial)', 'gq', '240'], 168 | ['Eritrea', 'er', '291'], 169 | ['Estonia (Eesti)', 'ee', '372'], 170 | ['Ethiopia', 'et', '251'], 171 | ['Falkland Islands (Islas Malvinas)', 'fk', '500'], 172 | ['Faroe Islands (Føroyar)', 'fo', '298'], 173 | ['Fiji', 'fj', '679'], 174 | ['Finland (Suomi)', 'fi', '358', 0], 175 | ['France', 'fr', '33'], 176 | ['French Guiana (Guyane française)', 'gf', '594'], 177 | ['French Polynesia (Polynésie française)', 'pf', '689'], 178 | ['Gabon', 'ga', '241'], 179 | ['Gambia', 'gm', '220'], 180 | ['Georgia (საქართველო)', 'ge', '995'], 181 | ['Germany (Deutschland)', 'de', '49'], 182 | ['Ghana (Gaana)', 'gh', '233'], 183 | ['Gibraltar', 'gi', '350'], 184 | ['Greece (Ελλάδα)', 'gr', '30'], 185 | ['Greenland (Kalaallit Nunaat)', 'gl', '299'], 186 | ['Grenada', 'gd', '1473'], 187 | ['Guadeloupe', 'gp', '590', 0], 188 | ['Guam', 'gu', '1671'], 189 | ['Guatemala', 'gt', '502'], 190 | ['Guernsey', 'gg', '44', 1], 191 | ['Guinea (Guinée)', 'gn', '224'], 192 | ['Guinea-Bissau (Guiné Bissau)', 'gw', '245'], 193 | ['Guyana', 'gy', '592'], 194 | ['Haiti', 'ht', '509'], 195 | ['Honduras', 'hn', '504'], 196 | ['Hong Kong (香港)', 'hk', '852'], 197 | ['Hungary (Magyarország)', 'hu', '36'], 198 | ['Iceland (Ísland)', 'is', '354'], 199 | ['India (भारत)', 'in', '91'], 200 | ['Indonesia', 'id', '62'], 201 | ['Iran (‫ایران‬‎)', 'ir', '98'], 202 | ['Iraq (‫العراق‬‎)', 'iq', '964'], 203 | ['Ireland', 'ie', '353'], 204 | ['Isle of Man', 'im', '44', 2], 205 | ['Israel (‫ישראל‬‎)', 'il', '972'], 206 | ['Italy (Italia)', 'it', '39', 0], 207 | ['Jamaica', 'jm', '1876'], 208 | ['Japan (日本)', 'jp', '81'], 209 | ['Jersey', 'je', '44', 3], 210 | ['Jordan (‫الأردن‬‎)', 'jo', '962'], 211 | ['Kazakhstan (Казахстан)', 'kz', '7', 1], 212 | ['Kenya', 'ke', '254'], 213 | ['Kiribati', 'ki', '686'], 214 | ['Kosovo', 'xk', '383'], 215 | ['Kuwait (‫الكويت‬‎)', 'kw', '965'], 216 | ['Kyrgyzstan (Кыргызстан)', 'kg', '996'], 217 | ['Laos (ລາວ)', 'la', '856'], 218 | ['Latvia (Latvija)', 'lv', '371'], 219 | ['Lebanon (‫لبنان‬‎)', 'lb', '961'], 220 | ['Lesotho', 'ls', '266'], 221 | ['Liberia', 'lr', '231'], 222 | ['Libya (‫ليبيا‬‎)', 'ly', '218'], 223 | ['Liechtenstein', 'li', '423'], 224 | ['Lithuania (Lietuva)', 'lt', '370'], 225 | ['Luxembourg', 'lu', '352'], 226 | ['Macau (澳門)', 'mo', '853'], 227 | ['Macedonia (FYROM) (Македонија)', 'mk', '389'], 228 | ['Madagascar (Madagasikara)', 'mg', '261'], 229 | ['Malawi', 'mw', '265'], 230 | ['Malaysia', 'my', '60'], 231 | ['Maldives', 'mv', '960'], 232 | ['Mali', 'ml', '223'], 233 | ['Malta', 'mt', '356'], 234 | ['Marshall Islands', 'mh', '692'], 235 | ['Martinique', 'mq', '596'], 236 | ['Mauritania (‫موريتانيا‬‎)', 'mr', '222'], 237 | ['Mauritius (Moris)', 'mu', '230'], 238 | ['Mayotte', 'yt', '262', 1], 239 | ['Mexico (México)', 'mx', '52'], 240 | ['Micronesia', 'fm', '691'], 241 | ['Moldova (Republica Moldova)', 'md', '373'], 242 | ['Monaco', 'mc', '377'], 243 | ['Mongolia (Монгол)', 'mn', '976'], 244 | ['Montenegro (Crna Gora)', 'me', '382'], 245 | ['Montserrat', 'ms', '1664'], 246 | ['Morocco (‫المغرب‬‎)', 'ma', '212', 0], 247 | ['Mozambique (Moçambique)', 'mz', '258'], 248 | ['Myanmar (Burma) (မြန်မာ)', 'mm', '95'], 249 | ['Namibia (Namibië)', 'na', '264'], 250 | ['Nauru', 'nr', '674'], 251 | ['Nepal (नेपाल)', 'np', '977'], 252 | ['Netherlands (Nederland)', 'nl', '31'], 253 | ['New Caledonia (Nouvelle-Calédonie)', 'nc', '687'], 254 | ['New Zealand', 'nz', '64'], 255 | ['Nicaragua', 'ni', '505'], 256 | ['Niger (Nijar)', 'ne', '227'], 257 | ['Nigeria', 'ng', '234'], 258 | ['Niue', 'nu', '683'], 259 | ['Norfolk Island', 'nf', '672'], 260 | ['North Korea (조선 민주주의 인민 공화국)', 'kp', '850'], 261 | ['Northern Mariana Islands', 'mp', '1670'], 262 | ['Norway (Norge)', 'no', '47', 0], 263 | ['Oman (‫عُمان‬‎)', 'om', '968'], 264 | ['Pakistan (‫پاکستان‬‎)', 'pk', '92'], 265 | ['Palau', 'pw', '680'], 266 | ['Palestine (‫فلسطين‬‎)', 'ps', '970'], 267 | ['Panama (Panamá)', 'pa', '507'], 268 | ['Papua New Guinea', 'pg', '675'], 269 | ['Paraguay', 'py', '595'], 270 | ['Peru (Perú)', 'pe', '51'], 271 | ['Philippines', 'ph', '63'], 272 | ['Poland (Polska)', 'pl', '48'], 273 | ['Portugal', 'pt', '351'], 274 | ['Puerto Rico', 'pr', '1', 3, ['787', '939']], 275 | ['Qatar (‫قطر‬‎)', 'qa', '974'], 276 | ['Réunion (La Réunion)', 're', '262', 0], 277 | ['Romania (România)', 'ro', '40'], 278 | ['Russia (Россия)', 'ru', '7', 0], 279 | ['Rwanda', 'rw', '250'], 280 | ['Saint Barthélemy (Saint-Barthélemy)', 'bl', '590', 1], 281 | ['Saint Helena', 'sh', '290'], 282 | ['Saint Kitts and Nevis', 'kn', '1869'], 283 | ['Saint Lucia', 'lc', '1758'], 284 | ['Saint Martin (Saint-Martin (partie française))', 'mf', '590', 2], 285 | ['Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)', 'pm', '508'], 286 | ['Saint Vincent and the Grenadines', 'vc', '1784'], 287 | ['Samoa', 'ws', '685'], 288 | ['San Marino', 'sm', '378'], 289 | ['São Tomé and Príncipe (São Tomé e Príncipe)', 'st', '239'], 290 | ['Saudi Arabia (‫المملكة العربية السعودية‬‎)', 'sa', '966'], 291 | ['Senegal (Sénégal)', 'sn', '221'], 292 | ['Serbia (Србија)', 'rs', '381'], 293 | ['Seychelles', 'sc', '248'], 294 | ['Sierra Leone', 'sl', '232'], 295 | ['Singapore', 'sg', '65'], 296 | ['Sint Maarten', 'sx', '1721'], 297 | ['Slovakia (Slovensko)', 'sk', '421'], 298 | ['Slovenia (Slovenija)', 'si', '386'], 299 | ['Solomon Islands', 'sb', '677'], 300 | ['Somalia (Soomaaliya)', 'so', '252'], 301 | ['South Africa', 'za', '27'], 302 | ['South Korea (대한민국)', 'kr', '82'], 303 | ['South Sudan (‫جنوب السودان‬‎)', 'ss', '211'], 304 | ['Spain (España)', 'es', '34'], 305 | ['Sri Lanka (ශ්‍රී ලංකාව)', 'lk', '94'], 306 | ['Sudan (‫السودان‬‎)', 'sd', '249'], 307 | ['Suriname', 'sr', '597'], 308 | ['Svalbard and Jan Mayen', 'sj', '47', 1], 309 | ['Swaziland', 'sz', '268'], 310 | ['Sweden (Sverige)', 'se', '46'], 311 | ['Switzerland (Schweiz)', 'ch', '41'], 312 | ['Syria (‫سوريا‬‎)', 'sy', '963'], 313 | ['Taiwan (台灣)', 'tw', '886'], 314 | ['Tajikistan', 'tj', '992'], 315 | ['Tanzania', 'tz', '255'], 316 | ['Thailand (ไทย)', 'th', '66'], 317 | ['Timor-Leste', 'tl', '670'], 318 | ['Togo', 'tg', '228'], 319 | ['Tokelau', 'tk', '690'], 320 | ['Tonga', 'to', '676'], 321 | ['Trinidad and Tobago', 'tt', '1868'], 322 | ['Tunisia (‫تونس‬‎)', 'tn', '216'], 323 | ['Turkey (Türkiye)', 'tr', '90'], 324 | ['Turkmenistan', 'tm', '993'], 325 | ['Turks and Caicos Islands', 'tc', '1649'], 326 | ['Tuvalu', 'tv', '688'], 327 | ['U.S. Virgin Islands', 'vi', '1340'], 328 | ['Uganda', 'ug', '256'], 329 | ['Ukraine (Україна)', 'ua', '380'], 330 | ['United Arab Emirates (‫الإمارات العربية المتحدة‬‎)', 'ae', '971'], 331 | ['United Kingdom', 'gb', '44', 0], 332 | ['United States', 'us', '1', 0], 333 | ['Uruguay', 'uy', '598'], 334 | ['Uzbekistan (Oʻzbekiston)', 'uz', '998'], 335 | ['Vanuatu', 'vu', '678'], 336 | ['Vatican City (Città del Vaticano)', 'va', '39', 1], 337 | ['Venezuela', 've', '58'], 338 | ['Vietnam (Việt Nam)', 'vn', '84'], 339 | ['Wallis and Futuna', 'wf', '681'], 340 | ['Western Sahara (‫الصحراء الغربية‬‎)', 'eh', '212', 1], 341 | ['Yemen (‫اليمن‬‎)', 'ye', '967'], 342 | ['Zambia', 'zm', '260'], 343 | ['Zimbabwe', 'zw', '263'], 344 | ['Åland Islands', 'ax', '358', 1], 345 | ] 346 | 347 | let countries 348 | 349 | function _formatCountriesData(countriesData) { 350 | return countriesData.map(country => ({ 351 | name: country[0], 352 | iso2: country[1], 353 | dialCode: country[2], 354 | priority: country[3] || 0, 355 | areaCodes: country[4] || null, 356 | })) 357 | } 358 | 359 | function initialize(externalCountriesList) { 360 | countries = _formatCountriesData( 361 | externalCountriesList || defaultCountriesData, 362 | ) 363 | } 364 | 365 | function getCountries() { 366 | if (!countries) { 367 | initialize() 368 | } 369 | 370 | return countries 371 | } 372 | 373 | const AllCountries = { 374 | initialize, 375 | getCountries, 376 | } 377 | 378 | export default AllCountries 379 | -------------------------------------------------------------------------------- /src/components/IntlTelInput.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { CountryData } from '../types' 4 | 5 | export interface IntlTelInputProps { 6 | /** 7 | * Container CSS class name. 8 | * @default 'intl-tel-input' 9 | */ 10 | containerClassName?: string 11 | /** 12 | * Text input CSS class name. 13 | * @default '' 14 | */ 15 | inputClassName?: string 16 | /** 17 | * It's used as `input` field `name` attribute. 18 | * @default '' 19 | */ 20 | fieldName?: string 21 | /** 22 | * It's used as `input` field `id` attribute. 23 | * @default '' 24 | */ 25 | fieldId?: string 26 | /** 27 | * The value of the input field. Useful for making input value controlled from outside the component. 28 | */ 29 | value?: string 30 | /** 31 | * The value used to initialize input. This will only work on uncontrolled component. 32 | * @default '' 33 | */ 34 | defaultValue?: string 35 | /** 36 | * Countries data can be configured, it defaults to data defined in `AllCountries`. 37 | * @default AllCountries.getCountries() 38 | */ 39 | countriesData?: CountryData[] | null 40 | /** 41 | * Whether or not to allow the dropdown. If disabled, there is no dropdown arrow, and the selected flag is not clickable. 42 | * Also we display the selected flag on the right instead because it is just a marker of state. 43 | * @default true 44 | */ 45 | allowDropdown?: boolean 46 | /** 47 | * If there is just a dial code in the input: remove it on blur, and re-add it on focus. 48 | * @default true 49 | */ 50 | autoHideDialCode?: boolean 51 | /** 52 | * Add or remove input placeholder with an example number for the selected country. 53 | * @default true 54 | */ 55 | autoPlaceholder?: boolean 56 | /** 57 | * Change the placeholder generated by autoPlaceholder. Must return a string. 58 | * @default null 59 | */ 60 | customPlaceholder?: 61 | | ((placeholder: string, selectedCountryData: CountryData) => string) 62 | | null 63 | /** 64 | * Don't display the countries you specify. (Array) 65 | * @default [] 66 | */ 67 | excludeCountries?: string[] 68 | /** 69 | * Format the input value during initialisation. 70 | * @default true 71 | */ 72 | formatOnInit?: boolean 73 | /** 74 | * Display the country dial code next to the selected flag so it's not part of the typed number. 75 | * Note that this will disable nationalMode because technically we are dealing with international numbers, 76 | * but with the dial code separated. 77 | * @default false 78 | */ 79 | separateDialCode?: boolean 80 | /** 81 | * Default country. 82 | * @default '' 83 | */ 84 | defaultCountry?: string 85 | /** 86 | * GeoIp lookup function. 87 | * @default null 88 | */ 89 | geoIpLookup?: (countryCode: string) => void 90 | /** 91 | * Don't insert international dial codes. 92 | * @default true 93 | */ 94 | nationalMode?: boolean 95 | /** 96 | * Number type to use for placeholders. 97 | * @default 'MOBILE' 98 | */ 99 | numberType?: string 100 | /** 101 | * The function which can catch the "no this default country" exception. 102 | * @default null 103 | */ 104 | noCountryDataHandler?: (countryCode: string) => void 105 | /** 106 | * Display only these countries. 107 | * @default [] 108 | */ 109 | onlyCountries?: string[] 110 | /** 111 | * The countries at the top of the list. defaults to United States and United Kingdom. 112 | * @default ['us', 'gb'] 113 | */ 114 | preferredCountries?: string[] 115 | /** 116 | * Optional validation callback function. It returns validation status, input box value and selected country data. 117 | * @default null 118 | */ 119 | onPhoneNumberChange?: ( 120 | isValid: boolean, 121 | value: string, 122 | selectedCountryData: CountryData, 123 | fullNumber: string, 124 | extension: string, 125 | ) => void 126 | /** 127 | * Optional validation callback function. It returns validation status, input box value and selected country data. 128 | * @default null 129 | */ 130 | onPhoneNumberBlur?: ( 131 | isValid: boolean, 132 | value: string, 133 | selectedCountryData: CountryData, 134 | fullNumber: string, 135 | extension: string, 136 | event: React.FocusEvent, 137 | ) => void 138 | /** 139 | * Optional validation callback function. It returns validation status, input box value and selected country data. 140 | * @default null 141 | */ 142 | onPhoneNumberFocus?: ( 143 | isValid: boolean, 144 | value: string, 145 | selectedCountryData: CountryData, 146 | fullNumber: string, 147 | extension: string, 148 | event: React.FocusEvent, 149 | ) => void 150 | /** 151 | * Allow main app to do things when a country is selected. 152 | * @default null 153 | */ 154 | onSelectFlag?: ( 155 | currentNumber: string, 156 | selectedCountryData: CountryData, 157 | fullNumber: string, 158 | isValid: boolean, 159 | ) => void 160 | /** 161 | * Disable this component. 162 | * @default false 163 | */ 164 | disabled?: boolean 165 | /** 166 | * Static placeholder for input controller. When defined it takes priority over autoPlaceholder. 167 | */ 168 | placeholder?: string 169 | /** 170 | * Enable auto focus 171 | * @default false 172 | */ 173 | autoFocus?: boolean 174 | /** 175 | * Set the value of the autoComplete attribute on the input. 176 | * For example, set it to phone to tell the browser where to auto complete phone numbers. 177 | * @default 'off' 178 | */ 179 | autoComplete?: string 180 | /** 181 | * Style object for the wrapper div. Useful for setting 100% width on the wrapper, etc. 182 | */ 183 | style?: React.CSSProperties 184 | /** 185 | * Render fullscreen flag dropdown when mobile useragent is detected. 186 | * The dropdown element is rendered as a direct child of document.body 187 | * @default true 188 | */ 189 | useMobileFullscreenDropdown?: boolean 190 | /** 191 | * Pass through arbitrary props to the tel input element. 192 | * @default {} 193 | */ 194 | telInputProps?: React.InputHTMLAttributes 195 | /** 196 | * Format the number. 197 | * @default true 198 | */ 199 | format?: boolean 200 | /** 201 | * Allow main app to do things when flag icon is clicked. 202 | * @default null 203 | */ 204 | onFlagClick?: (event: React.MouseEvent) => void 205 | } 206 | 207 | export interface IntlTelInputState { 208 | showDropdown: boolean 209 | highlightedCountry: number 210 | value: string 211 | disabled: boolean 212 | readonly: boolean 213 | offsetTop: number 214 | outerHeight: number 215 | placeholder: string 216 | title: string 217 | countryCode: string 218 | dialCode: string 219 | cursorPosition: any 220 | } 221 | 222 | export default class IntlTelInput extends React.Component< 223 | IntlTelInputProps, 224 | IntlTelInputState 225 | > { 226 | //#region Properties 227 | wrapperClass: { 228 | [key: string]: boolean 229 | } 230 | 231 | defaultCountry?: string 232 | 233 | autoCountry: string 234 | 235 | tempCountry: string 236 | 237 | startedLoadingAutoCountry: boolean 238 | 239 | dropdownContainer?: React.ElementType | '' 240 | 241 | isOpening: boolean 242 | 243 | isMobile: boolean 244 | 245 | preferredCountries: CountryData[] 246 | 247 | countries: CountryData[] 248 | 249 | countryCodes: { 250 | [key: string]: string[] 251 | } 252 | 253 | windowLoaded: boolean 254 | 255 | query: string 256 | 257 | selectedCountryData?: CountryData 258 | 259 | // prop copies 260 | autoHideDialCode: boolean 261 | 262 | nationalMode: boolean 263 | 264 | allowDropdown: boolean 265 | 266 | // refs 267 | /** 268 | * `
    ` HTML element of the `FlagDropDown` React component. 269 | */ 270 | flagDropDown: HTMLDivElement | null 271 | 272 | /** 273 | * `` HTML element of the `TelInput` React component. 274 | */ 275 | tel: HTMLInputElement | null 276 | 277 | // NOTE: 278 | // The underscore.deferred package doesn't have known type definitions. 279 | // The closest counterpart is jquery's Deferred object, which it claims to derive itself from. 280 | // These two are equivalent if you log it in console: 281 | // 282 | // underscore.deferred 283 | // var deferred = new _.Deferred() 284 | // 285 | // jquery 286 | // var deferred = $.Deferred() 287 | deferreds: JQuery.Deferred[] 288 | 289 | autoCountryDeferred: JQuery.Deferred 290 | 291 | utilsScriptDeferred: JQuery.Deferred 292 | //#endregion 293 | 294 | //#region Methods 295 | /** 296 | * Updates flag when value of defaultCountry props change 297 | */ 298 | updateFlagOnDefaultCountryChange(countryCode?: string): void 299 | 300 | getTempCountry(countryCode?: string): CountryData['iso2'] | 'auto' 301 | 302 | /** 303 | * set the input value and update the flag 304 | */ 305 | setNumber(number: string, preventFocus?: boolean): void 306 | 307 | setFlagDropdownRef(ref: HTMLDivElement | null): void 308 | 309 | setTelRef(ref: HTMLInputElement | null): void 310 | 311 | /** 312 | * select the given flag, update the placeholder and the active list item 313 | * 314 | * Note: called from setInitialState, updateFlagFromNumber, selectListItem, setCountry, updateFlagOnDefaultCountryChange 315 | */ 316 | setFlag(countryCode?: string, isInit?: boolean): void 317 | 318 | /** 319 | * get the extension from the current number 320 | */ 321 | getExtension(number?: string): string 322 | 323 | /** 324 | * format the number to the given format 325 | */ 326 | getNumber(number?: string, format?: string): string 327 | 328 | /** 329 | * get the input val, adding the dial code if separateDialCode is enabled 330 | */ 331 | getFullNumber(number?: string): string 332 | 333 | /** 334 | * try and extract a valid international dial code from a full telephone number 335 | */ 336 | getDialCode(number: string): string 337 | 338 | /** 339 | * check if the given number contains an unknown area code from 340 | */ 341 | isUnknownNanp(number?: string, dialCode?: string): boolean 342 | 343 | /** 344 | * add a country code to countryCodes 345 | */ 346 | addCountryCode( 347 | countryCodes: { 348 | [key: string]: string[] 349 | }, 350 | iso2: string, 351 | dialCode: string, 352 | priority?: number, 353 | ): { 354 | [key: string]: string[] 355 | } 356 | 357 | processAllCountries(): void 358 | 359 | /** 360 | * process the countryCodes map 361 | */ 362 | processCountryCodes(): void 363 | 364 | /** 365 | * process preferred countries - iterate through the preferences, 366 | * fetching the country data for each one 367 | */ 368 | processPreferredCountries(): void 369 | 370 | /** 371 | * set the initial state of the input value and the selected flag 372 | */ 373 | setInitialState(): void 374 | 375 | initRequests(): void 376 | 377 | loadCountryFromLocalStorage(): string 378 | 379 | loadAutoCountry(): void 380 | 381 | cap(number?: string): string | undefined 382 | 383 | removeEmptyDialCode(): void 384 | 385 | /** 386 | * highlight the next/prev item in the list (and ensure it is visible) 387 | */ 388 | handleUpDownKey(key?: number): void 389 | 390 | /** 391 | * select the currently highlighted item 392 | */ 393 | handleEnterKey(): void 394 | 395 | /** 396 | * find the first list item whose name starts with the query string 397 | */ 398 | searchForCountry(query: string): void 399 | 400 | formatNumber(number?: string): string 401 | 402 | /** 403 | * update the input's value to the given val (format first if possible) 404 | */ 405 | updateValFromNumber( 406 | number?: string, 407 | doFormat?: boolean, 408 | doNotify?: boolean, 409 | ): void 410 | 411 | /** 412 | * check if need to select a new flag based on the given number 413 | */ 414 | updateFlagFromNumber(number?: string, isInit?: boolean): void 415 | 416 | /** 417 | * filter the given countries using the process function 418 | */ 419 | filterCountries( 420 | countryArray: string[], 421 | processFunc: (iso2: string) => void, 422 | ): void 423 | 424 | /** 425 | * prepare all of the country data, including onlyCountries and preferredCountries options 426 | */ 427 | processCountryData(): void 428 | 429 | handleOnBlur(event: React.FocusEvent): void 430 | 431 | handleOnFocus(event: React.FocusEvent): void 432 | 433 | bindDocumentClick(): void 434 | 435 | unbindDocumentClick(): void 436 | 437 | clickSelectedFlag(event: React.MouseEvent): void 438 | 439 | /** 440 | * update the input placeholder to an 441 | * example number from the currently selected country 442 | */ 443 | updatePlaceholder(props?: IntlTelInputProps): void 444 | 445 | toggleDropdown(status?: boolean): void 446 | 447 | /** 448 | * check if an element is visible within it's container, else scroll until it is 449 | */ 450 | scrollTo(element: Element, middle?: boolean): void 451 | 452 | /** 453 | * replace any existing dial code with the new one 454 | * 455 | * Note: called from _setFlag 456 | */ 457 | updateDialCode(newDialCode?: string, hasSelectedListItem?: boolean): string 458 | 459 | generateMarkup(): void 460 | 461 | handleSelectedFlagKeydown(event: React.KeyboardEvent): void 462 | 463 | /** 464 | * validate the input val - assumes the global function isValidNumber (from libphonenumber) 465 | */ 466 | isValidNumber(number?: string): boolean 467 | 468 | formatFullNumber(number?: string): string 469 | 470 | notifyPhoneNumberChange(number?: string): void 471 | 472 | /** 473 | * remove the dial code if separateDialCode is enabled 474 | */ 475 | beforeSetNumber( 476 | number?: string, 477 | props?: IntlTelInputProps, 478 | ): string | undefined 479 | 480 | handleWindowScroll(): void 481 | 482 | handleDocumentKeyDown(event: KeyboardEvent): void 483 | 484 | handleDocumentClick(event: MouseEvent): void 485 | 486 | /** 487 | * Either notify phoneNumber changed if component is controlled 488 | */ 489 | handleInputChange(event: React.FocusEvent): void 490 | 491 | changeHighlightCountry(showDropdown: boolean, selectedIndex: number): void 492 | 493 | loadUtils(): void 494 | 495 | /** 496 | * this is called when the geoip call returns 497 | */ 498 | autoCountryLoaded(): void 499 | //#endregion 500 | } 501 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v7.1.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v7.1.0) 4 | 5 | ### New features 6 | 7 | - [#308](https://github.com/patw0929/react-intl-tel-input/pull/308): Add optional onPhoneNumberFocus function (by [@johannessjoberg](https://github.com/johannessjoberg)) 8 | 9 | ## [v7.0.3](https://github.com/patw0929/react-intl-tel-input/releases/tag/v7.0.2) 10 | 11 | ### Bug fixes 12 | 13 | - [#285](https://github.com/patw0929/react-intl-tel-input/pull/285): Update flag when defaultCountry value is changed (by [@dhanesh-kapadiya](https://github.com/dhanesh-kapadiya)) 14 | 15 | ## [v7.0.2](https://github.com/patw0929/react-intl-tel-input/releases/tag/v7.0.2) 16 | 17 | ### Bug fixes: 18 | 19 | - [#302](https://github.com/patw0929/react-intl-tel-input/pull/302): Remove package-lock.json (by [@patw0929](https://github.com/patw0929)) 20 | - [#283](https://github.com/patw0929/react-intl-tel-input/pull/283): Fix: invoke onPhoneNumberChange callback with formatted value on init instead of unformatted value from previous state (by [@coox](https://github.com/coox)) 21 | - [#300](https://github.com/patw0929/react-intl-tel-input/pull/300): fixed bug with pasting number over another number (by [@flagoon](https://github.com/flagoon)) 22 | - [#299](https://github.com/patw0929/react-intl-tel-input/pull/299): Use cross-env to solve cross platforms issue (scripts with node env variables) (by [@patw0929](https://github.com/patw0929)) 23 | 24 | ### Docs: 25 | 26 | - [#298](https://github.com/patw0929/react-intl-tel-input/pull/298): Update LICENSE (by [@Parikshit-Hooda](https://github.com/Parikshit-Hooda)) 27 | - [#297](https://github.com/patw0929/react-intl-tel-input/pull/297): Update README.md Update LICENSE (by [@Parikshit-Hooda](https://github.com/Parikshit-Hooda)) 28 | - [#294](https://github.com/patw0929/react-intl-tel-input/pull/294): Removed bash highlighting for npm/yarn commands in README (by [@bhumijgupta](https://github.com/bhumijgupta)) 29 | 30 | ## [v7.0.1](https://github.com/patw0929/react-intl-tel-input/releases/tag/v7.0.1) 31 | 32 | ### Bug fixes 33 | 34 | - [#277](https://github.com/patw0929/react-intl-tel-input/pull/277): Fix([#272](https://github.com/patw0929/react-intl-tel-input/pull/272)): Updating Allowdropdown after mount (by [@nutboltu](https://github.com/nutboltu)) 35 | 36 | ### Docs 37 | 38 | - [#273](https://github.com/patw0929/react-intl-tel-input/pull/273): Feature: Introducing storybook for documentation and playground (by [@nutboltu](https://github.com/nutboltu)) 39 | - [#274](https://github.com/patw0929/react-intl-tel-input/pull/274): fix(storybook-deploy): Fixed the storybook deployment script in travis (by [@nutboltu](https://github.com/nutboltu)) 40 | - [#275](https://github.com/patw0929/react-intl-tel-input/pull/275): Fix: storybook's public path (by [@patw0929](https://github.com/patw0929)) 41 | - [#276](https://github.com/patw0929/react-intl-tel-input/pull/276): Refactor: Removing all unused files and codes for example (by [@nutboltu](https://github.com/nutboltu)) 42 | 43 | ## [v7.0.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v7.0.0) 44 | 45 | ### Bug fixes 46 | 47 | - [#270](https://github.com/patw0929/react-intl-tel-input/pull/270): Fixed the issue of pasting number to text input cannot update flag in international mode (by [@patw0929](https://github.com/patw0929)) 48 | - [#271](https://github.com/patw0929/react-intl-tel-input/pull/271): Fixed the CSS prop name issue of styled-component (by [@patw0929](https://github.com/patw0929)) 49 | 50 | ## [v6.1.1](https://github.com/patw0929/react-intl-tel-input/releases/tag/v6.1.1) 51 | 52 | ### Bug fixes 53 | 54 | - [#269](https://github.com/patw0929/react-intl-tel-input/pull/269): Fixed issue [#268](https://github.com/patw0929/react-intl-tel-input/issues/268) - disabled state doesn't update an input field (by [@patw0929](https://github.com/patw0929)) 55 | - [#265](https://github.com/patw0929/react-intl-tel-input/pull/265): update cursor position after focused (by [@Loongwoo](https://github.com/Loongwoo)) 56 | 57 | ### Chores 58 | 59 | - [#267](https://github.com/patw0929/react-intl-tel-input/pull/267) - Use createPortal API to implement RootModal (by [@patw0929](https://github.com/patw0929)) 60 | - Added `ISSUE_TEMPLATE.md` & `PULL_REQUEST_TEMPLATE.md` 61 | 62 | ## [v6.1.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v6.1.0) 63 | 64 | ### New features 65 | 66 | - [#249](https://github.com/patw0929/react-intl-tel-input/pull/249): Add support for onFlagClick (by [@tomegz](https://github.com/tomegz)) 67 | - [#254](https://github.com/patw0929/react-intl-tel-input/pull/254): Updated libphonenumber to 8.10.2 (by [@superhit0](https://github.com/superhit0)) 68 | - [#256](https://github.com/patw0929/react-intl-tel-input/pull/256): Added event object to onPhoneNumberBlur callback's parameter (by [@superhit0](https://github.com/superhit0)) 69 | 70 | ### Bug fixes 71 | 72 | - [#254](https://github.com/patw0929/react-intl-tel-input/pull/254): Fixed issue [#253](https://github.com/patw0929/react-intl-tel-input/issues/253) - Can not import from Node.js since module build upgrade to webpack 4 (by [@superhit0](https://github.com/superhit0)) 73 | - [#256](https://github.com/patw0929/react-intl-tel-input/pull/256): Defined `.npmrc` to avoid overriding the default npm registry server (by [@superhit0](https://github.com/superhit0)) 74 | - [#259](https://github.com/patw0929/react-intl-tel-input/pull/259): Fixed not update value issue when value is empty string (by [@patw0929](https://github.com/patw0929)) 75 | 76 | ## [v6.0.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v6.0.0) 77 | 78 | ### Breaking changes 79 | 80 | - [#235](https://github.com/patw0929/react-intl-tel-input/pull/245): Remove utilsScript prop (by [@patw0929](https://github.com/patw0929)) 81 | - [#247](https://github.com/patw0929/react-intl-tel-input/pull/247): Removed libphonenumber.js (by [@patw0929](https://github.com/patw0929)) 82 | 83 | ### New features 84 | 85 | - [#248](https://github.com/patw0929/react-intl-tel-input/pull/248): Analyze bundle size & decrease the size of main.js (by [@patw0929](https://github.com/patw0929)) 86 | - [#227](https://github.com/patw0929/react-intl-tel-input/pull/227): Bumping React version to 16.4.1 & removing deprecated lifecycle events (by [@superhit0](https://github.com/superhit0)) 87 | - [#214](https://github.com/patw0929/react-intl-tel-input/pull/214): Provide fullNumber and isValid when onSelectFlag (by [@adrienharnay](https://github.com/adrienharnay)) 88 | - [#232](https://github.com/patw0929/react-intl-tel-input/pull/232): npmignore updated with file list (fixed [#231](https://github.com/patw0929/react-intl-tel-input/issues/231)) (by [@nutboltu](https://github.com/nutboltu)) 89 | - [#242](https://github.com/patw0929/react-intl-tel-input/pull/242): Upgrade webpack, eslint, babel and refine coding style (by [@patw0929](https://github.com/patw0929)) 90 | - [#243](https://github.com/patw0929/react-intl-tel-input/pull/243): Improvement: Utilize @babel/plugin-proposal-class-properties by using class properties in class components (by [@tomegz](https://github.com/tomegz)) 91 | 92 | ### Bug fixes 93 | 94 | - [#246](https://github.com/patw0929/react-intl-tel-input/pull/246): Refactor FlagDropDown: Avoid creating functions every time render() is invoked, use class properties instead (by [@tomegz](https://github.com/tomegz)) 95 | - [#221](https://github.com/patw0929/react-intl-tel-input/pull/221): Fix cursor Issue ([#205](https://github.com/patw0929/react-intl-tel-input/issues/205)) (by [@superhit0](https://github.com/superhit0)) 96 | - [#223](https://github.com/patw0929/react-intl-tel-input/pull/223): Removed second argument of parseFloat (by [@patw0929](https://github.com/patw0929)) 97 | - [#234](https://github.com/patw0929/react-intl-tel-input/pull/234): Hide country list when click on flag button (by [@ilagnev](https://github.com/ilagnev)) 98 | - [#241](https://github.com/patw0929/react-intl-tel-input/pull/241): Fixes [#235](https://github.com/patw0929/react-intl-tel-input/issues/235): Show countrylist when allowDropdown flag is set to true (by [@tomegz](https://github.com/tomegz)) 99 | 100 | ## [v5.1.0-rc.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.1.0-rc.0) 101 | 102 | ### New features 103 | 104 | - [#227](https://github.com/patw0929/react-intl-tel-input/pull/227): Bumping React version to 16.4.1 & removing deprecated lifecycle events (by [@superhit0](https://github.com/superhit0)) 105 | - [#214](https://github.com/patw0929/react-intl-tel-input/pull/214): Provide fullNumber and isValid when onSelectFlag (by [@adrienharnay](https://github.com/adrienharnay)) 106 | - [#232](https://github.com/patw0929/react-intl-tel-input/pull/232): npmignore updated with file list (fixed [#231](https://github.com/patw0929/react-intl-tel-input/issues/231)) (by [@nutboltu](https://github.com/nutboltu)) 107 | - [#242](https://github.com/patw0929/react-intl-tel-input/pull/242): Upgrade webpack, eslint, babel and refine coding style (by [@patw0929](https://github.com/patw0929)) 108 | - [#243](https://github.com/patw0929/react-intl-tel-input/pull/243): Improvement: Utilize @babel/plugin-proposal-class-properties by using class properties in class components (by [@tomegz](https://github.com/tomegz)) 109 | 110 | ### Bug fixes 111 | 112 | - [#221](https://github.com/patw0929/react-intl-tel-input/pull/221): Fix cursor Issue ([#205](https://github.com/patw0929/react-intl-tel-input/issues/205)) (by [@superhit0](https://github.com/superhit0)) 113 | - [#223](https://github.com/patw0929/react-intl-tel-input/pull/223): Removed second argument of parseFloat (by [@patw0929](https://github.com/patw0929)) 114 | - [#234](https://github.com/patw0929/react-intl-tel-input/pull/234): Hide country list when click on flag button (by [@ilagnev](https://github.com/ilagnev)) 115 | - [#241](https://github.com/patw0929/react-intl-tel-input/pull/241): Fixes [#235](https://github.com/patw0929/react-intl-tel-input/issues/235): Show countrylist when allowDropdown flag is set to true (by [@tomegz](https://github.com/tomegz)) 116 | 117 | ## [v5.0.7](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.7) 118 | 119 | ### Bug fixes 120 | 121 | - [#220](https://github.com/patw0929/react-intl-tel-input/pull/220): Upgrade Libphonenumber to v8.9.9 (by [@superhit0](https://github.com/superhit0)) 122 | 123 | 124 | ## [v5.0.6](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.6) 125 | 126 | ### Bug fixes 127 | 128 | - [#217](https://github.com/patw0929/react-intl-tel-input/pull/217): Add findIndex implementation for IE 11 (by [@ostap0207](https://github.com/ostap0207)) 129 | - [#219](https://github.com/patw0929/react-intl-tel-input/pull/219): Fixed [#218](https://github.com/patw0929/react-intl-tel-input/issues/218): Fix expanded class not being removed from wrapper (by [@MilosMosovsky](https://github.com/MilosMosovsky)) 130 | 131 | 132 | ## [v5.0.5](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.5) 133 | 134 | ### Bug fixes 135 | 136 | - Fixed [#208](https://github.com/patw0929/react-intl-tel-input/issues/208): issue of dial code shows twice in input ([#209](https://github.com/patw0929/react-intl-tel-input/pull/209) & [#210](https://github.com/patw0929/react-intl-tel-input/pull/210)) 137 | 138 | 139 | ## [v5.0.4](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.4) 140 | 141 | ### Bug fixes 142 | 143 | - [#207](https://github.com/patw0929/react-intl-tel-input/pull/207): Move Prop-types out of peer dependency. remove proptypes in dist ([57a6956](https://github.com/patw0929/react-intl-tel-input/commit/57a695617582a7662e1af4a66d326a9ff7d61ba7) by [@dphrag](https://github.com/dphrag)) 144 | 145 | 146 | ## [v5.0.3](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.3) 147 | 148 | ### Bug fixes 149 | 150 | - [#204](https://github.com/patw0929/react-intl-tel-input/pull/204): Handle placeholder and customPlaceholder change (by [@adrienharnay](https://github.com/adrienharnay)) 151 | 152 | 153 | ## [v5.0.2](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.2) 154 | 155 | ### Bug fixes 156 | 157 | - [#202](https://github.com/patw0929/react-intl-tel-input/pull/202): Fix runtime error when this.tel is null ([e021526](https://github.com/patw0929/react-intl-tel-input/commit/e02152686a39ae76dc801aa5a31df5f5b00e74ea) by[@adrienharnay](https://github.com/adrienharnay)) 158 | - [#201](https://github.com/patw0929/react-intl-tel-input/pull/201): Update placeholder when receiving new placeholder prop (4e9bcaf by @patw0929) 159 | 160 | ## [v5.0.1](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.1) 161 | 162 | ### Bug fixes 163 | 164 | - [#199](https://github.com/patw0929/react-intl-tel-input/pull/199): reconfigure packages to bring back compatibility to both react 15 & 16 ([ea2d593](https://github.com/patw0929/react-intl-tel-input/commit/ea2d593df075d59446d58f11df2d191afb813c6b) by [@ignatiusreza](https://github.com/ignatiusreza)) 165 | 166 | 167 | ## [v5.0.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v5.0.0) 168 | 169 | ### Breaking change 170 | 171 | - [#196](https://github.com/patw0929/react-intl-tel-input/pull/196) Upgrade to React 16 (by [@puffo](https://github.com/puffo) & [@ignatiusreza](https://github.com/ignatiusreza)) 172 | 173 | 174 | ## [v4.3.4](https://github.com/patw0929/react-intl-tel-input/releases/tag/v4.3.4) 175 | 176 | ### Bug fixes 177 | 178 | - [#198](https://github.com/patw0929/react-intl-tel-input/pull/198) Allow country code to be deleted (Fixed [#197](https://github.com/patw0929/react-intl-tel-input/issues/197)) ([c731a6b](https://github.com/patw0929/react-intl-tel-input/commit/c731a6b913b5d8852d886c4b0e35ae7cbc7c37b7) by [@MatthewAnstey](https://github.com/MatthewAnstey)) 179 | 180 | 181 | ## [v4.3.3](https://github.com/patw0929/react-intl-tel-input/releases/tag/v4.3.3) 182 | 183 | ### Bug fixes 184 | 185 | - [#195](https://github.com/patw0929/react-intl-tel-input/pull/195): Add flag update when phones changes through props ([9d58356](https://github.com/patw0929/react-intl-tel-input/commit/9d583560a80c0ff30ff5bf390d6ebcb31cea1130) by [@MatthewAnstey](https://github.com/MatthewAnstey)) 186 | 187 | 188 | ## [v4.3.2](https://github.com/patw0929/react-intl-tel-input/releases/tag/v4.3.2) 189 | 190 | ### Bug fixes 191 | 192 | - [#192](https://github.com/patw0929/react-intl-tel-input/pull/192): highlight country from preferred list ([b37cc3d](https://github.com/patw0929/react-intl-tel-input/commit/b37cc3d6c1f7d9f2b94dc912b4698b0143c5d4ee), [7f2b90e](https://github.com/patw0929/react-intl-tel-input/commit/7f2b90ecd74768e0a729327bd4af7e8ee4deeba3), [5bdbb79](https://github.com/patw0929/react-intl-tel-input/commit/5bdbb798bfec46c0df2237c2c52ceb72ef8b8ec0) by [@denis-k](https://github.com/denis-k)) 193 | 194 | 195 | ## [v4.3.1](https://github.com/patw0929/react-intl-tel-input/releases/tag/v4.3.1) 196 | 197 | ### Bug fixes 198 | 199 | - Changed line that sets countryCode. Now, when CC is invalid, it is set to null and therefor not changed ([34b5517](https://github.com/patw0929/react-intl-tel-input/commit/34b551772d3a21d823e42864f99d5f925ff9273a) by [@darkenvy](https://github.com/darkenvy)) 200 | 201 | 202 | ## [v4.0.1](https://github.com/patw0929/react-intl-tel-input/releases/tag/v4.0.1) 203 | 204 | ### Bug fixes 205 | 206 | - Make isMobile isomorphic-friendly ([690f25b](https://github.com/patw0929/react-intl-tel-input/commit/690f25b954fde8e810d029e70515229849722ff2) by [@mariusandra](https://github.com/mariusandra)) 207 | 208 | 209 | ## [v3.7.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v3.7.0) 210 | 211 | ### New features 212 | 213 | - [#162](https://github.com/patw0929/react-intl-tel-input/pull/162): Pass arbitrary props to the tel input element (Also fixed [#158](https://github.com/patw0929/react-intl-tel-input/issues/158)) ([5e2d4f9](https://github.com/patw0929/react-intl-tel-input/commit/5e2d4f999942b6cb33beb518ff317de76d6fafac) by [@Arkq](https://github.com/Arkq)) 214 | 215 | 216 | ## [v3.2.0](https://github.com/patw0929/react-intl-tel-input/releases/tag/v3.2.0) 217 | 218 | ### New features 219 | 220 | - [#140](https://github.com/patw0929/react-intl-tel-input/pull/140): Pass down status to onSelectFlag by using isValidNumberForRegion ([fd39e98](https://github.com/patw0929/react-intl-tel-input/commit/fd39e98607b833aec297a2dcfd23b7149a267677), [ed781ed](https://github.com/patw0929/react-intl-tel-input/commit/ed781edcc8bb686e43cb75998f2cf9a04e387349) by [@viqh](https://github.com/viqh)) 221 | - [#141](https://github.com/patw0929/react-intl-tel-input/pull/141): Added on blur callback handler ([5aaef6e](https://github.com/patw0929/react-intl-tel-input/commit/5aaef6edb0a0a3f27b28e9bb1fd4e31e7142d020) by [@matteoantoci](https://github.com/matteoantoci)) 222 | 223 | ### Bug fixes 224 | 225 | - [#142](https://github.com/patw0929/react-intl-tel-input/pull/142): implement state.value change in componentWillReceiveProps ([09eae7e](https://github.com/patw0929/react-intl-tel-input/commit/09eae7ec7132ab70fb34ffc1a2ff26becfe6424a) by [@pwlmaciejewski](https://github.com/pwlmaciejewski)) 226 | 227 | -------------------------------------------------------------------------------- /src/components/__tests__/FlagDropDown.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node, no-eval */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import ReactTestUtils from 'react-dom/test-utils' 5 | import { mount } from 'enzyme' 6 | import IntlTelInput from '../IntlTelInput' 7 | import FlagDropDown from '../FlagDropDown' 8 | import CountryList from '../CountryList' 9 | import TelInput from '../TelInput' 10 | 11 | // eslint-disable-next-line func-names 12 | describe('FlagDropDown', function() { 13 | beforeEach(() => { 14 | jest.resetModules() 15 | 16 | this.params = { 17 | containerClassName: 'intl-tel-input', 18 | inputClassName: 'form-control phoneNumber', 19 | fieldName: 'telephone', 20 | defaultCountry: 'tw', 21 | } 22 | 23 | this.makeSubject = () => { 24 | return mount() 25 | } 26 | }) 27 | 28 | it('should be rendered', () => { 29 | const subject = this.makeSubject() 30 | const flagComponent = subject.find(FlagDropDown) 31 | const countryListComponent = subject.find(CountryList) 32 | 33 | expect(flagComponent.length).toBeTruthy() 34 | expect(countryListComponent.length).toBeTruthy() 35 | }) 36 | 37 | it('should load country "jp" from localStorage', async () => { 38 | window.localStorage.setItem('itiAutoCountry', 'jp') 39 | this.params = { 40 | ...this.params, 41 | defaultCountry: 'auto', 42 | } 43 | const subject = await this.makeSubject() 44 | 45 | subject.instance().utilsScriptDeferred.then(() => { 46 | expect(subject.state().countryCode).toBe('jp') 47 | window.localStorage.clear() 48 | }) 49 | }) 50 | 51 | it('should fallback to US when localStorage is not available', async () => { 52 | const mockedLocalStorage = window.localStorage 53 | // This will cause calls to localStorage.getItem() to throw 54 | window.localStorage = {} 55 | 56 | this.params = { 57 | ...this.params, 58 | defaultCountry: 'auto', 59 | } 60 | const subject = await this.makeSubject() 61 | 62 | subject.instance().utilsScriptDeferred.then(() => { 63 | expect(subject.state().countryCode).toBe('us') 64 | window.localStorage.clear() 65 | }) 66 | 67 | window.localStorage = mockedLocalStorage 68 | }) 69 | 70 | it('should has .separate-dial-code class when with separateDialCode = true', () => { 71 | this.params = { 72 | ...this.params, 73 | separateDialCode: true, 74 | } 75 | const subject = this.makeSubject() 76 | 77 | expect(subject.find('.separate-dial-code').length).toBeTruthy() 78 | }) 79 | 80 | it('should has "tw" in class name', () => { 81 | const subject = this.makeSubject() 82 | const flagComponent = subject.find(FlagDropDown) 83 | 84 | expect(flagComponent.find('.iti-flag.tw').first().length).toBeTruthy() 85 | }) 86 | 87 | it('should not has .hide class after clicking flag component', () => { 88 | const subject = this.makeSubject() 89 | const flagComponent = subject.find(FlagDropDown) 90 | 91 | expect( 92 | subject.find(CountryList).find('.country-list.hide').length, 93 | ).toBeTruthy() 94 | flagComponent 95 | .find('.selected-flag') 96 | .last() 97 | .simulate('click') 98 | 99 | subject.update() 100 | expect( 101 | subject.find(CountryList).find('.country-list.hide').length, 102 | ).toBeFalsy() 103 | }) 104 | 105 | it('Simulate change to Japan flag in dropdown before & after', () => { 106 | const subject = this.makeSubject() 107 | const flagComponent = subject.find(FlagDropDown) 108 | 109 | expect(subject.state().showDropdown).toBeFalsy() 110 | expect(flagComponent.find('.iti-flag.tw').length).toBeTruthy() 111 | flagComponent.simulate('click') 112 | const japanOption = flagComponent.find('[data-country-code="jp"]') 113 | 114 | japanOption.simulate('click') 115 | expect(flagComponent.find('.iti-flag.jp').length).toBeTruthy() 116 | expect(subject.state().showDropdown).toBeFalsy() 117 | }) 118 | 119 | it('Set onlyCountries', () => { 120 | this.params.onlyCountries = ['tw', 'us', 'kr'] 121 | const subject = this.makeSubject() 122 | const flagComponent = subject.find(FlagDropDown) 123 | 124 | const result = [ 125 | { 126 | name: 'South Korea (대한민국)', 127 | iso2: 'kr', 128 | dialCode: '82', 129 | priority: 0, 130 | areaCodes: null, 131 | }, 132 | { 133 | name: 'Taiwan (台灣)', 134 | iso2: 'tw', 135 | dialCode: '886', 136 | priority: 0, 137 | areaCodes: null, 138 | }, 139 | { 140 | name: 'United States', 141 | iso2: 'us', 142 | dialCode: '1', 143 | priority: 0, 144 | areaCodes: null, 145 | }, 146 | ] 147 | 148 | expect(flagComponent.props().countries).toEqual(result) 149 | }) 150 | 151 | it('Set excludeCountries', () => { 152 | this.params.excludeCountries = ['us', 'kr'] 153 | const subject = this.makeSubject() 154 | const flagComponent = subject.find(FlagDropDown) 155 | 156 | expect(flagComponent.props().countries.length).toBe(241) 157 | }) 158 | 159 | it('Set defaultCountry as "auto"', async () => { 160 | const lookup = callback => { 161 | callback('jp') 162 | } 163 | 164 | this.params = { 165 | ...this.params, 166 | defaultCountry: 'auto', 167 | geoIpLookup: lookup, 168 | } 169 | const subject = await this.makeSubject() 170 | 171 | subject.instance().utilsScriptDeferred.then(() => { 172 | expect(subject.state().countryCode).toBe('jp') 173 | }) 174 | }) 175 | 176 | describe('with original ReactTestUtils', () => { 177 | it('Mouse over on country', () => { 178 | const renderedComponent = ReactTestUtils.renderIntoDocument( 179 | , 184 | ) 185 | 186 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 187 | renderedComponent, 188 | 'selected-flag', 189 | ) 190 | 191 | const dropDownComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 192 | renderedComponent, 193 | 'country-list', 194 | ) 195 | 196 | ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(flagComponent)) 197 | const options = ReactDOM.findDOMNode(dropDownComponent).querySelectorAll( 198 | '.country:not([class="preferred"])', 199 | ) 200 | const koreaOption = ReactDOM.findDOMNode(dropDownComponent).querySelector( 201 | '[data-country-code="kr"]', 202 | ) 203 | 204 | let index = -1 205 | 206 | for (let i = 0, max = options.length; i < max; ++i) { 207 | if (options[i] === koreaOption) { 208 | index = i 209 | } 210 | } 211 | 212 | ReactTestUtils.Simulate.mouseOver(koreaOption) 213 | expect(renderedComponent.state.highlightedCountry).toBe(index) 214 | }) 215 | 216 | it('Simulate change to flag in dropdown by up and down key', () => { 217 | const renderedComponent = ReactTestUtils.renderIntoDocument( 218 | , 223 | ) 224 | 225 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 226 | renderedComponent, 227 | 'selected-flag', 228 | ) 229 | 230 | expect( 231 | ReactDOM.findDOMNode(flagComponent).querySelector('.iti-flag') 232 | .className, 233 | ).toBe('iti-flag tw') 234 | 235 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 236 | key: 'Enter', 237 | keyCode: 13, 238 | which: 13, 239 | }) 240 | expect(renderedComponent.state.showDropdown).toBeTruthy() 241 | 242 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 243 | key: 'Tab', 244 | keyCode: 9, 245 | which: 9, 246 | }) 247 | expect(renderedComponent.state.showDropdown).toBeFalsy() 248 | 249 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 250 | key: 'Enter', 251 | keyCode: 13, 252 | which: 13, 253 | }) 254 | 255 | const pressUpEvent = new window.KeyboardEvent('keydown', { 256 | bubbles: true, 257 | cancelable: true, 258 | shiftKey: true, 259 | keyCode: 38, 260 | key: 'Up', 261 | which: 38, 262 | }) 263 | 264 | document.dispatchEvent(pressUpEvent) 265 | expect(renderedComponent.state.highlightedCountry).toBe(212) 266 | 267 | const pressEnterEvent = new window.KeyboardEvent('keydown', { 268 | bubbles: true, 269 | cancelable: true, 270 | shiftKey: true, 271 | keyCode: 13, 272 | key: 'Enter', 273 | which: 13, 274 | }) 275 | 276 | document.dispatchEvent(pressEnterEvent) 277 | expect(renderedComponent.state.showDropdown).toBeFalsy() 278 | expect( 279 | ReactDOM.findDOMNode(flagComponent).querySelector('.iti-flag') 280 | .className === 'iti-flag sy', 281 | ) 282 | }) 283 | 284 | it('Simulate close the dropdown menu by ESC key', () => { 285 | const renderedComponent = ReactTestUtils.renderIntoDocument( 286 | , 291 | ) 292 | 293 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 294 | renderedComponent, 295 | 'selected-flag', 296 | ) 297 | 298 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 299 | key: 'Enter', 300 | keyCode: 13, 301 | which: 13, 302 | }) 303 | expect(renderedComponent.state.showDropdown).toBeTruthy() 304 | 305 | const pressEscEvent = new window.KeyboardEvent('keydown', { 306 | bubbles: true, 307 | cancelable: true, 308 | shiftKey: true, 309 | keyCode: 27, 310 | key: 'Esc', 311 | which: 27, 312 | }) 313 | 314 | document.dispatchEvent(pressEscEvent) 315 | expect(renderedComponent.state.showDropdown).toBeFalsy() 316 | }) 317 | 318 | it('Simulate close the dropdown menu by clicking on document', () => { 319 | const renderedComponent = ReactTestUtils.renderIntoDocument( 320 | , 325 | ) 326 | 327 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 328 | renderedComponent, 329 | 'selected-flag', 330 | ) 331 | 332 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 333 | key: 'Enter', 334 | keyCode: 13, 335 | which: 13, 336 | }) 337 | expect(renderedComponent.state.showDropdown).toBeTruthy() 338 | 339 | const clickEvent = new window.MouseEvent('click', { 340 | view: window, 341 | bubbles: true, 342 | cancelable: true, 343 | }) 344 | 345 | document.querySelector('html').dispatchEvent(clickEvent) 346 | expect(renderedComponent.state.showDropdown).toBeFalsy() 347 | }) 348 | 349 | it('componentWillUnmount', () => { 350 | const renderedComponent = ReactTestUtils.renderIntoDocument( 351 | , 356 | ) 357 | 358 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 359 | renderedComponent, 360 | 'selected-flag', 361 | ) 362 | 363 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 364 | key: 'Enter', 365 | keyCode: 13, 366 | which: 13, 367 | }) 368 | expect(renderedComponent.state.showDropdown).toBeTruthy() 369 | 370 | renderedComponent.componentWillUnmount() 371 | 372 | const clickEvent = new window.MouseEvent('click', { 373 | view: window, 374 | bubbles: true, 375 | cancelable: true, 376 | }) 377 | 378 | document.querySelector('html').dispatchEvent(clickEvent) 379 | expect(renderedComponent.state.showDropdown).toBeTruthy() 380 | }) 381 | 382 | it('Simulate search country name in dropdown menu', () => { 383 | const renderedComponent = ReactTestUtils.renderIntoDocument( 384 | , 389 | ) 390 | 391 | const flagComponent = ReactTestUtils.findRenderedDOMComponentWithClass( 392 | renderedComponent, 393 | 'selected-flag', 394 | ) 395 | 396 | ReactTestUtils.Simulate.keyDown(ReactDOM.findDOMNode(flagComponent), { 397 | key: 'Enter', 398 | keyCode: 13, 399 | which: 13, 400 | }) 401 | expect(renderedComponent.state.showDropdown).toBe(true) 402 | 403 | const pressJEvent = new window.KeyboardEvent('keydown', { 404 | bubbles: true, 405 | cancelable: true, 406 | shiftKey: true, 407 | keyCode: 74, 408 | key: 'J', 409 | which: 74, 410 | }) 411 | const pressAEvent = new window.KeyboardEvent('keydown', { 412 | bubbles: true, 413 | cancelable: true, 414 | shiftKey: true, 415 | keyCode: 65, 416 | key: 'A', 417 | which: 65, 418 | }) 419 | const pressPEvent = new window.KeyboardEvent('keydown', { 420 | bubbles: true, 421 | cancelable: true, 422 | shiftKey: true, 423 | keyCode: 80, 424 | key: 'P', 425 | which: 80, 426 | }) 427 | 428 | document.dispatchEvent(pressJEvent) 429 | document.dispatchEvent(pressAEvent) 430 | document.dispatchEvent(pressPEvent) 431 | const pressEnterEvent = new window.KeyboardEvent('keydown', { 432 | bubbles: true, 433 | cancelable: true, 434 | shiftKey: true, 435 | keyCode: 13, 436 | key: 'Enter', 437 | which: 13, 438 | }) 439 | 440 | document.dispatchEvent(pressEnterEvent) 441 | 442 | expect(renderedComponent.state.showDropdown).toBeFalsy() 443 | expect(renderedComponent.state.highlightedCountry).toBe(108) 444 | expect(renderedComponent.state.countryCode).toBe('jp') 445 | }) 446 | }) 447 | 448 | it('customPlaceholder', () => { 449 | let expected = '' 450 | const customPlaceholder = (placeholder, countryData) => { 451 | expected = `${placeholder},${countryData.iso2}` 452 | } 453 | 454 | this.params.customPlaceholder = customPlaceholder 455 | const subject = this.makeSubject() 456 | const flagComponent = subject.find(FlagDropDown) 457 | const countryListComponent = subject.find(CountryList) 458 | 459 | expect(expected).toBe('0912 345 678,tw') 460 | flagComponent.simulate('click') 461 | const japanOption = countryListComponent.find('[data-country-code="jp"]') 462 | 463 | japanOption.simulate('click') 464 | expect(expected).toBe('090-1234-5678,jp') 465 | }) 466 | 467 | it('onSelectFlag', () => { 468 | let expected = '' 469 | const onSelectFlag = (currentNumber, countryData, fullNumber, isValid) => { 470 | expected = Object.assign( 471 | {}, 472 | { currentNumber, fullNumber, isValid, ...countryData }, 473 | ) 474 | } 475 | 476 | this.params.onSelectFlag = onSelectFlag 477 | const subject = this.makeSubject() 478 | const flagComponent = subject.find(FlagDropDown) 479 | const inputComponent = subject.find(TelInput) 480 | const countryListComponent = subject.find(CountryList) 481 | 482 | inputComponent.simulate('change', { target: { value: '+8109012345678' } }) 483 | flagComponent.simulate('click') 484 | const japanOption = countryListComponent.find('[data-country-code="jp"]') 485 | 486 | japanOption.simulate('click') 487 | 488 | expect(expected).toEqual({ 489 | currentNumber: '+8109012345678', 490 | fullNumber: '+81 90-1234-5678', 491 | isValid: true, 492 | name: 'Japan (日本)', 493 | iso2: 'jp', 494 | dialCode: '81', 495 | priority: 0, 496 | areaCodes: null, 497 | }) 498 | }) 499 | 500 | it('should output formatted number with formatNumber function', () => { 501 | this.params.format = true 502 | this.params.nationalMode = true 503 | const subject = this.makeSubject() 504 | 505 | expect(subject.instance().formatNumber('+886 912 345 678')).toBe( 506 | '0912 345 678', 507 | ) 508 | }) 509 | 510 | it('should highlight country from preferred list', async () => { 511 | const { defaultCountry } = this.params 512 | 513 | this.params = { 514 | ...this.params, 515 | preferredCountries: ['us', 'gb', defaultCountry], 516 | } 517 | const subject = await this.makeSubject() 518 | 519 | expect(defaultCountry).toBeTruthy() 520 | expect(subject.state().highlightedCountry).toBe(2) 521 | }) 522 | }) 523 | -------------------------------------------------------------------------------- /src/components/__tests__/TelInput.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-eval, no-restricted-properties */ 2 | import React from 'react' 3 | import { mount } from 'enzyme' 4 | import IntlTelInput from '../IntlTelInput' 5 | import TelInput from '../TelInput' 6 | import FlagDropDown from '../FlagDropDown' 7 | 8 | // eslint-disable-next-line func-names 9 | describe('TelInput', function() { 10 | beforeEach(() => { 11 | jest.resetModules() 12 | 13 | document.body.innerHTML = '
    ' 14 | 15 | this.params = { 16 | containerClassName: 'intl-tel-input', 17 | inputClassName: 'form-control phoneNumber', 18 | fieldName: 'telephone', 19 | fieldId: 'telephone-id', 20 | defaultCountry: 'tw', 21 | defaultValue: '0999 123 456', 22 | } 23 | this.makeSubject = () => { 24 | return mount(, { 25 | attachTo: document.querySelector('#root'), 26 | }) 27 | } 28 | }) 29 | 30 | it('should set fieldName as "telephone"', () => { 31 | const subject = this.makeSubject() 32 | const inputComponent = subject.find(TelInput) 33 | 34 | expect(inputComponent.props().fieldName).toBe('telephone') 35 | }) 36 | 37 | it('should set fieldId as "telephone-id"', () => { 38 | const subject = this.makeSubject() 39 | const inputComponent = subject.find(TelInput) 40 | 41 | expect(inputComponent.props().fieldId).toBe('telephone-id') 42 | }) 43 | 44 | it('onPhoneNumberChange without libphonenumber', () => { 45 | let expected = '' 46 | const onPhoneNumberChange = ( 47 | isValid, 48 | newNumber, 49 | countryData, 50 | fullNumber, 51 | ext, 52 | ) => { 53 | expected = `${isValid},${newNumber},${countryData.iso2},${fullNumber},${ext}` 54 | } 55 | 56 | window.intlTelInputUtils = undefined 57 | 58 | this.params.onPhoneNumberChange = onPhoneNumberChange 59 | const subject = this.makeSubject() 60 | const inputComponent = subject.find(TelInput) 61 | 62 | inputComponent.simulate('change', { target: { value: '+886911222333' } }) 63 | expect(expected).toBe('false,+886911222333,tw,+886911222333,') 64 | }) 65 | 66 | it('should set value as "0999 123 456"', async () => { 67 | const subject = await this.makeSubject() 68 | const inputComponent = subject.find(TelInput) 69 | 70 | expect(inputComponent.props().value).toBe('0999 123 456') 71 | }) 72 | 73 | it('should set className', () => { 74 | const subject = this.makeSubject() 75 | const inputComponent = subject.find(TelInput) 76 | 77 | expect(inputComponent.find('.form-control.phoneNumber').length).toBeTruthy() 78 | }) 79 | 80 | it('should not focused on render', () => { 81 | const initialSelectFlag = IntlTelInput.prototype.selectFlag 82 | 83 | let focused = false 84 | 85 | IntlTelInput.prototype.selectFlag = function selectFlag( 86 | countryCode, 87 | setFocus = true, 88 | ) { 89 | focused = focused || setFocus 90 | initialSelectFlag.call(this, countryCode, setFocus) 91 | } 92 | 93 | this.params = { 94 | ...this.params, 95 | value: '+886901234567', 96 | preferredCountries: ['kr', 'jp', 'tw'], 97 | } 98 | this.makeSubject() 99 | 100 | IntlTelInput.prototype.selectFlag = initialSelectFlag 101 | expect(focused).toBeFalsy() 102 | }) 103 | 104 | it('should has "kr" in preferred countries state', () => { 105 | this.params = { 106 | ...this.params, 107 | defaultCountry: 'zz', 108 | preferredCountries: ['kr', 'jp', 'tw'], 109 | } 110 | const subject = this.makeSubject() 111 | 112 | expect(subject.state().countryCode).toBe('kr') 113 | }) 114 | 115 | it('should set countryCode as "af" in state, when giving an invalid default country', () => { 116 | this.params = { 117 | ...this.params, 118 | preferredCountries: [], 119 | defaultValue: '', 120 | defaultCountry: 'zz', 121 | } 122 | const subject = this.makeSubject() 123 | 124 | expect(subject.state().countryCode).toBe('af') 125 | }) 126 | 127 | it('getNumber without libphonenumber', () => { 128 | window.intlTelInputUtils = undefined 129 | 130 | this.params = { 131 | ...this.params, 132 | } 133 | const subject = this.makeSubject() 134 | 135 | expect(subject.instance().getNumber(1)).toBe('') 136 | }) 137 | 138 | it('setNumber', () => { 139 | const subject = this.makeSubject() 140 | 141 | subject.instance().setNumber('+810258310015') 142 | expect(subject.state().countryCode).toBe('jp') 143 | }) 144 | 145 | it('handleKeyUp', () => { 146 | const subject = this.makeSubject() 147 | const inputComponent = subject.find(TelInput) 148 | 149 | inputComponent.simulate('focus') 150 | inputComponent.simulate('keyDown', { keyCode: 35 }) 151 | inputComponent.simulate('keyUp', { 152 | key: 'Backspace', 153 | keyCode: 8, 154 | which: 8, 155 | }) 156 | inputComponent.simulate('change', { 157 | target: { value: '0999 123 45' }, 158 | }) 159 | 160 | const changedInputComponent = subject.find(TelInput) 161 | 162 | expect(changedInputComponent.props().value).toBe('0999 123 45') 163 | }) 164 | 165 | it('ensurePlus', () => { 166 | this.params = { 167 | ...this.params, 168 | nationalMode: false, 169 | defaultValue: '+886999111222345', 170 | } 171 | const subject = this.makeSubject() 172 | const inputComponent = subject.find(TelInput) 173 | 174 | inputComponent.simulate('focus') 175 | inputComponent.simulate('keyDown', { keyCode: 35 }) 176 | const bspaceKey = { 177 | key: 'Backspace', 178 | keyCode: 8, 179 | which: 8, 180 | } 181 | 182 | inputComponent.simulate('keyUp', bspaceKey) 183 | inputComponent.simulate('keyUp', bspaceKey) 184 | inputComponent.simulate('keyUp', bspaceKey) 185 | inputComponent.simulate('change', { 186 | target: { value: '+886 999 111 222' }, 187 | }) 188 | expect(subject.state().value).toBe('+886 999 111 222') 189 | }) 190 | 191 | it('Disabled nationalMode and input phone number', () => { 192 | this.params.nationalMode = false 193 | const subject = this.makeSubject() 194 | const inputComponent = subject.find(TelInput) 195 | 196 | inputComponent.simulate('change', { target: { value: '+886901234567' } }) 197 | 198 | const changedInputComponent = subject.find(TelInput) 199 | 200 | expect(changedInputComponent.props().value).toBe('+886901234567') 201 | }) 202 | 203 | it('utils loaded', () => { 204 | this.makeSubject() 205 | 206 | expect(typeof window.intlTelInputUtils === 'object') 207 | expect(typeof window.intlTelInputUtils.isValidNumber === 'function') 208 | }) 209 | 210 | it('onPhoneNumberChange', () => { 211 | let expected = '' 212 | const onPhoneNumberChange = ( 213 | isValid, 214 | newNumber, 215 | countryData, 216 | fullNumber, 217 | ext, 218 | ) => { 219 | expected = `${isValid},${newNumber},${countryData.iso2},${fullNumber},${ext}` 220 | } 221 | 222 | this.params.onPhoneNumberChange = onPhoneNumberChange 223 | const subject = this.makeSubject() 224 | const inputComponent = subject.find(TelInput) 225 | 226 | inputComponent.simulate('change', { target: { value: '+886911222333' } }) 227 | expect(expected).toBe('true,+886911222333,tw,+886 911 222 333,null') 228 | }) 229 | 230 | it('Blur and cleaning the empty dialcode', () => { 231 | const subject = this.makeSubject() 232 | const inputComponent = subject.find(TelInput) 233 | 234 | inputComponent.simulate('change', { target: { value: '+886' } }) 235 | subject.instance().handleOnBlur() 236 | expect(subject.state().value).toBe('') 237 | }) 238 | 239 | const testOnPhoneNumberEvent = ({ property, eventType }) => 240 | it(`${property}`, () => { 241 | let expected = '' 242 | const onPhoneNumberEvent = ( 243 | isValid, 244 | newNumber, 245 | countryData, 246 | fullNumber, 247 | ext, 248 | event, 249 | ) => { 250 | const { type } = event 251 | 252 | expected = `${isValid},${newNumber},${countryData.iso2},${fullNumber},${ext},${type}` 253 | } 254 | 255 | this.params[property] = onPhoneNumberEvent 256 | const subject = this.makeSubject() 257 | const inputComponent = subject.find(TelInput) 258 | 259 | inputComponent.simulate('change', { target: { value: '+886911222333' } }) 260 | inputComponent.simulate(eventType) 261 | expect(expected).toBe( 262 | `true,+886911222333,tw,+886 911 222 333,null,${eventType}`, 263 | ) 264 | }) 265 | 266 | ;[ 267 | { property: 'onPhoneNumberBlur', eventType: 'blur' }, 268 | { property: 'onPhoneNumberFocus', eventType: 'focus' }, 269 | ].forEach(testOnPhoneNumberEvent) 270 | 271 | it('should has empty value with false nationalMode, false autoHideDialCode and false separateDialCode', () => { 272 | this.params = { 273 | ...this.params, 274 | defaultValue: '', 275 | nationalMode: false, 276 | autoHideDialCode: false, 277 | separateDialCode: false, 278 | } 279 | const subject = this.makeSubject() 280 | 281 | expect(subject.state().value).toBe('+886') 282 | }) 283 | 284 | it('updateFlagFromNumber', () => { 285 | this.params = { 286 | defaultCountry: 'us', 287 | nationalMode: true, 288 | } 289 | const subject = this.makeSubject() 290 | const inputComponent = subject.find(TelInput) 291 | 292 | inputComponent.simulate('change', { target: { value: '9183319436' } }) 293 | expect(subject.state().countryCode).toBe('us') 294 | 295 | inputComponent.simulate('change', { target: { value: '+' } }) 296 | expect(subject.state().countryCode).toBe('us') 297 | }) 298 | 299 | it('isValidNumber', () => { 300 | const subject = this.makeSubject() 301 | 302 | expect(subject.instance().isValidNumber('0910123456')).toBeTruthy() 303 | expect(subject.instance().isValidNumber('091012345')).toBeFalsy() 304 | }) 305 | 306 | it('getFullNumber', () => { 307 | this.params = { 308 | ...this.params, 309 | separateDialCode: true, 310 | } 311 | const subject = this.makeSubject() 312 | const inputComponent = subject.find(TelInput) 313 | 314 | inputComponent.simulate('change', { target: { value: '910123456' } }) 315 | expect(subject.instance().getFullNumber(910123456)).toBe('+886910123456') 316 | }) 317 | 318 | it('should render custom placeholder', () => { 319 | this.params.placeholder = 'foo' 320 | const subject = this.makeSubject() 321 | const inputComponent = subject.find(TelInput) 322 | 323 | expect(inputComponent.props().placeholder).toBe('foo') 324 | }) 325 | 326 | // FIXME: Enzyme not support :focus in current time 327 | xit('should focus input when autoFocus set to true', () => { 328 | this.params.autoFocus = true 329 | const subject = this.makeSubject() 330 | const inputComponent = subject.find(TelInput) 331 | 332 | expect(inputComponent.is(':focus')).toBeTruthy() 333 | }) 334 | 335 | it('should not focus input when autoFocus set to false', () => { 336 | this.params.autoFocus = false 337 | const subject = this.makeSubject() 338 | const inputComponent = subject.find(TelInput) 339 | 340 | expect(document.activeElement).not.toBe(inputComponent) 341 | }) 342 | 343 | describe('when mobile useragent', () => { 344 | let defaultUserAgent 345 | 346 | beforeEach(() => { 347 | defaultUserAgent = navigator.userAgent 348 | window.navigator.__defineGetter__('userAgent', () => 'iPhone') 349 | }) 350 | 351 | afterEach(() => { 352 | window.navigator.__defineGetter__('userAgent', () => defaultUserAgent) 353 | }) 354 | 355 | it('sets FlagDropDown "dropdowncontainer" prop to "body"', () => { 356 | const subject = this.makeSubject() 357 | const flagDropdownComponent = subject.find(FlagDropDown) 358 | 359 | expect(flagDropdownComponent.props().dropdownContainer).toBe('body') 360 | }) 361 | 362 | it('sets FlagDropDown "isMobile" prop to true', () => { 363 | const subject = this.makeSubject() 364 | const flagDropdownComponent = subject.find(FlagDropDown) 365 | 366 | expect(flagDropdownComponent.props().isMobile).toBeTruthy() 367 | }) 368 | 369 | it('sets "iti-mobile" class to "body"', () => { 370 | expect(document.body.className).toBe('iti-mobile') 371 | }) 372 | 373 | it(`does not set FlagDropDown "dropdowncontainer" to "body" 374 | when "useMobileFullscreenDropdown" set to false`, () => { 375 | this.params.useMobileFullscreenDropdown = false 376 | const subject = this.makeSubject() 377 | const flagDropdownComponent = subject.find(FlagDropDown) 378 | 379 | expect(flagDropdownComponent.props().dropdownContainer).toBe('') 380 | }) 381 | }) 382 | 383 | describe('controlled', () => { 384 | it('should set the value', () => { 385 | const subject = this.makeSubject() 386 | 387 | expect(subject.state().value).toBe('0999 123 456') 388 | }) 389 | 390 | it('should not change input value if value is constrained by parent', () => { 391 | this.params.value = '0999 123 456' 392 | const subject = this.makeSubject() 393 | const inputComponent = subject.find(TelInput) 394 | 395 | inputComponent.simulate('change', { target: { value: '12345' } }) 396 | expect(subject.state().value).toBe('0999 123 456') 397 | }) 398 | 399 | it('should change input value on value prop change', () => { 400 | const subject = this.makeSubject() 401 | 402 | subject.setProps({ value: '+447598455159' }) 403 | subject.update() 404 | 405 | expect(subject.find(FlagDropDown).props().highlightedCountry).toBe(1) 406 | 407 | subject.setProps({ value: '+1(201) 555-0129' }) 408 | subject.update() 409 | 410 | expect(subject.find(FlagDropDown).props().highlightedCountry).toBe(0) 411 | }) 412 | 413 | it('should update country flag when value updates', () => { 414 | const subject = this.makeSubject() 415 | 416 | subject.setProps({ value: 'foo bar' }) 417 | subject.update() 418 | 419 | expect(subject.find(TelInput).props().value).toBe('foo bar') 420 | }) 421 | 422 | it('should be able to delete country code after input field has been populated with number', () => { 423 | const subject = this.makeSubject() 424 | 425 | subject.setProps({ value: '+447598455159' }) 426 | 427 | subject.setProps({ value: '+' }) 428 | 429 | expect(subject.state().value).toBe('+') 430 | }) 431 | 432 | it('should change input placeholder on placeholder prop change', () => { 433 | const subject = this.makeSubject() 434 | 435 | subject.setProps({ placeholder: 'Phone number' }) 436 | subject.update() 437 | 438 | expect(subject.find(TelInput).props().placeholder).toBe('Phone number') 439 | 440 | subject.setProps({ placeholder: 'Your phone' }) 441 | subject.update() 442 | 443 | expect(subject.find(TelInput).props().placeholder).toBe('Your phone') 444 | }) 445 | 446 | it('should change input placeholder on customPlaceholder prop change', () => { 447 | const subject = this.makeSubject() 448 | 449 | subject.setProps({ customPlaceholder: () => 'Phone number' }) 450 | subject.update() 451 | 452 | expect(subject.find(TelInput).props().placeholder).toBe('Phone number') 453 | 454 | subject.setProps({ customPlaceholder: () => 'Your phone' }) 455 | subject.update() 456 | 457 | expect(subject.find(TelInput).props().placeholder).toBe('Your phone') 458 | }) 459 | 460 | it('should set "expanded" class to wrapper only when flags are open', () => { 461 | const subject = this.makeSubject() 462 | const flagComponent = subject 463 | .find(FlagDropDown) 464 | .find('.selected-flag') 465 | .last() 466 | 467 | flagComponent.simulate('click') 468 | expect(subject.instance().wrapperClass.expanded).toBe(true) 469 | 470 | const taiwanOption = subject 471 | .find(FlagDropDown) 472 | .find('[data-country-code="tw"]') 473 | 474 | taiwanOption.simulate('click') 475 | expect(subject.instance().wrapperClass.expanded).toBe(false) 476 | }) 477 | }) 478 | 479 | describe('uncontrolled', () => { 480 | it('should initialize state with defaultValue', () => { 481 | this.params.defaultValue = '54321' 482 | const subject = this.makeSubject() 483 | const inputComponent = subject.find(TelInput) 484 | 485 | expect(inputComponent.props().value).toBe('54321') 486 | expect(subject.state().value).toBe('54321') 487 | }) 488 | 489 | it('should change value', () => { 490 | this.params.defaultValue = '' 491 | const subject = this.makeSubject() 492 | const inputComponent = subject.find(TelInput) 493 | 494 | inputComponent.simulate('change', { target: { value: '12345' } }) 495 | 496 | const changedInputComponent = subject.find(TelInput) 497 | 498 | expect(changedInputComponent.props().value).toBe('12345') 499 | expect(subject.state().value).toBe('12345') 500 | }) 501 | 502 | it('should change props value', () => { 503 | const subject = this.makeSubject() 504 | 505 | subject.setState({ 506 | value: '+886912345678', 507 | }) 508 | 509 | const changedInputComponent = subject.find(TelInput) 510 | 511 | expect(changedInputComponent.props().value).toBe('+886912345678') 512 | }) 513 | }) 514 | }) 515 | -------------------------------------------------------------------------------- /src/sprite.scss: -------------------------------------------------------------------------------- 1 | @function retina-size($value) { 2 | @return floor($value / 2); 3 | } 4 | 5 | @mixin retina-bg-size($spriteWidth, $spriteHeight) { 6 | background-size: floor($spriteWidth / 2) floor($spriteHeight / 2); 7 | } 8 | 9 | .iti-flag { 10 | $item-width-maps: (ac: 20px, ad: 20px, ae: 20px, af: 20px, ag: 20px, ai: 20px, al: 20px, am: 20px, ao: 20px, aq: 20px, ar: 20px, as: 20px, at: 20px, au: 20px, aw: 20px, ax: 20px, az: 20px, ba: 20px, bb: 20px, bd: 20px, be: 18px, bf: 20px, bg: 20px, bh: 20px, bi: 20px, bj: 20px, bl: 20px, bm: 20px, bn: 20px, bo: 20px, bq: 20px, br: 20px, bs: 20px, bt: 20px, bv: 20px, bw: 20px, by: 20px, bz: 20px, ca: 20px, cc: 20px, cd: 20px, cf: 20px, cg: 20px, ch: 15px, ci: 20px, ck: 20px, cl: 20px, cm: 20px, cn: 20px, co: 20px, cp: 20px, cr: 20px, cu: 20px, cv: 20px, cw: 20px, cx: 20px, cy: 20px, cz: 20px, de: 20px, dg: 20px, dj: 20px, dk: 20px, dm: 20px, do: 20px, dz: 20px, ea: 20px, ec: 20px, ee: 20px, eg: 20px, eh: 20px, er: 20px, es: 20px, et: 20px, eu: 20px, fi: 20px, fj: 20px, fk: 20px, fm: 20px, fo: 20px, fr: 20px, ga: 20px, gb: 20px, gd: 20px, ge: 20px, gf: 20px, gg: 20px, gh: 20px, gi: 20px, gl: 20px, gm: 20px, gn: 20px, gp: 20px, gq: 20px, gr: 20px, gs: 20px, gt: 20px, gu: 20px, gw: 20px, gy: 20px, hk: 20px, hm: 20px, hn: 20px, hr: 20px, ht: 20px, hu: 20px, ic: 20px, id: 20px, ie: 20px, il: 20px, im: 20px, in: 20px, io: 20px, iq: 20px, ir: 20px, is: 20px, it: 20px, je: 20px, jm: 20px, jo: 20px, jp: 20px, ke: 20px, kg: 20px, kh: 20px, ki: 20px, km: 20px, kn: 20px, kp: 20px, kr: 20px, kw: 20px, ky: 20px, kz: 20px, la: 20px, lb: 20px, lc: 20px, li: 20px, lk: 20px, lr: 20px, ls: 20px, lt: 20px, lu: 20px, lv: 20px, ly: 20px, ma: 20px, mc: 19px, md: 20px, me: 20px, mf: 20px, mg: 20px, mh: 20px, mk: 20px, ml: 20px, mm: 20px, mn: 20px, mo: 20px, mp: 20px, mq: 20px, mr: 20px, ms: 20px, mt: 20px, mu: 20px, mv: 20px, mw: 20px, mx: 20px, my: 20px, mz: 20px, na: 20px, nc: 20px, ne: 18px, nf: 20px, ng: 20px, ni: 20px, nl: 20px, no: 20px, np: 13px, nr: 20px, nu: 20px, nz: 20px, om: 20px, pa: 20px, pe: 20px, pf: 20px, pg: 20px, ph: 20px, pk: 20px, pl: 20px, pm: 20px, pn: 20px, pr: 20px, ps: 20px, pt: 20px, pw: 20px, py: 20px, qa: 20px, re: 20px, ro: 20px, rs: 20px, ru: 20px, rw: 20px, sa: 20px, sb: 20px, sc: 20px, sd: 20px, se: 20px, sg: 20px, sh: 20px, si: 20px, sj: 20px, sk: 20px, sl: 20px, sm: 20px, sn: 20px, so: 20px, sr: 20px, ss: 20px, st: 20px, sv: 20px, sx: 20px, sy: 20px, sz: 20px, ta: 20px, tc: 20px, td: 20px, tf: 20px, tg: 20px, th: 20px, tj: 20px, tk: 20px, tl: 20px, tm: 20px, tn: 20px, to: 20px, tr: 20px, tt: 20px, tv: 20px, tw: 20px, tz: 20px, ua: 20px, ug: 20px, um: 20px, us: 20px, uy: 20px, uz: 20px, va: 15px, vc: 20px, ve: 20px, vg: 20px, vi: 20px, vn: 20px, vu: 20px, wf: 20px, ws: 20px, xk: 20px, ye: 20px, yt: 20px, za: 20px, zm: 20px, zw: 20px, ); 11 | $standard-country: 'ac'; 12 | width: map-get($item-width-maps, $standard-country); 13 | 14 | @each $key, $width in $item-width-maps { 15 | @if $width != map-get($item-width-maps, $standard-country) { 16 | &.#{$key} { 17 | width: $width; 18 | } 19 | } 20 | } 21 | 22 | @media 23 | only screen and (-webkit-min-device-pixel-ratio: 2), 24 | only screen and ( min--moz-device-pixel-ratio: 2), 25 | only screen and ( -o-min-device-pixel-ratio: 2/1), 26 | only screen and ( min-device-pixel-ratio: 2), 27 | only screen and ( min-resolution: 192dpi), 28 | only screen and ( min-resolution: 2dppx) { 29 | background-size: 5630px 15px; 30 | } 31 | 32 | &.ac { 33 | height: 10px; 34 | background-position: 0px 0px; 35 | } 36 | &.ad { 37 | height: 14px; 38 | background-position: -22px 0px; 39 | } 40 | &.ae { 41 | height: 10px; 42 | background-position: -44px 0px; 43 | } 44 | &.af { 45 | height: 14px; 46 | background-position: -66px 0px; 47 | } 48 | &.ag { 49 | height: 14px; 50 | background-position: -88px 0px; 51 | } 52 | &.ai { 53 | height: 10px; 54 | background-position: -110px 0px; 55 | } 56 | &.al { 57 | height: 15px; 58 | background-position: -132px 0px; 59 | } 60 | &.am { 61 | height: 10px; 62 | background-position: -154px 0px; 63 | } 64 | &.ao { 65 | height: 14px; 66 | background-position: -176px 0px; 67 | } 68 | &.aq { 69 | height: 14px; 70 | background-position: -198px 0px; 71 | } 72 | &.ar { 73 | height: 13px; 74 | background-position: -220px 0px; 75 | } 76 | &.as { 77 | height: 10px; 78 | background-position: -242px 0px; 79 | } 80 | &.at { 81 | height: 14px; 82 | background-position: -264px 0px; 83 | } 84 | &.au { 85 | height: 10px; 86 | background-position: -286px 0px; 87 | } 88 | &.aw { 89 | height: 14px; 90 | background-position: -308px 0px; 91 | } 92 | &.ax { 93 | height: 13px; 94 | background-position: -330px 0px; 95 | } 96 | &.az { 97 | height: 10px; 98 | background-position: -352px 0px; 99 | } 100 | &.ba { 101 | height: 10px; 102 | background-position: -374px 0px; 103 | } 104 | &.bb { 105 | height: 14px; 106 | background-position: -396px 0px; 107 | } 108 | &.bd { 109 | height: 12px; 110 | background-position: -418px 0px; 111 | } 112 | &.be { 113 | height: 15px; 114 | background-position: -440px 0px; 115 | } 116 | &.bf { 117 | height: 14px; 118 | background-position: -460px 0px; 119 | } 120 | &.bg { 121 | height: 12px; 122 | background-position: -482px 0px; 123 | } 124 | &.bh { 125 | height: 12px; 126 | background-position: -504px 0px; 127 | } 128 | &.bi { 129 | height: 12px; 130 | background-position: -526px 0px; 131 | } 132 | &.bj { 133 | height: 14px; 134 | background-position: -548px 0px; 135 | } 136 | &.bl { 137 | height: 14px; 138 | background-position: -570px 0px; 139 | } 140 | &.bm { 141 | height: 10px; 142 | background-position: -592px 0px; 143 | } 144 | &.bn { 145 | height: 10px; 146 | background-position: -614px 0px; 147 | } 148 | &.bo { 149 | height: 14px; 150 | background-position: -636px 0px; 151 | } 152 | &.bq { 153 | height: 14px; 154 | background-position: -658px 0px; 155 | } 156 | &.br { 157 | height: 14px; 158 | background-position: -680px 0px; 159 | } 160 | &.bs { 161 | height: 10px; 162 | background-position: -702px 0px; 163 | } 164 | &.bt { 165 | height: 14px; 166 | background-position: -724px 0px; 167 | } 168 | &.bv { 169 | height: 15px; 170 | background-position: -746px 0px; 171 | } 172 | &.bw { 173 | height: 14px; 174 | background-position: -768px 0px; 175 | } 176 | &.by { 177 | height: 10px; 178 | background-position: -790px 0px; 179 | } 180 | &.bz { 181 | height: 14px; 182 | background-position: -812px 0px; 183 | } 184 | &.ca { 185 | height: 10px; 186 | background-position: -834px 0px; 187 | } 188 | &.cc { 189 | height: 10px; 190 | background-position: -856px 0px; 191 | } 192 | &.cd { 193 | height: 15px; 194 | background-position: -878px 0px; 195 | } 196 | &.cf { 197 | height: 14px; 198 | background-position: -900px 0px; 199 | } 200 | &.cg { 201 | height: 14px; 202 | background-position: -922px 0px; 203 | } 204 | &.ch { 205 | height: 15px; 206 | background-position: -944px 0px; 207 | } 208 | &.ci { 209 | height: 14px; 210 | background-position: -961px 0px; 211 | } 212 | &.ck { 213 | height: 10px; 214 | background-position: -983px 0px; 215 | } 216 | &.cl { 217 | height: 14px; 218 | background-position: -1005px 0px; 219 | } 220 | &.cm { 221 | height: 14px; 222 | background-position: -1027px 0px; 223 | } 224 | &.cn { 225 | height: 14px; 226 | background-position: -1049px 0px; 227 | } 228 | &.co { 229 | height: 14px; 230 | background-position: -1071px 0px; 231 | } 232 | &.cp { 233 | height: 14px; 234 | background-position: -1093px 0px; 235 | } 236 | &.cr { 237 | height: 12px; 238 | background-position: -1115px 0px; 239 | } 240 | &.cu { 241 | height: 10px; 242 | background-position: -1137px 0px; 243 | } 244 | &.cv { 245 | height: 12px; 246 | background-position: -1159px 0px; 247 | } 248 | &.cw { 249 | height: 14px; 250 | background-position: -1181px 0px; 251 | } 252 | &.cx { 253 | height: 10px; 254 | background-position: -1203px 0px; 255 | } 256 | &.cy { 257 | height: 14px; 258 | background-position: -1225px 0px; 259 | } 260 | &.cz { 261 | height: 14px; 262 | background-position: -1247px 0px; 263 | } 264 | &.de { 265 | height: 12px; 266 | background-position: -1269px 0px; 267 | } 268 | &.dg { 269 | height: 10px; 270 | background-position: -1291px 0px; 271 | } 272 | &.dj { 273 | height: 14px; 274 | background-position: -1313px 0px; 275 | } 276 | &.dk { 277 | height: 15px; 278 | background-position: -1335px 0px; 279 | } 280 | &.dm { 281 | height: 10px; 282 | background-position: -1357px 0px; 283 | } 284 | &.do { 285 | height: 13px; 286 | background-position: -1379px 0px; 287 | } 288 | &.dz { 289 | height: 14px; 290 | background-position: -1401px 0px; 291 | } 292 | &.ea { 293 | height: 14px; 294 | background-position: -1423px 0px; 295 | } 296 | &.ec { 297 | height: 14px; 298 | background-position: -1445px 0px; 299 | } 300 | &.ee { 301 | height: 13px; 302 | background-position: -1467px 0px; 303 | } 304 | &.eg { 305 | height: 14px; 306 | background-position: -1489px 0px; 307 | } 308 | &.eh { 309 | height: 10px; 310 | background-position: -1511px 0px; 311 | } 312 | &.er { 313 | height: 10px; 314 | background-position: -1533px 0px; 315 | } 316 | &.es { 317 | height: 14px; 318 | background-position: -1555px 0px; 319 | } 320 | &.et { 321 | height: 10px; 322 | background-position: -1577px 0px; 323 | } 324 | &.eu { 325 | height: 14px; 326 | background-position: -1599px 0px; 327 | } 328 | &.fi { 329 | height: 12px; 330 | background-position: -1621px 0px; 331 | } 332 | &.fj { 333 | height: 10px; 334 | background-position: -1643px 0px; 335 | } 336 | &.fk { 337 | height: 10px; 338 | background-position: -1665px 0px; 339 | } 340 | &.fm { 341 | height: 11px; 342 | background-position: -1687px 0px; 343 | } 344 | &.fo { 345 | height: 15px; 346 | background-position: -1709px 0px; 347 | } 348 | &.fr { 349 | height: 14px; 350 | background-position: -1731px 0px; 351 | } 352 | &.ga { 353 | height: 15px; 354 | background-position: -1753px 0px; 355 | } 356 | &.gb { 357 | height: 10px; 358 | background-position: -1775px 0px; 359 | } 360 | &.gd { 361 | height: 12px; 362 | background-position: -1797px 0px; 363 | } 364 | &.ge { 365 | height: 14px; 366 | background-position: -1819px 0px; 367 | } 368 | &.gf { 369 | height: 14px; 370 | background-position: -1841px 0px; 371 | } 372 | &.gg { 373 | height: 14px; 374 | background-position: -1863px 0px; 375 | } 376 | &.gh { 377 | height: 14px; 378 | background-position: -1885px 0px; 379 | } 380 | &.gi { 381 | height: 10px; 382 | background-position: -1907px 0px; 383 | } 384 | &.gl { 385 | height: 14px; 386 | background-position: -1929px 0px; 387 | } 388 | &.gm { 389 | height: 14px; 390 | background-position: -1951px 0px; 391 | } 392 | &.gn { 393 | height: 14px; 394 | background-position: -1973px 0px; 395 | } 396 | &.gp { 397 | height: 14px; 398 | background-position: -1995px 0px; 399 | } 400 | &.gq { 401 | height: 14px; 402 | background-position: -2017px 0px; 403 | } 404 | &.gr { 405 | height: 14px; 406 | background-position: -2039px 0px; 407 | } 408 | &.gs { 409 | height: 10px; 410 | background-position: -2061px 0px; 411 | } 412 | &.gt { 413 | height: 13px; 414 | background-position: -2083px 0px; 415 | } 416 | &.gu { 417 | height: 11px; 418 | background-position: -2105px 0px; 419 | } 420 | &.gw { 421 | height: 10px; 422 | background-position: -2127px 0px; 423 | } 424 | &.gy { 425 | height: 12px; 426 | background-position: -2149px 0px; 427 | } 428 | &.hk { 429 | height: 14px; 430 | background-position: -2171px 0px; 431 | } 432 | &.hm { 433 | height: 10px; 434 | background-position: -2193px 0px; 435 | } 436 | &.hn { 437 | height: 10px; 438 | background-position: -2215px 0px; 439 | } 440 | &.hr { 441 | height: 10px; 442 | background-position: -2237px 0px; 443 | } 444 | &.ht { 445 | height: 12px; 446 | background-position: -2259px 0px; 447 | } 448 | &.hu { 449 | height: 10px; 450 | background-position: -2281px 0px; 451 | } 452 | &.ic { 453 | height: 14px; 454 | background-position: -2303px 0px; 455 | } 456 | &.id { 457 | height: 14px; 458 | background-position: -2325px 0px; 459 | } 460 | &.ie { 461 | height: 10px; 462 | background-position: -2347px 0px; 463 | } 464 | &.il { 465 | height: 15px; 466 | background-position: -2369px 0px; 467 | } 468 | &.im { 469 | height: 10px; 470 | background-position: -2391px 0px; 471 | } 472 | &.in { 473 | height: 14px; 474 | background-position: -2413px 0px; 475 | } 476 | &.io { 477 | height: 10px; 478 | background-position: -2435px 0px; 479 | } 480 | &.iq { 481 | height: 14px; 482 | background-position: -2457px 0px; 483 | } 484 | &.ir { 485 | height: 12px; 486 | background-position: -2479px 0px; 487 | } 488 | &.is { 489 | height: 15px; 490 | background-position: -2501px 0px; 491 | } 492 | &.it { 493 | height: 14px; 494 | background-position: -2523px 0px; 495 | } 496 | &.je { 497 | height: 12px; 498 | background-position: -2545px 0px; 499 | } 500 | &.jm { 501 | height: 10px; 502 | background-position: -2567px 0px; 503 | } 504 | &.jo { 505 | height: 10px; 506 | background-position: -2589px 0px; 507 | } 508 | &.jp { 509 | height: 14px; 510 | background-position: -2611px 0px; 511 | } 512 | &.ke { 513 | height: 14px; 514 | background-position: -2633px 0px; 515 | } 516 | &.kg { 517 | height: 12px; 518 | background-position: -2655px 0px; 519 | } 520 | &.kh { 521 | height: 13px; 522 | background-position: -2677px 0px; 523 | } 524 | &.ki { 525 | height: 10px; 526 | background-position: -2699px 0px; 527 | } 528 | &.km { 529 | height: 12px; 530 | background-position: -2721px 0px; 531 | } 532 | &.kn { 533 | height: 14px; 534 | background-position: -2743px 0px; 535 | } 536 | &.kp { 537 | height: 10px; 538 | background-position: -2765px 0px; 539 | } 540 | &.kr { 541 | height: 14px; 542 | background-position: -2787px 0px; 543 | } 544 | &.kw { 545 | height: 10px; 546 | background-position: -2809px 0px; 547 | } 548 | &.ky { 549 | height: 10px; 550 | background-position: -2831px 0px; 551 | } 552 | &.kz { 553 | height: 10px; 554 | background-position: -2853px 0px; 555 | } 556 | &.la { 557 | height: 14px; 558 | background-position: -2875px 0px; 559 | } 560 | &.lb { 561 | height: 14px; 562 | background-position: -2897px 0px; 563 | } 564 | &.lc { 565 | height: 10px; 566 | background-position: -2919px 0px; 567 | } 568 | &.li { 569 | height: 12px; 570 | background-position: -2941px 0px; 571 | } 572 | &.lk { 573 | height: 10px; 574 | background-position: -2963px 0px; 575 | } 576 | &.lr { 577 | height: 11px; 578 | background-position: -2985px 0px; 579 | } 580 | &.ls { 581 | height: 14px; 582 | background-position: -3007px 0px; 583 | } 584 | &.lt { 585 | height: 12px; 586 | background-position: -3029px 0px; 587 | } 588 | &.lu { 589 | height: 12px; 590 | background-position: -3051px 0px; 591 | } 592 | &.lv { 593 | height: 10px; 594 | background-position: -3073px 0px; 595 | } 596 | &.ly { 597 | height: 10px; 598 | background-position: -3095px 0px; 599 | } 600 | &.ma { 601 | height: 14px; 602 | background-position: -3117px 0px; 603 | } 604 | &.mc { 605 | height: 15px; 606 | background-position: -3139px 0px; 607 | } 608 | &.md { 609 | height: 10px; 610 | background-position: -3160px 0px; 611 | } 612 | &.me { 613 | height: 10px; 614 | background-position: -3182px 0px; 615 | } 616 | &.mf { 617 | height: 14px; 618 | background-position: -3204px 0px; 619 | } 620 | &.mg { 621 | height: 14px; 622 | background-position: -3226px 0px; 623 | } 624 | &.mh { 625 | height: 11px; 626 | background-position: -3248px 0px; 627 | } 628 | &.mk { 629 | height: 10px; 630 | background-position: -3270px 0px; 631 | } 632 | &.ml { 633 | height: 14px; 634 | background-position: -3292px 0px; 635 | } 636 | &.mm { 637 | height: 14px; 638 | background-position: -3314px 0px; 639 | } 640 | &.mn { 641 | height: 10px; 642 | background-position: -3336px 0px; 643 | } 644 | &.mo { 645 | height: 14px; 646 | background-position: -3358px 0px; 647 | } 648 | &.mp { 649 | height: 10px; 650 | background-position: -3380px 0px; 651 | } 652 | &.mq { 653 | height: 14px; 654 | background-position: -3402px 0px; 655 | } 656 | &.mr { 657 | height: 14px; 658 | background-position: -3424px 0px; 659 | } 660 | &.ms { 661 | height: 10px; 662 | background-position: -3446px 0px; 663 | } 664 | &.mt { 665 | height: 14px; 666 | background-position: -3468px 0px; 667 | } 668 | &.mu { 669 | height: 14px; 670 | background-position: -3490px 0px; 671 | } 672 | &.mv { 673 | height: 14px; 674 | background-position: -3512px 0px; 675 | } 676 | &.mw { 677 | height: 14px; 678 | background-position: -3534px 0px; 679 | } 680 | &.mx { 681 | height: 12px; 682 | background-position: -3556px 0px; 683 | } 684 | &.my { 685 | height: 10px; 686 | background-position: -3578px 0px; 687 | } 688 | &.mz { 689 | height: 14px; 690 | background-position: -3600px 0px; 691 | } 692 | &.na { 693 | height: 14px; 694 | background-position: -3622px 0px; 695 | } 696 | &.nc { 697 | height: 10px; 698 | background-position: -3644px 0px; 699 | } 700 | &.ne { 701 | height: 15px; 702 | background-position: -3666px 0px; 703 | } 704 | &.nf { 705 | height: 10px; 706 | background-position: -3686px 0px; 707 | } 708 | &.ng { 709 | height: 10px; 710 | background-position: -3708px 0px; 711 | } 712 | &.ni { 713 | height: 12px; 714 | background-position: -3730px 0px; 715 | } 716 | &.nl { 717 | height: 14px; 718 | background-position: -3752px 0px; 719 | } 720 | &.no { 721 | height: 15px; 722 | background-position: -3774px 0px; 723 | } 724 | &.np { 725 | height: 15px; 726 | background-position: -3796px 0px; 727 | } 728 | &.nr { 729 | height: 10px; 730 | background-position: -3811px 0px; 731 | } 732 | &.nu { 733 | height: 10px; 734 | background-position: -3833px 0px; 735 | } 736 | &.nz { 737 | height: 10px; 738 | background-position: -3855px 0px; 739 | } 740 | &.om { 741 | height: 10px; 742 | background-position: -3877px 0px; 743 | } 744 | &.pa { 745 | height: 14px; 746 | background-position: -3899px 0px; 747 | } 748 | &.pe { 749 | height: 14px; 750 | background-position: -3921px 0px; 751 | } 752 | &.pf { 753 | height: 14px; 754 | background-position: -3943px 0px; 755 | } 756 | &.pg { 757 | height: 15px; 758 | background-position: -3965px 0px; 759 | } 760 | &.ph { 761 | height: 10px; 762 | background-position: -3987px 0px; 763 | } 764 | &.pk { 765 | height: 14px; 766 | background-position: -4009px 0px; 767 | } 768 | &.pl { 769 | height: 13px; 770 | background-position: -4031px 0px; 771 | } 772 | &.pm { 773 | height: 14px; 774 | background-position: -4053px 0px; 775 | } 776 | &.pn { 777 | height: 10px; 778 | background-position: -4075px 0px; 779 | } 780 | &.pr { 781 | height: 14px; 782 | background-position: -4097px 0px; 783 | } 784 | &.ps { 785 | height: 10px; 786 | background-position: -4119px 0px; 787 | } 788 | &.pt { 789 | height: 14px; 790 | background-position: -4141px 0px; 791 | } 792 | &.pw { 793 | height: 13px; 794 | background-position: -4163px 0px; 795 | } 796 | &.py { 797 | height: 11px; 798 | background-position: -4185px 0px; 799 | } 800 | &.qa { 801 | height: 8px; 802 | background-position: -4207px 0px; 803 | } 804 | &.re { 805 | height: 14px; 806 | background-position: -4229px 0px; 807 | } 808 | &.ro { 809 | height: 14px; 810 | background-position: -4251px 0px; 811 | } 812 | &.rs { 813 | height: 14px; 814 | background-position: -4273px 0px; 815 | } 816 | &.ru { 817 | height: 14px; 818 | background-position: -4295px 0px; 819 | } 820 | &.rw { 821 | height: 14px; 822 | background-position: -4317px 0px; 823 | } 824 | &.sa { 825 | height: 14px; 826 | background-position: -4339px 0px; 827 | } 828 | &.sb { 829 | height: 10px; 830 | background-position: -4361px 0px; 831 | } 832 | &.sc { 833 | height: 10px; 834 | background-position: -4383px 0px; 835 | } 836 | &.sd { 837 | height: 10px; 838 | background-position: -4405px 0px; 839 | } 840 | &.se { 841 | height: 13px; 842 | background-position: -4427px 0px; 843 | } 844 | &.sg { 845 | height: 14px; 846 | background-position: -4449px 0px; 847 | } 848 | &.sh { 849 | height: 10px; 850 | background-position: -4471px 0px; 851 | } 852 | &.si { 853 | height: 10px; 854 | background-position: -4493px 0px; 855 | } 856 | &.sj { 857 | height: 15px; 858 | background-position: -4515px 0px; 859 | } 860 | &.sk { 861 | height: 14px; 862 | background-position: -4537px 0px; 863 | } 864 | &.sl { 865 | height: 14px; 866 | background-position: -4559px 0px; 867 | } 868 | &.sm { 869 | height: 15px; 870 | background-position: -4581px 0px; 871 | } 872 | &.sn { 873 | height: 14px; 874 | background-position: -4603px 0px; 875 | } 876 | &.so { 877 | height: 14px; 878 | background-position: -4625px 0px; 879 | } 880 | &.sr { 881 | height: 14px; 882 | background-position: -4647px 0px; 883 | } 884 | &.ss { 885 | height: 10px; 886 | background-position: -4669px 0px; 887 | } 888 | &.st { 889 | height: 10px; 890 | background-position: -4691px 0px; 891 | } 892 | &.sv { 893 | height: 12px; 894 | background-position: -4713px 0px; 895 | } 896 | &.sx { 897 | height: 14px; 898 | background-position: -4735px 0px; 899 | } 900 | &.sy { 901 | height: 14px; 902 | background-position: -4757px 0px; 903 | } 904 | &.sz { 905 | height: 14px; 906 | background-position: -4779px 0px; 907 | } 908 | &.ta { 909 | height: 10px; 910 | background-position: -4801px 0px; 911 | } 912 | &.tc { 913 | height: 10px; 914 | background-position: -4823px 0px; 915 | } 916 | &.td { 917 | height: 14px; 918 | background-position: -4845px 0px; 919 | } 920 | &.tf { 921 | height: 14px; 922 | background-position: -4867px 0px; 923 | } 924 | &.tg { 925 | height: 13px; 926 | background-position: -4889px 0px; 927 | } 928 | &.th { 929 | height: 14px; 930 | background-position: -4911px 0px; 931 | } 932 | &.tj { 933 | height: 10px; 934 | background-position: -4933px 0px; 935 | } 936 | &.tk { 937 | height: 10px; 938 | background-position: -4955px 0px; 939 | } 940 | &.tl { 941 | height: 10px; 942 | background-position: -4977px 0px; 943 | } 944 | &.tm { 945 | height: 14px; 946 | background-position: -4999px 0px; 947 | } 948 | &.tn { 949 | height: 14px; 950 | background-position: -5021px 0px; 951 | } 952 | &.to { 953 | height: 10px; 954 | background-position: -5043px 0px; 955 | } 956 | &.tr { 957 | height: 14px; 958 | background-position: -5065px 0px; 959 | } 960 | &.tt { 961 | height: 12px; 962 | background-position: -5087px 0px; 963 | } 964 | &.tv { 965 | height: 10px; 966 | background-position: -5109px 0px; 967 | } 968 | &.tw { 969 | height: 14px; 970 | background-position: -5131px 0px; 971 | } 972 | &.tz { 973 | height: 14px; 974 | background-position: -5153px 0px; 975 | } 976 | &.ua { 977 | height: 14px; 978 | background-position: -5175px 0px; 979 | } 980 | &.ug { 981 | height: 14px; 982 | background-position: -5197px 0px; 983 | } 984 | &.um { 985 | height: 11px; 986 | background-position: -5219px 0px; 987 | } 988 | &.us { 989 | height: 11px; 990 | background-position: -5241px 0px; 991 | } 992 | &.uy { 993 | height: 14px; 994 | background-position: -5263px 0px; 995 | } 996 | &.uz { 997 | height: 10px; 998 | background-position: -5285px 0px; 999 | } 1000 | &.va { 1001 | height: 15px; 1002 | background-position: -5307px 0px; 1003 | } 1004 | &.vc { 1005 | height: 14px; 1006 | background-position: -5324px 0px; 1007 | } 1008 | &.ve { 1009 | height: 14px; 1010 | background-position: -5346px 0px; 1011 | } 1012 | &.vg { 1013 | height: 10px; 1014 | background-position: -5368px 0px; 1015 | } 1016 | &.vi { 1017 | height: 14px; 1018 | background-position: -5390px 0px; 1019 | } 1020 | &.vn { 1021 | height: 14px; 1022 | background-position: -5412px 0px; 1023 | } 1024 | &.vu { 1025 | height: 12px; 1026 | background-position: -5434px 0px; 1027 | } 1028 | &.wf { 1029 | height: 14px; 1030 | background-position: -5456px 0px; 1031 | } 1032 | &.ws { 1033 | height: 10px; 1034 | background-position: -5478px 0px; 1035 | } 1036 | &.xk { 1037 | height: 15px; 1038 | background-position: -5500px 0px; 1039 | } 1040 | &.ye { 1041 | height: 14px; 1042 | background-position: -5522px 0px; 1043 | } 1044 | &.yt { 1045 | height: 14px; 1046 | background-position: -5544px 0px; 1047 | } 1048 | &.za { 1049 | height: 14px; 1050 | background-position: -5566px 0px; 1051 | } 1052 | &.zm { 1053 | height: 14px; 1054 | background-position: -5588px 0px; 1055 | } 1056 | &.zw { 1057 | height: 10px; 1058 | background-position: -5610px 0px; 1059 | } 1060 | } 1061 | --------------------------------------------------------------------------------