Bug 1926706 - Loading state in account hub. r=aleca

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

--HG--
extra : amend_source : d668ead45c02ccdd2c99c614aee8715a004c4bf2
This commit is contained in:
Martin Giger 2024-11-20 08:34:47 +02:00
Родитель 41ae1dd1ea
Коммит ec2809549f
7 изменённых файлов: 235 добавлений и 27 удалений

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

@ -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);

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

@ -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);

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

@ -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);

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

@ -105,4 +105,10 @@
</account-hub-step>
<account-hub-footer id="emailFooter" class="hub-footer"></account-hub-footer>
<div id="loadingOverlay">
<div class="loader-outside">
<div class="loader-inside"></div>
</div>
</div>
</html:template>

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

@ -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();

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

@ -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

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

@ -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 */