Bug 1474905 - Use a dropdown for the state/province field when possible. r=MattN

Differential Revision: https://phabricator.services.mozilla.com/D11854

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2018-12-20 22:10:00 +00:00
Родитель 4b7924d93b
Коммит 4bf1467ca5
9 изменённых файлов: 252 добавлений и 22 удалений

Просмотреть файл

@ -96,6 +96,10 @@ let PaymentFrameScript = {
return Cu.cloneInto(format, waivedContent);
},
findAddressSelectOption(selectEl, address, fieldName) {
return FormAutofillUtils.findAddressSelectOption(selectEl, address, fieldName);
},
getDefaultPreferences() {
let prefValues = Cu.cloneInto({
saveCreditCardDefaultChecked:

Просмотреть файл

@ -100,6 +100,7 @@ export default class AddressForm extends
}, record, {
DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
getFormFormat: PaymentDialogUtils.getFormFormat,
findAddressSelectOption: PaymentDialogUtils.findAddressSelectOption,
countries: PaymentDialogUtils.countries,
});

Просмотреть файл

@ -57,6 +57,7 @@ var PaymentDialogUtils = {
addressLevel3Label: "suburb",
addressLevel2Label: "city",
addressLevel1Label: "province",
addressLevel1Options: null,
postalCodeLabel: "postalCode",
fieldsOrder: [
{
@ -79,6 +80,21 @@ var PaymentDialogUtils = {
};
}
let addressLevel1Options = null;
if (country == "US") {
addressLevel1Options = new Map([
["CA", "California"],
["MA", "Massachusetts"],
["MI", "Michigan"],
]);
} else if (country == "CA") {
addressLevel1Options = new Map([
["NS", "Nova Scotia"],
["ON", "Ontario"],
["YT", "Yukon"],
]);
}
let fieldsOrder = [
{fieldId: "name", newLine: true},
{fieldId: "street-address", newLine: true},
@ -95,6 +111,7 @@ var PaymentDialogUtils = {
addressLevel3Label: "suburb",
addressLevel2Label: "city",
addressLevel1Label: country == "US" ? "state" : "province",
addressLevel1Options,
postalCodeLabel: country == "US" ? "zip" : "postalCode",
fieldsOrder,
// The following values come from addressReferences.js and should not be changed.
@ -105,6 +122,9 @@ var PaymentDialogUtils = {
["street-address", "address-level2", "postal-code"],
};
},
findAddressSelectOption(selectEl, address, fieldName) {
return null;
},
getDefaultPreferences() {
let prefValues = {
saveCreditCardDefaultChecked: false,

Просмотреть файл

@ -205,7 +205,11 @@ add_task(async function test_saveButton() {
fillField(form.form.querySelector("#country"), "CA");
ok(form.saveButton.disabled, "Save button is disabled after changing the country to Canada");
fillField(form.form.querySelector("#country"), "US");
ok(!form.saveButton.disabled, "Save button is enabled after changing the country to US");
ok(form.saveButton.disabled,
"Save button is disabled after changing the country back to US since address-level1 " +
"got cleared when changing countries");
fillField(form.form.querySelector("#address-level1"), "CA");
ok(!form.saveButton.disabled, "Save button is enabled after re-entering address-level1");
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
is(form.saveButton.textContent, "Next", "Check label");
@ -444,25 +448,25 @@ add_task(async function test_field_validation() {
sendStringAndCheckValidity(addressLevel1Input, "MI", true);
sendStringAndCheckValidity(addressLevel1Input, "", false);
sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false);
sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
sendStringAndCheckValidity(addressLevel1Input, "NS", false);
sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", false);
sendStringAndCheckValidity(addressLevel1Input, "", false);
sendStringAndCheckValidity(postalCodeInput, "11109", true);
sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
sendStringAndCheckValidity(addressLevel1Input, "NS", false);
sendStringAndCheckValidity(postalCodeInput, "06390-0001", true);
fillField(countrySelect, "CA");
sendStringAndCheckValidity(postalCodeInput, "00001", false);
sendStringAndCheckValidity(addressLevel1Input, "CA", true);
sendStringAndCheckValidity(addressLevel1Input, "CA", false);
sendStringAndCheckValidity(postalCodeInput, "94043", false);
sendStringAndCheckValidity(addressLevel1Input, "", false);
sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true);
sendStringAndCheckValidity(addressLevel1Input, "MI", true);
sendStringAndCheckValidity(addressLevel1Input, "MI", false);
sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true);
sendStringAndCheckValidity(addressLevel1Input, "", false);
sendStringAndCheckValidity(postalCodeInput, "11109", false);
sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
sendStringAndCheckValidity(addressLevel1Input, "NS", true);
sendStringAndCheckValidity(postalCodeInput, "06390-0001", false);
form.remove();

Просмотреть файл

@ -94,7 +94,7 @@ let AddressDataLoader = {
return null;
}
const properties = ["languages", "sub_keys", "sub_names", "sub_lnames"];
const properties = ["languages", "sub_keys", "sub_isoids", "sub_names", "sub_lnames"];
for (let key of properties) {
if (!data[key]) {
continue;
@ -536,6 +536,40 @@ this.FormAutofillUtils = {
}, []);
},
/**
* Used to populate dropdowns in the UI (e.g. FormAutofill preferences, Web Payments).
* Use findAddressSelectOption for matching a value to a region.
*
* @param {string[]} subKeys An array of regionCode strings
* @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key
* @param {string[]} subNames An array of regionName strings
* @param {string[]} subLnames An array of latinised regionName strings
* @returns {Map?} Returns null if subKeys or subNames are not truthy.
* Otherwise, a Map will be returned mapping keys -> names.
*/
buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) {
// Not all regions have sub_keys. e.g. DE
if (!subKeys || !subKeys.length ||
(!subNames && !subLnames) ||
(subNames && subKeys.length != subNames.length ||
subLnames && subKeys.length != subLnames.length)) {
return null;
}
// Overwrite subKeys with subIsoids, when available
if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) {
for (let i = 0; i < subIsoids.length; i++) {
if (subIsoids[i]) {
subKeys[i] = subIsoids[i];
}
}
}
// Apply sub_lnames if sub_names does not exist
let names = subNames || subLnames;
return new Map(subKeys.map((key, index) => [key, names[index]]));
},
/**
* Parse a require string and outputs an array of fields.
* Spaces, commas, and other literals are ignored in this implementation.
@ -866,10 +900,11 @@ this.FormAutofillUtils = {
addressLevel3Label: dataset.sublocality_name_type || "suburb",
addressLevel2Label: dataset.locality_name_type || "city",
addressLevel1Label: dataset.state_name_type || "province",
postalCodeLabel: dataset.zip_name_type || "postalCode",
fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
postalCodePattern: dataset.zip,
addressLevel1Options: this.buildRegionMapIfAvailable(dataset.sub_keys, dataset.sub_isoids, dataset.sub_names, dataset.sub_lnames),
countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
postalCodeLabel: dataset.zip_name_type || "postalCode",
postalCodePattern: dataset.zip,
};
},

Просмотреть файл

@ -135,6 +135,8 @@ class EditAddress extends EditAutofillForm {
* @param {object} config
* @param {string[]} config.DEFAULT_REGION
* @param {function} config.getFormFormat Function to return form layout info for a given country.
* @param {function} config.findAddressSelectOption Finds the matching select option for a given
select element, address, and fieldName.
* @param {string[]} config.countries
* @param {boolean} [config.noValidate=undefined] Whether to validate the form
*/
@ -167,7 +169,12 @@ class EditAddress extends EditAutofillForm {
country: this.DEFAULT_REGION,
};
}
let {addressLevel1Options} = this.getFormFormat(record.country);
this.populateAddressLevel1(addressLevel1Options, record.country);
super.loadRecord(record);
this.loadAddressLevel1(record["address-level1"], record.country);
this.formatForm(record.country);
}
@ -228,6 +235,7 @@ class EditAddress extends EditAutofillForm {
addressLevel3Label,
addressLevel2Label,
addressLevel1Label,
addressLevel1Options,
postalCodeLabel,
fieldsOrder: mailingFieldsOrder,
postalCodePattern,
@ -248,6 +256,7 @@ class EditAddress extends EditAutofillForm {
}
this.arrangeFields(fieldClasses, requiredFields);
this.updatePostalCodeValidation(postalCodePattern);
this.populateAddressLevel1(addressLevel1Options, country);
}
/**
@ -318,6 +327,85 @@ class EditAddress extends EditAutofillForm {
}
}
/**
* Set the address-level1 value on the form field (input or select, whichever is present).
*
* @param {string} addressLevel1Value Value of the address-level1 from the autofill record
* @param {string} country The corresponding country
*/
loadAddressLevel1(addressLevel1Value, country) {
let field = this._elements.form.querySelector("#address-level1");
if (field.localName == "input") {
field.value = addressLevel1Value || "";
return;
}
let matchedSelectOption = this.findAddressSelectOption(field, {
country,
"address-level1": addressLevel1Value,
}, "address-level1");
if (matchedSelectOption && !matchedSelectOption.selected) {
field.value = matchedSelectOption.value;
field.dispatchEvent(new Event("input", {bubbles: true}));
field.dispatchEvent(new Event("change", {bubbles: true}));
} else if (addressLevel1Value) {
// If the option wasn't found, insert an option at the beginning of
// the select that matches the stored value.
field.insertBefore(new Option(addressLevel1Value, addressLevel1Value, true, true), field.firstChild);
}
}
/**
* Replace the text input for address-level1 with a select dropdown if
* a fixed set of names exists. Otherwise show a text input.
*
* @param {Map?} options Map of options with regionCode -> name mappings
* @param {string} country The corresponding country
*/
populateAddressLevel1(options, country) {
let field = this._elements.form.querySelector("#address-level1");
if (field.dataset.country == country) {
return;
}
if (!options) {
if (field.localName == "input") {
return;
}
let input = document.createElement("input");
input.setAttribute("type", "text");
input.id = "address-level1";
input.required = field.required;
input.disabled = field.disabled;
input.tabIndex = field.tabIndex;
field.replaceWith(input);
return;
}
if (field.localName == "input") {
let select = document.createElement("select");
select.id = "address-level1";
select.required = field.required;
select.disabled = field.disabled;
select.tabIndex = field.tabIndex;
field.replaceWith(select);
field = select;
}
field.textContent = "";
field.dataset.country = country;
let fragment = document.createDocumentFragment();
fragment.appendChild(new Option(undefined, undefined, true, true));
for (let [regionCode, regionName] of options) {
let option = new Option(regionName, regionCode);
fragment.appendChild(option);
}
field.appendChild(fragment);
}
populateCountries() {
let fragment = document.createDocumentFragment();
// Sort countries by their visible names.

Просмотреть файл

@ -53,6 +53,9 @@
<span class="label-text"/>
</label>
<label id="address-level1-container" class="container">
<!-- The address-level1 input will get replaced by a select dropdown
by autofillEditForms.js when the selected country has provided
specific options. -->
<input id="address-level1" type="text"/>
<span class="label-text"/>
</label>
@ -89,6 +92,7 @@
} = FormAutofill;
let {
getFormFormat,
findAddressSelectOption,
} = FormAutofillUtils;
let args = window.arguments || [];
let {
@ -102,6 +106,7 @@
}, record, {
DEFAULT_REGION,
getFormFormat: getFormFormat.bind(FormAutofillUtils),
findAddressSelectOption: findAddressSelectOption.bind(FormAutofillUtils),
countries,
noValidate,
});

Просмотреть файл

@ -25,6 +25,7 @@ class AutofillEditDialog {
}
async init() {
this.updateSaveButtonState();
this.attachEventListeners();
// For testing only: signal to tests that the dialog is ready for testing.
// This is likely no longer needed since retrieving from storage is fully
@ -108,13 +109,7 @@ class AutofillEditDialog {
* @param {DOMEvent} event
*/
handleInput(event) {
// Toggle disabled attribute on the save button based on
// whether the form is filled or empty.
if (Object.keys(this._elements.fieldContainer.buildFormObject()).length == 0) {
this._elements.save.setAttribute("disabled", true);
} else {
this._elements.save.removeAttribute("disabled");
}
this.updateSaveButtonState();
}
/**
@ -128,6 +123,16 @@ class AutofillEditDialog {
}
}
updateSaveButtonState() {
// Toggle disabled attribute on the save button based on
// whether the form is filled or empty.
if (Object.keys(this._elements.fieldContainer.buildFormObject()).length == 0) {
this._elements.save.setAttribute("disabled", true);
} else {
this._elements.save.removeAttribute("disabled");
}
}
/**
* Attach event listener
*/

Просмотреть файл

@ -56,7 +56,7 @@ add_task(async function test_saveAddress() {
is(doc.querySelector("#postal-code-container > .label-text").textContent, "ZIP Code",
"US postal-code label should be 'ZIP Code'");
// Input address info and verify move through form with tab keys
const keyInputs = [
const keypresses = [
"VK_TAB",
TEST_ADDRESS_1["given-name"],
"VK_TAB",
@ -83,7 +83,16 @@ add_task(async function test_saveAddress() {
"VK_TAB",
"VK_RETURN",
];
keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
keypresses.forEach(keypress => {
if (doc.activeElement.localName == "select" && !keypress.startsWith("VK_")) {
let field = doc.activeElement;
while (field.value != keypress) {
EventUtils.synthesizeKey(keypress[0], {}, win);
}
} else {
EventUtils.synthesizeKey(keypress, {}, win);
}
});
});
let addresses = await getAddresses();
@ -100,6 +109,11 @@ add_task(async function test_editAddress() {
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("VK_RIGHT", {}, win);
EventUtils.synthesizeKey("test", {}, win);
let stateSelect = win.document.querySelector("#address-level1");
is(stateSelect.selectedOptions[0].value, TEST_ADDRESS_1["address-level1"],
"address-level1 should be selected in the dropdown");
win.document.querySelector("#save").click();
}, {
record: addresses[0],
@ -114,6 +128,31 @@ add_task(async function test_editAddress() {
is(addresses.length, 0, "Address storage is empty");
});
add_task(async function test_editAddressFrenchCanadianChangedToEnglishRepresentation() {
let addressClone = Object.assign({}, TEST_ADDRESS_CA_1);
addressClone["address-level1"] = "Colombie-Britannique";
await saveAddress(addressClone);
let addresses = await getAddresses();
await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
let stateSelect = win.document.querySelector("#address-level1");
is(stateSelect.selectedOptions[0].value, "BC",
"address-level1 should have 'BC' selected in the dropdown");
win.document.querySelector("#save").click();
}, {
record: addresses[0],
});
addresses = await getAddresses();
is(addresses.length, 1, "only one address is in storage");
is(addresses[0]["address-level1"], "BC", "address-level1 changed");
await removeAddresses([addresses[0].guid]);
addresses = await getAddresses();
is(addresses.length, 0, "Address storage is empty");
});
add_task(async function test_editSparseAddress() {
let record = {...TEST_ADDRESS_1};
info("delete some usually required properties");
@ -294,7 +333,7 @@ add_task(async function test_saveAddressIE() {
await removeAllRecords();
});
add_task(async function test_countryFieldLabels() {
add_task(async function test_countryAndStateFieldLabels() {
await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
let doc = win.document;
// Change country to verify labels
@ -309,7 +348,7 @@ add_task(async function test_countryFieldLabels() {
for (let countryOption of doc.querySelector("#country").options) {
if (countryOption.value == "") {
info("Skipping the empty option");
info("Skipping the empty country option");
continue;
}
@ -334,6 +373,31 @@ add_task(async function test_countryFieldLabels() {
is(labelEl.dataset.localization, undefined,
"Ensure data-localization was removed: " + countryOption.value);
}
let stateOptions = doc.querySelector("#address-level1").options;
/* eslint-disable max-len */
let expectedStateOptions = {
"BS": {
// The Bahamas is an interesting testcase because they have some keys that are full names, and others are replaced with ISO IDs.
"keys": "Abaco~AK~Andros~BY~BI~CI~Crooked Island~Eleuthera~EX~Grand Bahama~HI~IN~LI~MG~N.P.~RI~RC~SS~SW".split("~"),
"names": "Abaco Islands~Acklins~Andros Island~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma and Cays~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~New Providence~Ragged Island~Rum Cay~San Salvador~Spanish Wells".split("~"),
},
"US": {
"keys": "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY".split("~"),
"names": "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming".split("~"),
},
};
/* eslint-enable max-len */
if (expectedStateOptions[countryOption.value]) {
let {keys, names} = expectedStateOptions[countryOption.value];
is(stateOptions.length, keys.length + 1, "stateOptions should list all options plus a blank entry");
is(stateOptions[0].value, "", "First State option should be blank");
for (let i = 1; i < stateOptions.length; i++) {
is(stateOptions[i].value, keys[i - 1], "Each State should be listed in alphabetical name order (key)");
is(stateOptions[i].text, names[i - 1], "Each State should be listed in alphabetical name order (name)");
}
}
}
doc.querySelector("#cancel").click();
@ -454,7 +518,9 @@ add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
doc.querySelector("#address-level2").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
doc.querySelector("#address-level1").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
while (doc.querySelector("#address-level1").value != TEST_ADDRESS_1["address-level1"]) {
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"][0], {}, win);
}
doc.querySelector("#save").focus();
EventUtils.synthesizeKey("VK_RETURN", {}, win);
});
@ -496,6 +562,7 @@ add_task(async function test_countrySpecificFieldsGetRequiredness() {
EventUtils.synthesizeKey("United States", {}, win);
await TestUtils.waitForCondition(() => {
provinceField = doc.getElementById("address-level1");
return provinceField.parentNode.style.display != "none";
}, "Wait for address-level1 to become visible", 10);
@ -506,6 +573,7 @@ add_task(async function test_countrySpecificFieldsGetRequiredness() {
EventUtils.synthesizeKey("Romania", {}, win);
await TestUtils.waitForCondition(() => {
provinceField = doc.getElementById("address-level1");
return provinceField.parentNode.style.display == "none";
}, "Wait for address-level1 to become hidden", 10);