Bug 1422164 - Create a rich-select, address-option, and basic-card-option custom elements. r?MattN
MozReview-Commit-ID: 7EtK5GEWNZd
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -5,15 +5,16 @@
toolkit.jar:
% content payments %content/payments/
content/payments/paymentDialog.css (content/paymentDialog.css)
content/payments/paymentDialog.js (content/paymentDialog.js)
content/payments/paymentDialogFrameScript.js (content/paymentDialogFrameScript.js)
content/payments/paymentDialog.xhtml (content/paymentDialog.xhtml)
% resource payments %res/payments/
res/payments (res/paymentRequest.*)
+ res/payments/components/ (res/components/*.css)
res/payments/components/ (res/components/*.js)
res/payments/containers/ (res/containers/*.js)
res/payments/debugging.html (res/debugging.html)
res/payments/debugging.js (res/debugging.js)
res/payments/mixins/ (res/mixins/*.js)
res/payments/PaymentsStore.js (res/PaymentsStore.js)
res/payments/vendor/ (res/vendor/*)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+address-option {
+ display: grid;
+ grid-row-gap: 5px;
+ grid-column-gap: 10px;
+ grid-template-areas:
+ "recipient "
+ "addressLine";
+
+ border-bottom: 1px solid #ddd;
+ background: #fff;
+ padding: 5px;
+ padding-inline-start: 20px;
+ width: 400px;
+ font-size: .8em;
+}
+
+rich-select[open] > .rich-select-popup-box > address-option {
+ grid-template-areas:
+ "recipient recipient"
+ "addressLine addressLine"
+ "email phone ";
+}
+
+address-option > .recipient {
+ grid-area: recipient;
+}
+
+address-option > .addressLine {
+ grid-area: addressLine;
+}
+
+address-option > .email {
+ grid-area: email;
+}
+
+address-option > .phone {
+ grid-area: phone;
+}
+
+address-option > .recipient,
+address-option > .addressLine,
+address-option > .email,
+address-option > .phone {
+ white-space: nowrap;
+}
+
+.rich-select-popup-box > address-option[selected] {
+ background-color: #ffa;
+}
+
+rich-select > .rich-select-selected-clone > .email,
+rich-select > .rich-select-selected-clone > .phone {
+ display: none;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/address-option.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * <rich-select>
+ * <address-option addressLine="1234 Anywhere St"
+ * city="Some City"
+ * country="USA"
+ * dependentLocality=""
+ * languageCode="en-US"
+ * phone=""
+ * postalCode="90210"
+ * recipient="Jared Wein"
+ * region="MI"></address-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin, RichOption */
+
+class AddressOption extends ObservedPropertiesMixin(RichOption) {
+ static get observedAttributes() {
+ return RichOption.observedAttributes.concat([
+ "addressLine",
+ "city",
+ "country",
+ "dependentLocality",
+ "email",
+ "languageCode",
+ "organization",
+ "phone",
+ "postalCode",
+ "recipient",
+ "region",
+ "sortingCode",
+ ]);
+ }
+
+ connectedCallback() {
+ for (let child of this.children) {
+ child.remove();
+ }
+
+ let fragment = document.createDocumentFragment();
+ RichOption._createElement(fragment, "recipient");
+ RichOption._createElement(fragment, "addressLine");
+ RichOption._createElement(fragment, "email");
+ RichOption._createElement(fragment, "phone");
+ this.appendChild(fragment);
+
+ super.connectedCallback();
+ }
+
+ render() {
+ if (!this.parentNode) {
+ return;
+ }
+
+ this.querySelector(".recipient").textContent = this.recipient;
+ this.querySelector(".addressLine").textContent =
+ `${this.addressLine} ${this.city} ${this.region} ${this.postalCode} ${this.country}`;
+ this.querySelector(".email").textContent = this.email;
+ this.querySelector(".phone").textContent = this.phone;
+ }
+}
+
+customElements.define("address-option", AddressOption);
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/basic-card-option.css
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+basic-card-option {
+ display: grid;
+ grid-row-gap: 5px;
+ grid-column-gap: 10px;
+ grid-template-areas:
+ "owner type"
+ "number ...";
+
+ border-bottom: 1px solid #ddd;
+ background: #fff;
+ padding: 5px;
+ padding-inline-start: 20px;
+ width: 400px;
+ font-size: .8em;
+}
+
+rich-select[open] > .rich-select-popup-box > basic-card-option {
+ grid-template-areas:
+ "owner type"
+ "number expiration";
+}
+
+basic-card-option > .number {
+ grid-area: number;
+}
+
+basic-card-option > .owner {
+ grid-area: owner;
+}
+
+basic-card-option > .expiration {
+ grid-area: expiration;
+}
+
+basic-card-option > .type {
+ grid-area: type;
+}
+
+basic-card-option > .number,
+basic-card-option > .owner,
+basic-card-option > .expiration,
+basic-card-option > .type {
+ white-space: nowrap;
+}
+
+.rich-select-popup-box > basic-card-option[selected] {
+ background-color: #ffa;
+}
+
+rich-select > .rich-select-selected-clone > .expiration {
+ display: none;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/basic-card-option.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * <rich-select>
+ * <basic-card-option></basic-card-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin, RichOption */
+
+class BasicCardOption extends ObservedPropertiesMixin(RichOption) {
+ static get observedAttributes() {
+ return RichOption.observedAttributes.concat([
+ "expiration",
+ "number",
+ "owner",
+ "type",
+ ]);
+ }
+
+ connectedCallback() {
+ for (let child of this.children) {
+ child.remove();
+ }
+
+ let fragment = document.createDocumentFragment();
+ RichOption._createElement(fragment, "owner");
+ RichOption._createElement(fragment, "number");
+ RichOption._createElement(fragment, "expiration");
+ RichOption._createElement(fragment, "type");
+ this.appendChild(fragment);
+
+ super.connectedCallback();
+ }
+
+ render() {
+ if (!this.parentNode) {
+ return;
+ }
+
+ this.querySelector(".owner").textContent = this.owner;
+ this.querySelector(".number").textContent = this.number;
+ this.querySelector(".expiration").textContent = this.expiration;
+ this.querySelector(".type").textContent = this.type;
+ }
+}
+
+customElements.define("basic-card-option", BasicCardOption);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-option.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * <rich-select>
+ * <rich-option></rich-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin */
+/* exported RichOption */
+
+class RichOption extends ObservedPropertiesMixin(HTMLElement) {
+ static get observedAttributes() { return ["selected", "hidden"]; }
+
+ constructor() {
+ super();
+
+ this.addEventListener("click", this);
+ this.addEventListener("keypress", this);
+ }
+
+ connectedCallback() {
+ this.render();
+ let richSelect = this.closest("rich-select");
+ if (richSelect && richSelect.render) {
+ richSelect.render();
+ }
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "keypress": {
+ this.onKeyPress(event);
+ break;
+ }
+ }
+ }
+
+ onClick(event) {
+ if (this.closest("rich-select").open &&
+ !this.disabled &&
+ event.button == 0) {
+ for (let option of this.parentNode.children) {
+ option.selected = option == this;
+ }
+ }
+ }
+
+ onKeyPress(event) {
+ if (!this.disabled &&
+ event.which == 13 /* Enter */) {
+ for (let option of this.parentNode.children) {
+ option.selected = option == this;
+ }
+ }
+ }
+
+ get selected() {
+ return this.hasAttribute("selected");
+ }
+
+ set selected(value) {
+ if (value) {
+ let oldSelectedOptions = this.parentNode.querySelectorAll("[selected]");
+ for (let option of oldSelectedOptions) {
+ option.removeAttribute("selected");
+ }
+ this.setAttribute("selected", value);
+ } else {
+ this.removeAttribute("selected");
+ }
+ let richSelect = this.closest("rich-select");
+ if (richSelect && richSelect.render) {
+ richSelect.render();
+ }
+ return value;
+ }
+
+ static _createElement(fragment, className) {
+ let element = document.createElement("span");
+ element.classList.add(className);
+ fragment.appendChild(element);
+ return element;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-select.css
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+rich-select:not([open]) > .rich-select-popup-box {
+ display: none;
+}
+
+rich-select[open] {
+ position: relative;
+}
+
+rich-select[open] > .rich-select-popup-box {
+ position: absolute;
+ z-index: 1;
+ top: 1em;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-select.js
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * <rich-select>
+ * <rich-option></rich-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin */
+
+class RichSelect extends ObservedPropertiesMixin(HTMLElement) {
+ static get observedAttributes() {
+ return [
+ "open",
+ "disabled",
+ "hidden",
+ ];
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("blur", this);
+ this.addEventListener("click", this);
+ this.addEventListener("keypress", this);
+ }
+
+ connectedCallback() {
+ this.setAttribute("tabindex", "0");
+ this.render();
+ }
+
+ get popupBox() {
+ return this.querySelector(":scope > .rich-select-popup-box");
+ }
+
+ get selectedOption() {
+ return this.popupBox.querySelector(":scope > [selected]");
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "blur": {
+ this.onBlur(event);
+ break;
+ }
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "keypress": {
+ this.onKeyPress(event);
+ break;
+ }
+ }
+ }
+
+ onBlur(event) {
+ if (event.target == this) {
+ this.open = false;
+ }
+ }
+
+ onClick(event) {
+ if (!this.disabled &&
+ event.button == 0) {
+ this.open = !this.open;
+ }
+ }
+
+ onKeyPress(event) {
+ if (event.key == " ") {
+ this.open = !this.open;
+ } else if (event.key == "ArrowDown") {
+ let selectedOption = this.selectedOption;
+ let next = selectedOption.nextElementSibling;
+ if (next) {
+ next.selected = true;
+ selectedOption.selected = false;
+ }
+ } else if (event.key == "ArrowUp") {
+ let selectedOption = this.selectedOption;
+ let next = selectedOption.previousElementSibling;
+ if (next) {
+ next.selected = true;
+ selectedOption.selected = false;
+ }
+ } else if (event.key == "Enter" ||
+ event.key == "Escape") {
+ this.open = false;
+ }
+ }
+
+ _optionsAreEquivalent(a, b) {
+ if (!a || !b) {
+ return false;
+ }
+
+ let aAttrs = a.constructor.observedAttributes;
+ let bAttrs = b.constructor.observedAttributes;
+ if (aAttrs.length != bAttrs.length) {
+ return false;
+ }
+
+ for (let aAttr of aAttrs) {
+ if (a.getAttribute(aAttr) != b.getAttribute(aAttr)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ render() {
+ let popupBox = this.popupBox;
+ if (!popupBox) {
+ popupBox = document.createElement("div");
+ popupBox.classList.add("rich-select-popup-box");
+ this.appendChild(popupBox);
+ }
+
+ /* eslint-disable max-len */
+ let options =
+ this.querySelectorAll(":scope > :not(.rich-select-popup-box):not(.rich-select-selected-clone)");
+ /* eslint-enable max-len */
+ for (let option of options) {
+ popupBox.appendChild(option);
+ }
+
+ let selectedChild;
+ for (let child of popupBox.children) {
+ if (child.selected) {
+ selectedChild = child;
+ }
+ }
+ if (!selectedChild && popupBox.children.length) {
+ selectedChild = popupBox.children[0];
+ selectedChild.selected = true;
+ }
+
+ if (!this._optionsAreEquivalent(this._selectedChild, selectedChild)) {
+ let selectedClone = this.querySelector(":scope > .rich-select-selected-clone");
+ if (selectedClone) {
+ selectedClone.remove();
+ }
+
+ if (selectedChild) {
+ this._selectedChild = selectedChild;
+ selectedClone = selectedChild.cloneNode(false);
+ selectedClone.removeAttribute("id");
+ selectedClone.classList.add("rich-select-selected-clone");
+ selectedClone = this.appendChild(selectedClone);
+ }
+ }
+ }
+}
+
+customElements.define("rich-select", RichSelect);
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -3,25 +3,30 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
<title></title>
<link rel="stylesheet" href="paymentRequest.css"/>
+ <link rel="stylesheet" href="components/rich-select.css"/>
+ <link rel="stylesheet" href="components/address-option.css"/>
<script src="vendor/custom-elements.min.js"></script>
<script src="PaymentsStore.js"></script>
<script src="mixins/ObservedPropertiesMixin.js"></script>
<script src="mixins/PaymentStateSubscriberMixin.js"></script>
<script src="components/currency-amount.js"></script>
+ <script src="components/rich-select.js"></script>
+ <script src="components/rich-option.js"></script>
+ <script src="components/address-option.js"></script>
<script src="containers/payment-dialog.js"></script>
<script src="paymentRequest.js"></script>
<template id="payment-dialog-template">
<div id="host-name"></div>
<div id="total">
@@ -32,11 +37,55 @@
<button id="cancel">Cancel</button>
<button id="pay">Pay</button>
</div>
</template>
</head>
<body>
<iframe id="debugging-console" hidden="hidden" src="debugging.html"></iframe>
+ <rich-select>
+ <address-option email="emzembrano92@example.com"
+ recipient="Emily Zembrano"
+ addressLine="717 Hyde Street #6"
+ city="San Francisco"
+ region="CA"
+ phone="415 203 0845"
+ postalCode="94109"
+ country="USA"></address-option>
+ <address-option email="jenz9382@example.com"
+ recipient="Jennifer Zembrano"
+ addressLine="42 Fairydust Lane"
+ city="Lala Land"
+ region="HI"
+ phone="415 439 2827"
+ postalCode="98765"
+ country="USA"></address-option>
+ <address-option email="johnz9382@example.com"
+ recipient="John Zembrano"
+ addressLine="42 Fairydust Lane"
+ city="Lala Land"
+ missinginformation="true"
+ region="HI"
+ phone="415 439 2827"
+ postalCode="98765"
+ country="USA"></address-option>
+ <address-option email="adbrwodne@example.com"
+ recipient="Andrew Browne"
+ addressLine="42 Fairydust Lane"
+ city="Lala Land"
+ region="HI"
+ phone="517 410 0845"
+ postalCode="98765"
+ country="USA"></address-option>
+ <address-option email="johnz9382@example.com"
+ recipient="Jacob Humphrey"
+ addressLine="1855 Pinecrest Rd"
+ city="East Lansing"
+ region="MI"
+ phone="517 439 2827"
+ postalCode="48823"
+ country="USA"></address-option>
+ </rich-select>
+
<payment-dialog></payment-dialog>
</body>
</html>
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,14 +1,22 @@
[DEFAULT]
support-files =
../../../../../testing/modules/sinon-2.3.2.js
../../res/PaymentsStore.js
../../res/components/currency-amount.js
+ ../../res/components/address-option.js
+ ../../res/components/address-option.css
+ ../../res/components/basic-card-option.js
+ ../../res/components/basic-card-option.css
+ ../../res/components/rich-option.js
+ ../../res/components/rich-select.css
+ ../../res/components/rich-select.js
../../res/mixins/ObservedPropertiesMixin.js
../../res/mixins/PaymentStateSubscriberMixin.js
../../res/vendor/custom-elements.min.js
../../res/vendor/custom-elements.min.js.map
payments_common.js
[test_currency_amount.html]
+[test_rich_select.html]
[test_ObservedPropertiesMixin.html]
[test_PaymentStateSubscriberMixin.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_rich_select.html
@@ -0,0 +1,317 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rich-select component
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test the rich-select component</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="payments_common.js"></script>
+ <script src="custom-elements.min.js"></script>
+ <script src="ObservedPropertiesMixin.js"></script>
+ <script src="rich-select.js"></script>
+ <script src="rich-option.js"></script>
+ <script src="address-option.js"></script>
+ <script src="basic-card-option.js"></script>
+ <link rel="stylesheet" type="text/css" href="rich-select.css"/>
+ <link rel="stylesheet" type="text/css" href="address-option.css"/>
+ <link rel="stylesheet" type="text/css" href="basic-card-option.css"/>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <p id="display">
+ <rich-select id="select1">
+ <address-option id="option1"
+ email="emzembrano92@email.com"
+ recipient="Emily Zembrano"
+ addressLine="717 Hyde Street #6"
+ city="San Francisco"
+ region="CA"
+ phone="415 203 0845"
+ postalCode="94109"
+ country="USA"></address-option>
+ <address-option id="option2"
+ email="jenz9382@email.com"
+ recipient="Jennifer Zembrano"
+ addressLine="42 Fairydust Lane"
+ city="Lala Land"
+ region="HI"
+ phone="415 439 2827"
+ postalCode="98765"
+ country="USA"></address-option>
+ <address-option id="option3"
+ email="johnz9382@email.com"
+ recipient="John Zembrano"
+ addressLine="42 Fairydust Lane"
+ city="Lala Land"
+ missinginformation="true"
+ region="HI"
+ phone="415 439 2827"
+ postalCode="98765"
+ country="USA"></address-option>
+ </rich-select>
+
+ <rich-select id="select2">
+ <basic-card-option owner="Jared Wein"
+ expiration="01/1970"
+ number="4024007197293599"
+ type="Visa"></basic-card-option>
+ <basic-card-option owner="Whimsy Corn"
+ expiration="01/1970"
+ number="5220465104517667"
+ type="Mastercard"></basic-card-option>
+ <basic-card-option owner="Fire Fox"
+ expiration="01/1970"
+ number="6011777095481054"
+ type="Discover"></basic-card-option>
+ </rich-select>
+ </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the rich-select address-option component **/
+
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/components/address-option.js */
+/* import-globals-from ../../res/components/basic-card-option.js */
+
+let select1 = document.getElementById("select1");
+let option1 = document.getElementById("option1");
+let option2 = document.getElementById("option2");
+let option3 = document.getElementById("option3");
+
+function get_selected_clone() {
+ return select1.querySelector(".rich-select-selected-clone");
+}
+
+function is_visible(element, message) {
+ ok(!isHidden(element), message);
+}
+
+function is_hidden(element, message) {
+ ok(isHidden(element), message);
+}
+
+function dispatchKeyPress(key, keyCode) {
+ select1.dispatchEvent(new KeyboardEvent("keypress", {key, keyCode}));
+}
+
+add_task(async function test_addressLine_combines_address_city_region_postalCode_country() {
+ ok(option1, "option1 exists");
+ let addressLine = option1.querySelector(".addressLine");
+ /* eslint-disable max-len */
+ is(addressLine.textContent,
+ `${option1.addressLine} ${option1.city} ${option1.region} ${option1.postalCode} ${option1.country}`);
+ /* eslint-enable max-len */
+});
+
+add_task(async function test_no_option_selected_first_displayed() {
+ ok(select1, "select1 exists");
+
+ await asyncElementRendered();
+
+ is_hidden(option1, "option 1 should be hidden when popup is not open");
+ is_hidden(option2, "option 2 should be hidden when popup is not open");
+ is_hidden(option3, "option 3 should be hidden when popup is not open");
+ ok(option1.selected, "option 1 should be selected");
+ ok(option1.hasAttribute("selected"), "option 1 should have selected attribute");
+ let selectedClone = get_selected_clone();
+ is_visible(selectedClone, "The selected clone should be visible at all times");
+ is(selectedClone.getAttribute("email"), option1.getAttribute("email"),
+ "The selected clone email should be equivalent to the selected option 1");
+ is(selectedClone.getAttribute("recipient"), option1.getAttribute("recipient"),
+ "The selected clone recipient should be equivalent to the selected option 1");
+});
+
+add_task(async function test_clicking_on_select_shows_all_options() {
+ ok(select1, "select1 exists");
+ ok(!select1.open, "select is not open by default");
+ ok(option1.selected, "option 1 should be selected by default");
+
+ select1.click();
+
+ ok(select1.open, "select is open after clicking on it");
+ ok(option1.selected, "option 1 should be selected when open");
+ is_visible(option1, "option 1 is visible when select is open");
+ is_visible(option2, "option 2 is visible when select is open");
+ is_visible(option3, "option 3 is visible when select is open");
+
+ option2.click();
+
+ ok(!select1.open, "select is not open after blur");
+ ok(!option1.selected, "option 1 is not selected after click on option 2");
+ ok(option2.selected, "option 2 is selected after clicking on it");
+ is_hidden(option1, "option 1 is hidden when select is closed");
+ is_hidden(option2, "option 2 is hidden when select is closed");
+ is_hidden(option3, "option 3 is hidden when select is closed");
+
+ await asyncElementRendered();
+
+ let selectedClone = get_selected_clone();
+ is_visible(selectedClone, "The selected clone should be visible at all times");
+ is(selectedClone.getAttribute("email"), option2.getAttribute("email"),
+ "The selected clone email should be equivalent to the selected option 2");
+ is(selectedClone.getAttribute("recipient"), option2.getAttribute("recipient"),
+ "The selected clone recipient should be equivalent to the selected option 2");
+});
+
+add_task(async function test_changing_option_selected_affects_other_options() {
+ ok(option2.selected, "Option 2 should be selected from prior test");
+
+ option1.selected = true;
+ ok(!option2.selected, "Option 2 should no longer be selected after making option 1 selected");
+ ok(option1.hasAttribute("selected"), "Option 1 should now have selected attribute");
+});
+
+add_task(async function test_up_down_keys_change_selected_item() {
+ let openObserver = new MutationObserver(mutations => {
+ for (let mutation of mutations) {
+ ok(mutation.attributeName != "open", "the select should not open/close during this test");
+ }
+ });
+ openObserver.observe(select1, {attributes: true});
+
+ ok(select1, "select1 exists");
+ ok(option1.selected, "option 1 should be selected by default");
+
+ ok(!select1.open, "select should not be open before focusing");
+ select1.focus();
+ ok(!select1.open, "select should not be open after focusing");
+
+ dispatchKeyPress("ArrowDown", 40);
+ ok(!option1.selected, "option 1 should no longer be selected");
+ ok(option2.selected, "option 2 should now be selected");
+
+ dispatchKeyPress("ArrowDown", 40);
+ ok(!option2.selected, "option 2 should no longer be selected");
+ ok(option3.selected, "option 3 should now be selected");
+
+ dispatchKeyPress("ArrowDown", 40);
+ ok(option3.selected, "option 3 should remain selected");
+ ok(!option1.selected, "option 1 should not be selected");
+
+ dispatchKeyPress("ArrowUp", 38);
+ ok(!option3.selected, "option 3 should no longer be selected");
+ ok(option2.selected, "option 2 should now be selected");
+
+ dispatchKeyPress("ArrowUp", 38);
+ ok(!option2.selected, "option 2 should no longer be selected");
+ ok(option1.selected, "option 1 should now be selected");
+
+ dispatchKeyPress("ArrowUp", 38);
+ ok(option1.selected, "option 1 should remain selected");
+ ok(!option3.selected, "option 3 should not be selected");
+
+ // Wait for any mutation observer notifications to fire before exiting.
+ await Promise.resolve();
+
+ openObserver.disconnect();
+});
+
+add_task(async function test_open_close_from_keyboard() {
+ select1.focus();
+
+ ok(!select1.open, "select should not be open by default");
+
+ dispatchKeyPress(" ", 32);
+ ok(select1.open, "select should now be open");
+ ok(option1.selected, "option 1 should be selected by default");
+
+ dispatchKeyPress("ArrowDown", 40);
+ ok(!option1.selected, "option 1 should not be selected");
+ ok(option2.selected, "option 2 should now be selected");
+ ok(select1.open, "select should remain open");
+
+ dispatchKeyPress("ArrowUp", 38);
+ ok(option1.selected, "option 1 should now be selected");
+ ok(!option2.selected, "option 2 should not be selected");
+ ok(select1.open, "select should remain open");
+
+ dispatchKeyPress("Enter", 13);
+ ok(option1.selected, "option 1 should now be selected");
+ ok(!select1.open, "select should be closed");
+
+ dispatchKeyPress(" ", 32);
+ ok(select1.open, "select should now be open");
+
+ dispatchKeyPress("Escape", 27);
+ ok(!select1.open, "select should be closed");
+});
+
+add_task(async function test_clicking_on_options_maintain_one_item_always_selected() {
+ ok(!select1.open, "select should be closed by default");
+ ok(option1.selected, "option 1 should be selected by default");
+ select1.click();
+ ok(select1.open, "select should now be open");
+
+ option3.click();
+ ok(!select1.open, "select should be closed");
+ ok(!option1.selected, "option 1 should be unselected");
+ ok(option3.selected, "option 3 should be selected");
+
+ select1.click();
+ ok(select1.open, "select should open");
+ ok(!option1.selected, "option 1 should be unselected");
+ ok(option3.selected, "option 3 should be selected");
+
+ option1.click();
+ ok(!select1.open, "select should be closed");
+ ok(option1.selected, "option 1 should be selected");
+ ok(!option3.selected, "option 3 should be unselected");
+});
+
+add_task(async function test_selected_clone_should_equal_selected_option() {
+ ok(option1.selected, "option 1 should be selected");
+ await asyncElementRendered();
+
+ let clonedOptions = select1.querySelectorAll(".rich-select-selected-clone");
+ is(clonedOptions.length, 1, "there should only be one cloned option");
+
+ let clonedOption = clonedOptions[0];
+ for (let attrName of AddressOption.observedAttributes) {
+ is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+ option1.attributes[attrName] && option1.attributes[attrName].value,
+ "attributes should have matching value; name=" + attrName);
+ }
+
+ option2.selected = true;
+ await asyncElementRendered();
+
+ clonedOptions = select1.querySelectorAll(".rich-select-selected-clone");
+ is(clonedOptions.length, 1, "there should only be one cloned option");
+
+ clonedOption = clonedOptions[0];
+ for (let attrName of AddressOption.observedAttributes) {
+ is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+ option2.attributes[attrName] && option2.attributes[attrName].value,
+ "attributes should have matching value; name=" + attrName);
+ }
+});
+
+add_task(async function test_basic_card_simple() {
+ let select2 = document.getElementById("select2");
+ ok(select2, "basic card select should exist");
+ let selectPopupBox = select2.querySelector(".rich-select-popup-box");
+ ok(selectPopupBox, "basic card popup box exists");
+
+ is(selectPopupBox.childElementCount, 3, "There should be three children in the popup box");
+
+ let clonedOption = select2.querySelector(".rich-select-selected-clone");
+ let selectedOption = selectPopupBox.firstChild;
+ for (let attrName of BasicCardOption.observedAttributes) {
+ is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+ selectedOption.attributes[attrName] && selectedOption.attributes[attrName].value,
+ "attributes should have matching value; name=" + attrName);
+ }
+});
+
+</script>
+
+</body>
+</html>