diff --git a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs index 7f7bc6bd2b..24c15074b1 100644 --- a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs +++ b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs @@ -50,15 +50,14 @@ class AccountHubFooter extends HTMLElement { } toggleForwardDisabled(value) { - this.querySelector("#forward").disabled = value; + this.querySelector("#forward").disabled = value || this.disabled; } canCustom(value) { const customAction = this.querySelector("#custom"); customAction.hidden = !value; - customAction.disabled = !value; + customAction.disabled = !value || this.disabled; if (value) { - customAction.disabled = false; customAction.addEventListener("click", this); document.l10n.setAttributes(customAction, value); } @@ -86,6 +85,19 @@ class AccountHubFooter extends HTMLElement { relNotesLink.href = relNotesURL; relNotesLink.closest("li[hidden]").hidden = false; } + + get disabled() { + return this.querySelector("#back").disabled; + } + + set disabled(val) { + this.toggleForwardDisabled(val); + this.querySelector("#back").disabled = val; + const customAction = this.querySelector("#custom"); + if (!customAction.hidden) { + customAction.disabled = val; + } + } } customElements.define("account-hub-footer", AccountHubFooter); diff --git a/mail/components/accountcreation/content/widgets/email-auto-form.mjs b/mail/components/accountcreation/content/widgets/email-auto-form.mjs index 8af5c345b2..2b0d2c0baf 100644 --- a/mail/components/accountcreation/content/widgets/email-auto-form.mjs +++ b/mail/components/accountcreation/content/widgets/email-auto-form.mjs @@ -116,6 +116,12 @@ class EmailAutoForm extends AccountHubStep { captureState() { return this.#currentConfig; } + + set disabled(val) { + for (const input of this.querySelectorAll("input")) { + input.disabled = val; + } + } } customElements.define("email-auto-form", EmailAutoForm); diff --git a/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs b/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs index 3e151b3ca3..4fba6f9ec7 100644 --- a/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs +++ b/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs @@ -341,6 +341,16 @@ class EmailOutgoingForm extends AccountHubStep { this.#adjustOAuth2Visibility(config); } + + set disabled(val) { + this.#outgoingPort.disabled = val; + this.#outgoingUsername.disabled = + val || this.#outgoingAuthenticationMethod == 1; + this.#outgoingHostname.disabled = val; + this.#outgoingConnectionSecurity.disabled = val; + this.#outgoingAuthenticationMethod.disabled = val; + this.querySelector("#advancedConfigurationOutgoing").disabled = val; + } } customElements.define("email-manual-outgoing-form", EmailOutgoingForm); diff --git a/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml b/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml index 3116d0c64f..e36d9d38cb 100644 --- a/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml +++ b/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml @@ -105,4 +105,10 @@ + +
+
+
+
+
diff --git a/mail/components/accountcreation/views/email.mjs b/mail/components/accountcreation/views/email.mjs index fb99b18042..9b23513039 100644 --- a/mail/components/accountcreation/views/email.mjs +++ b/mail/components/accountcreation/views/email.mjs @@ -289,6 +289,7 @@ class AccountHubEmail extends HTMLElement { * @param {string} subview - Subview for which the UI is being inititialized. */ async #initUI(subview) { + this.#stopLoading(); this.#hideSubviews(); this.#clearNotifications(); this.#currentState = subview; @@ -352,6 +353,48 @@ class AccountHubEmail extends HTMLElement { } } + #loadingTimeout = null; + + /** + * Show a loading notification and disable all inputs (except closing the + * dialog). If the load takes too long, a spinner is overlaid. + * + * TODO: should be able to cancel some loads, if they're abortable. + * + * @param {string} loadingFluentId + */ + #startLoading(loadingFluentId) { + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: loadingFluentId, + type: "info", + }); + this.classList.add("busy"); + this.#states[this.#currentState].subview.disabled = true; + this.#emailFooter.disabled = true; + this.#loadingTimeout = setTimeout(() => { + this.classList.add("spinner"); + this.#loadingTimeout = null; + }, 3000); + } + + /** + * Stop loading, clearing the notification, restoring form controls and hiding + * the spinner if it was visible. + */ + #stopLoading() { + if (!this.classList.contains("busy")) { + return; + } + this.#clearNotifications(); + this.#states[this.#currentState].subview.disabled = false; + this.#emailFooter.disabled = false; + this.classList.remove("busy", "spinner"); + if (this.#loadingTimeout) { + clearTimeout(this.#loadingTimeout); + this.#loadingTimeout = null; + } + } + /** * Handle the events from the subviews. * @@ -393,7 +436,11 @@ class AccountHubEmail extends HTMLElement { this.#states[this.#currentState].subview.setState(config); } catch (error) { this.#handleAbortable(); - stateDetails.subview.showErrorNotification(error.title, error.text); + stateDetails.subview.showNotification({ + title: error.title || error.message, + description: error.text, + type: "error", + }); } break; case "custom-footer-action": @@ -487,6 +534,7 @@ class AccountHubEmail extends HTMLElement { async #handleForwardAction(currentState, stateData) { switch (currentState) { case "autoConfigSubview": + this.#startLoading("account-hub-lookup-email-configuration-title"); try { this.#emailFooter.canBack(true); this.#email = stateData.email; @@ -501,25 +549,32 @@ class AccountHubEmail extends HTMLElement { this.#currentConfig = this.#fillAccountConfig( this.#getEmptyAccountConfig() ); + this.#stopLoading(); await this.#initUI("incomingConfigSubview"); this.#states[this.#currentState].previousStep = "autoConfigSubview"; - } else { - this.#hasCancelled = false; + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-find-settings-failed", + type: "warning", + }); break; } - } else { - this.#currentConfig = this.#fillAccountConfig(config); - await this.#initUI(this.#states[this.#currentState].nextStep); - this.#states.incomingConfigSubview.previousStep = - "emailConfigFoundSubview"; - this.#states[this.#currentState].subview.showNotification({ - fluentTitleId: "account-hub-config-success", - type: "success", - }); + this.#hasCancelled = false; + this.#stopLoading(); + break; } + this.#currentConfig = this.#fillAccountConfig(config); + this.#stopLoading(); + await this.#initUI(this.#states[this.#currentState].nextStep); + this.#states.incomingConfigSubview.previousStep = + "emailConfigFoundSubview"; + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-config-success", + type: "success", + }); } catch (error) { this.#emailFooter.canBack(false); + this.#stopLoading(); if (!(error instanceof UserCancelledException)) { // TODO: Throw proper error here; throw error; @@ -581,11 +636,11 @@ class AccountHubEmail extends HTMLElement { case "incomingConfigSubview": break; case "outgoingConfigSubview": + this.#startLoading("account-hub-adding-account-subheader"); stateData = this.#states[this.#currentState].subview.captureState(); stateData.incoming = this.#states.incomingConfigSubview.subview.captureState().config.incoming; stateData = this.#fillAccountConfig(stateData); - try { const config = await this.#guessConfig( this.#email.split("@")[1], @@ -593,17 +648,32 @@ class AccountHubEmail extends HTMLElement { ); if (config.isComplete()) { - // TODO: Show success message here. + this.#stopLoading(); + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-config-test-scucess", + type: "success", + }); this.#emailFooter.toggleForwardDisabled(false); } else { + this.#stopLoading(); // The config is not complete, go back to the incoming view and // show an error. this.#initUI(this.#states[this.#currentState].previousStep); // TODO: Show error message here. + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-find-settings-failed", + type: "error", + }); } } catch (error) { + this.#stopLoading(); this.#initUI(this.#states[this.#currentState].previousStep); // TODO: Show error message here. + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-find-settings-failed", + error, + type: "error", + }); } break; case "emailAddedSubview": @@ -705,9 +775,9 @@ class AccountHubEmail extends HTMLElement { gAccountSetupLogger.warn(`guessConfig failed: ${e}`); reject(e); - this.showNotification({ - title: "account-hub-find-settings-failed", - e, + this.#states[this.#currentState].subview.showNotification({ + fluentTitleId: "account-hub-find-settings-failed", + error: e, type: "error", }); this.abortable = null; @@ -939,6 +1009,7 @@ class AccountHubEmail extends HTMLElement { return false; } + this.#stopLoading(); this.#currentState = "autoConfigSubview"; this.#currentConfig = {}; this.#hideSubviews(); diff --git a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl index 7bf292071b..fdcf61e2af 100644 --- a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl +++ b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl @@ -173,3 +173,5 @@ account-hub-password-info = Your credentials will only be stored locally on your account-hub-sync-success = Thunderbird found some connected services account-hub-email-added-success = Email account connected successfully + +account-hub-config-test-scucess = Configuration settings valid diff --git a/mail/themes/shared/mail/accountHub.css b/mail/themes/shared/mail/accountHub.css index 1b5306bdbc..f5303b90ee 100644 --- a/mail/themes/shared/mail/accountHub.css +++ b/mail/themes/shared/mail/accountHub.css @@ -35,6 +35,8 @@ dialog { --hub-account-footer-link-color: var(--color-primary-default); --hub-border-color: light-dark(var(--color-primary-soft), var(--color-primary-default)); --hub-divider-color: var(--color-neutral-base); + --hub-loader-background: var(--color-neutral-base); + --hub-loader-color: var(--color-primary-default); --hub-box-shadow: 0 2px 4px rgba(58, 57, 68, 0.3); --hub-input-height: 33px; @@ -64,21 +66,20 @@ dialog { .account-hub-dialog { display: grid; width: 800px; - height: 600px; + min-height: 600px; overflow: initial; padding: 0; box-shadow: none; &::after { + --hub-blur-radius: 15px; content: ''; position: absolute; - top: 17%; - left: 50%; - width: 765px; - height: 500px; - transform: translateX(-50%); + inset-block-end: -2px; + inset-inline: calc(2 * var(--hub-blur-radius) + 5px); + min-height: calc(3 * var(--hub-blur-radius)); background: linear-gradient(to right, rgba(159, 244, 240, 1), rgba(76, 177, 249, 1), rgba(168, 85, 247, 1)); - filter: blur(15px); + filter: blur(var(--hub-blur-radius)); z-index: -1; } @@ -118,6 +119,106 @@ dialog { @media (prefers-color-scheme: dark) { background-image: url("chrome://messenger/skin/images/accounthub-bg-dark.webp"); } + + &.busy { + cursor: wait; + } + + #loadingOverlay { + display: none; + opacity: 0; + } + + &.spinner #loadingOverlay { + display: grid; + position: absolute; + inset: 0; + place-items: center; + background-color: var(--hub-loader-background); + opacity: 0.8; + z-index: 2; + border-radius: inherit; + + @media (prefers-reduced-motion: no-preference) { + animation: 0.5s linear 0s hub-reveal-loader; + } + } +} + +@keyframes hub-reveal-loader { + 0% { + display: none; + opacity: 0; + } + + 1% { + display: grid; + opacity: 0; + } + + 100% { + opacity: 0.8; + } +} + +@keyframes hub-loader-loading { + from { + rotate: 0deg; + } + to { + rotate: 360deg; + } +} + +.loader-outside { + --hub-loader-width: 8px; + --hub-loader-size: 64px; + --hub-trail-offset: 360deg; + + position: relative; + height: var(--hub-loader-size); + aspect-ratio: 1; + background-image: conic-gradient( + from 0deg, + transparent 0deg, + var(--hub-loader-color) 360deg, + transparent var(--hub-trail-offset) + ); + border-radius: 50%; + pointer-events: none; + + + @media (prefers-reduced-motion: no-preference) { + animation: 1.1s cubic-bezier(0.61, 0.12, 0, 0.99) 0s infinite hub-loader-loading; + } + + &::before { + content: ''; + position: absolute; + inset: var(--hub-loader-width); + height: calc(var(--hub-loader-size) - 2 * var(--hub-loader-width)); + aspect-ratio: 1; + background: var(--hub-loader-background); + border-radius: 50%; + } + + .loader-inside { + position: absolute; + inset: 0; + aspect-ratio: 1; + border-radius: 50%; + + &::before { + content: ''; + position: absolute; + height: var(--hub-loader-width); + aspect-ratio: 1; + border-radius: 50%; + background: var(--hub-loader-color); + inset-block-start: 0; + inset-inline: calc(50% - var(--hub-loader-width) / 2); + } + } } /* Typography */