Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"globalThis": true
},
"rules": {
// `no-unresolved` doesn't support package.json exports
// See: https://github.com/import-js/eslint-plugin-import/issues/1810
"import/no-unresolved": ["error", { "ignore": ["intl-tel-input"] }],
"no-restricted-syntax": [
"error",
{
Expand Down
4 changes: 4 additions & 0 deletions app/components/phone_input_component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ lg-phone-input {
}
}

.iti__search-input {
@extend %block-input-styles;
}

.iti__dial-code {
color: color('ink');
}
Expand Down
16 changes: 6 additions & 10 deletions app/javascript/packages/phone-input/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@ import userEvent from '@testing-library/user-event';
import { computeAccessibleName } from 'dom-accessibility-api';
import * as analytics from '@18f/identity-analytics';
import { useSandbox } from '@18f/identity-test-helpers';
import './index.ts';

describe('PhoneInput', () => {
const sandbox = useSandbox();

before(async () => {
await import('intl-tel-input/build/js/utils.js');
window.intlTelInputUtils = global.intlTelInputUtils;
await import('./index');
});

beforeEach(() => {
sandbox.stub(analytics, 'trackEvent');
});
Expand Down Expand Up @@ -164,6 +159,7 @@ describe('PhoneInput', () => {
expect(analytics.trackEvent).to.have.been.calledOnceWith('phone_input_country_changed', {
country_code: 'CA',
});
await userEvent.clear(phoneNumber);

const dropdownButton = getByRole(iti, 'combobox', { name: 'Country code' });
await userEvent.click(dropdownButton);
Expand All @@ -180,7 +176,7 @@ describe('PhoneInput', () => {

it('renders as an accessible combobox', () => {
const phoneInput = createAndConnectElement();
const comboboxes = getAllByRole(phoneInput, 'combobox');
const comboboxes = getAllByRole(phoneInput, 'combobox', { name: 'Country code' });
const listbox = getByRole(phoneInput, 'listbox');

// There are two comboboxes, one for no-JavaScript, and the other JavaScript enhanced. Only one
Expand All @@ -198,7 +194,7 @@ describe('PhoneInput', () => {
//
// See: https://w3c.github.io/aria/#combobox
const value = combobox.textContent;
const controlled = document.getElementById(combobox.getAttribute('aria-controls')!);
const controlled = document.getElementById(combobox.getAttribute('aria-controls')!)!;

// "listbox" is the default value for a combobox role.
//
Expand All @@ -212,8 +208,8 @@ describe('PhoneInput', () => {
// See: https://github.com/jsdom/jsdom/issues/3323
expect(hasPopup).to.be.oneOf([null, 'listbox']);
expect(name).to.equal('Country code');
expect(value).to.equal('United States: +1');
expect(controlled).to.equal(listbox);
expect(value).to.equal('United States +1');
expect(controlled.contains(listbox)).to.be.true();
});

context('with constrained delivery options', () => {
Expand Down
65 changes: 19 additions & 46 deletions app/javascript/packages/phone-input/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { isValidNumberForRegion, isValidNumber } from 'libphonenumber-js';
import 'intl-tel-input/build/js/utils.js';
import intlTelInput from 'intl-tel-input';
import intlTelInput from 'intl-tel-input/intlTelInputWithUtils';
import type { CountryCode } from 'libphonenumber-js';
import type { Plugin as IntlTelInputPlugin, Options } from 'intl-tel-input';
import type { Iti } from 'intl-tel-input';
import { replaceVariables } from '@18f/identity-i18n';
import { trackEvent } from '@18f/identity-analytics';

Expand All @@ -16,14 +15,6 @@ interface PhoneInputStrings {
unsupported_country: string;
}

interface IntlTelInput extends IntlTelInputPlugin {
flagsContainer: HTMLElement;

selectedFlag: HTMLElement;

options: Options;
}

const updateInternationalCodeInPhone = (phone, newCode) =>
phone.replace(new RegExp(`^\\+?(\\d+\\s+|${newCode})?`), `+${newCode} `);

Expand All @@ -40,7 +31,7 @@ export class PhoneInputElement extends HTMLElement {

codeWrapper: Element | null;

iti: IntlTelInput;
iti: Iti;

connectedCallback() {
const textInput = this.querySelector<HTMLInputElement>('.phone-input__number');
Expand Down Expand Up @@ -95,15 +86,12 @@ export class PhoneInputElement extends HTMLElement {
return this.#strings;
}

/**
* Returns the element which represents the flag dropdown's currently selected value, which is
* rendered as a text element within the combobox. As defined by the ARIA specification, a
* combobox's value is determined by its contents if it is not an input element.
*
* @see https://w3c.github.io/aria/#combobox
*/
get valueText(): HTMLElement {
return this.iti.selectedFlag.querySelector('.usa-sr-only')!;
get selectedCountry(): HTMLElement {
return this.querySelector('.iti__selected-country')!;
}

get countryList(): HTMLUListElement | null {
return this.querySelector('.iti__country-list')!;
}

/**
Expand All @@ -123,10 +111,7 @@ export class PhoneInputElement extends HTMLElement {
const countryCode = this.getSelectedCountryCode();
if (countryCode) {
this.codeInput.value = countryCode;
// Move value text from title attribute to the flag's hidden text element.
// See: https://github.com/jackocnr/intl-tel-input/blob/d54b127/src/js/intlTelInput.js#L1191-L1197
this.valueText.textContent = this.iti.selectedFlag.title;
this.iti.selectedFlag.removeAttribute('title');
this.selectedCountry.removeAttribute('title');
if (fireChangeEvent) {
this.codeInput.dispatchEvent(new CustomEvent('change', { bubbles: true }));
}
Expand All @@ -137,34 +122,22 @@ export class PhoneInputElement extends HTMLElement {
const { supportedCountryCodes, countryCodePairs } = this;

const iti = intlTelInput(this.textInput, {
preferredCountries: ['US', 'CA'],
countryOrder: ['US', 'CA'],
initialCountry: this.codeInput.value,
localizedCountries: countryCodePairs,
i18n: {
...countryCodePairs,
selectedCountryAriaLabel: this.strings.country_code_label,
},
onlyCountries: supportedCountryCodes,
autoPlaceholder: 'off',
}) as IntlTelInput;
formatAsYouType: false,
useFullscreenPopup: false,
});

this.iti = iti;

// Remove duplicate items in the country list
const preferred: NodeListOf<HTMLLIElement> =
iti.countryList.querySelectorAll('.iti__preferred');
preferred.forEach((listItem) => {
const { countryCode } = listItem.dataset;
const duplicates: NodeListOf<HTMLLIElement> = iti.countryList.querySelectorAll(
`.iti__standard[data-country-code="${countryCode}"]`,
);
duplicates.forEach((duplicateListItem) => {
duplicateListItem.parentNode?.removeChild(duplicateListItem);
});
});

// Improve base accessibility of intl-tel-input
const valueText = document.createElement('div');
valueText.classList.add('usa-sr-only');
iti.selectedFlag.appendChild(valueText);
iti.selectedFlag.setAttribute('aria-label', this.strings.country_code_label);
iti.selectedFlag.removeAttribute('aria-owns');
this.selectedCountry.setAttribute('aria-haspopup', 'listbox');

this.syncCountryToCodeInput({ fireChangeEvent: false });

Expand Down
2 changes: 1 addition & 1 deletion app/javascript/packages/phone-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"version": "1.0.0",
"dependencies": {
"intl-tel-input": "^17.0.19",
"intl-tel-input": "^24.5.0",
"libphonenumber-js": "^1.11.4"
},
"sideEffects": [
Expand Down
4 changes: 1 addition & 3 deletions app/javascript/packs/idv-phone-alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ const { iti, textInput: input } = document.querySelector('lg-phone-input') as Ph
const failedPhoneNumbers: string[] = JSON.parse(alertElement.dataset.failedPhoneNumbers!);

input.addEventListener('input', () => {
const isFailedPhoneNumber = failedPhoneNumbers.includes(
iti.getNumber(intlTelInputUtils.numberFormat.E164),
);
const isFailedPhoneNumber = failedPhoneNumbers.includes(iti.getNumber());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is made because intl-tel-input no longer assigns a global intlTelInputUtils, and E.164 is the default (source).

alertElement.hidden = !isFailedPhoneNumber;
});
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"core-js": "^3.21.1",
"fast-glob": "^3.3.2",
"foundation-emails": "^2.3.1",
"intl-tel-input": "^17.0.19",
"intl-tel-input": "^24.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"source-map-loader": "^4.0.0",
Expand All @@ -56,7 +56,6 @@
"@types/cleave.js": "^1.4.7",
"@types/dirty-chai": "^2.0.2",
"@types/grecaptcha": "^3.0.4",
"@types/intl-tel-input": "^17.0.6",
"@types/mocha": "^10.0.0",
"@types/node": "^20.2.5",
"@types/react": "^17.0.39",
Expand Down
4 changes: 2 additions & 2 deletions spec/features/idv/doc_auth/hybrid_handoff_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
end

it 'displays error if user selects a country to which we cannot send SMS', js: true do
page.find('div[aria-label="Country code"]').click
within(page.find('.iti__flag-container', visible: :all)) do
click_on t('components.phone_input.country_code_label')
within(page.find('.iti__country-container', visible: :all)) do
find('span', text: 'Sri Lanka').click
end
focused_input = page.find('.phone-input__number:focus')
Expand Down
14 changes: 7 additions & 7 deletions spec/features/phone/add_phone_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@
expect(error_message).to have_content(t('errors.messages.invalid_phone_number.us'))

# Unsupported country should prompt as invalid and hide delivery options immediately
page.find('div[aria-label="Country code"]').click
within(page.find('.iti__flag-container', visible: :all)) do
click_on t('components.phone_input.country_code_label')
within(page.find('.iti__country-container', visible: :all)) do
find('span', text: 'Sri Lanka').click
end
focused_input = page.find('.phone-input__number:focus')
Expand All @@ -106,8 +106,8 @@
expect(page.find(':focus')).to match_css('.phone-input__number')

# Switching to supported country should re-show delivery options, but prompt as invalid number
page.find('div[aria-label="Country code"]').click
within(page.find('.iti__flag-container', visible: :all)) do
click_on t('components.phone_input.country_code_label')
within(page.find('.iti__country-container', visible: :all)) do
find('span', text: 'United States').click
end
expect(page).to have_content(t('two_factor_authentication.otp_delivery_preference.title'))
Expand Down Expand Up @@ -162,7 +162,7 @@
expect(page).to have_content(I18n.t('errors.messages.phone_duplicate'))

# Ensure that phone input initializes with the country flag maintained
expect(page).to have_css('.iti__selected-flag [class^="iti__flag iti__"]', visible: :all)
expect(page).to have_css('.iti__selected-country [class^="iti__flag iti__"]', visible: :all)
end
end

Expand Down Expand Up @@ -193,8 +193,8 @@
expect(page.find_field('Text message (SMS)', disabled: false, visible: :all)).to be_present
expect(page.find_field('Phone call', disabled: false, visible: :all)).to be_present

page.find('div[aria-label="Country code"]').click
within(page.find('.iti__flag-container', visible: :all)) do
click_on t('components.phone_input.country_code_label')
within(page.find('.iti__country-container', visible: :all)) do
find('span', text: 'Australia').click # a country where SMS is disabled currently
end

Expand Down
2 changes: 0 additions & 2 deletions spec/support/matchers/accessibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,6 @@ def expect_page_to_have_no_accessibility_violations(page, validate_markup: true)
:wcag22a,
:wcag22aa,
:"best-practice",
).excluding(
'.iti__selected-flag', # See: LG-14382
)
expect(page).to have_unique_ids
expect(page).to have_valid_idrefs
Expand Down
27 changes: 4 additions & 23 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1517,13 +1517,6 @@
dependencies:
"@types/node" "*"

"@types/intl-tel-input@^17.0.6":
version "17.0.6"
resolved "https://registry.yarnpkg.com/@types/intl-tel-input/-/intl-tel-input-17.0.6.tgz#43d5fa06c3e988747670bc9f8c3ef148faf9e9a2"
integrity sha512-Xqkfun/71N3wqvnwFzZiBacC3JsHHgYWjOEXxzl91nXrm/b/DLhDWM7baXOZksfLwggyOsn/McT1/neJejXmVg==
dependencies:
"@types/jquery" "*"

"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
Expand All @@ -1543,13 +1536,6 @@
dependencies:
"@types/istanbul-lib-report" "*"

"@types/jquery@*":
version "3.5.16"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.16.tgz#632131baf30951915b0317d48c98e9890bdf051d"
integrity sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==
dependencies:
"@types/sizzle" "*"

"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
Expand Down Expand Up @@ -1688,11 +1674,6 @@
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==

"@types/sizzle@*":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==

"@types/sockjs@^0.3.36":
version "0.3.36"
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535"
Expand Down Expand Up @@ -4272,10 +4253,10 @@ interpret@^3.1.1:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==

intl-tel-input@^17.0.19:
version "17.0.19"
resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-17.0.19.tgz#4c277e3bf02069fac2ef3821a62a3d7e8b55740a"
integrity sha512-GBNoUT4JVgm2e1N+yFMaBQ24g5EQfZhDznGneCM9IEZwfKsMUAUa1dS+v0wOiKpRAZ5IPNLJMIEEFGgqlCI22A==
intl-tel-input@^24.5.0:
version "24.5.0"
resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-24.5.0.tgz#1a7589f046b72a97f8cd30ebae4c0615635f542d"
integrity sha512-utSW+7a2JpUFdRRwo6CAiEQuAMwWxnCurPr2VDkfnW7W9g9ZprvPJPj00vxkiGV8h3GMRAD7aUtX9+oYwD/8OQ==

ipaddr.js@1.9.1:
version "1.9.1"
Expand Down