@@ -69,7 +69,6 @@ export class SignInStep extends BaseFormStep {
this.#formEl.addEventListener("submit", this);
this.#emailEl.addEventListener("blur", this);
this.#emailEl.addEventListener("input", this);
- this.#emailEl.focus();
}
disconnectedCallback() {
@@ -169,6 +168,18 @@ export class SignInStep extends BaseFormStep {
}
}
+ // If we've reached the point where we've got hasUpdatedFxAState set true,
+ // this means that we've confirmed that we're running on a non-disqualified
+ // version of Firefox Desktop. If focus hasn't moved anywhere else in the
+ // meantime, we'll move focus to the email field if it's never been moved
+ // there before.
+ if (this.state.hasUpdatedFxAState && this.#emailEl.hasAttribute("pending-autofocus")) {
+ this.#emailEl.removeAttribute("pending-autofocus");
+ if (document.activeElement == document.body) {
+ this.#emailEl.focus();
+ }
+ }
+
let alternativeLinks = this.shadowRoot.querySelectorAll(".alternative-link");
for (let link of alternativeLinks) {
link.href = this.state.linkHref;
diff --git a/kitsune/sumo/static/sumo/js/form-wizard.js b/kitsune/sumo/static/sumo/js/form-wizard.js
index 341e4b07e13..ed060e9487e 100644
--- a/kitsune/sumo/static/sumo/js/form-wizard.js
+++ b/kitsune/sumo/static/sumo/js/form-wizard.js
@@ -55,7 +55,7 @@ export class FormWizard extends HTMLElement {
static get markup() {
return `
-
+
@@ -117,6 +117,17 @@ export class FormWizard extends HTMLElement {
// If there's no active step, default to the first step.
this.activeStep = this.firstElementChild?.getAttribute("name");
}
+
+ // We set up our aria attributes here instead of in
+ // hook_device_migration_wizard.html where the is put into
+ // the markup of the page because that hook is only ever rendered when the
+ // SUMO article embedding the wizard is updated. By adding the attributes here,
+ // we can ensure that the attributes will apply, even for instances of SUMO
+ // kb articles embedding the form-wizard that might not get updated in a while.
+ let root = this.shadowRoot.querySelector("#form-wizard-root");
+ root.setAttribute("role", "dialog");
+ root.setAttribute("aria-label", gettext("Backup assistant"));
+ this.setAttribute("aria-describedby", "form-wizard-root");
}
get activeStep() {
@@ -208,7 +219,7 @@ export class FormWizard extends HTMLElement {
child.toggleAttribute("active", child.getAttribute("reason") === reason);
}
- let root = this.shadowRoot.querySelector(".form-wizard-root");
+ let root = this.shadowRoot.querySelector("#form-wizard-root");
root.toggleAttribute("inert", true);
}
diff --git a/kitsune/sumo/static/sumo/js/switching-devices-wizard-manager.js b/kitsune/sumo/static/sumo/js/switching-devices-wizard-manager.js
index c14de4a0b29..4ecf3d0d2d8 100644
--- a/kitsune/sumo/static/sumo/js/switching-devices-wizard-manager.js
+++ b/kitsune/sumo/static/sumo/js/switching-devices-wizard-manager.js
@@ -62,6 +62,7 @@ export default class SwitchingDevicesWizardManager {
sumoEmail: "",
syncEnabled: false,
confirmedSyncChoices: false,
+ hasUpdatedFxAState: false,
};
#state = Object.assign({}, this.#defaultState);
@@ -127,6 +128,7 @@ export default class SwitchingDevicesWizardManager {
return {
fxaRoot: state.fxaRoot,
email: state.sumoEmail,
+ hasUpdatedFxAState: state.hasUpdatedFxAState,
linkHref: `${state.fxaRoot}?${linkParams}`,
...baseParams,
@@ -425,6 +427,10 @@ export default class SwitchingDevicesWizardManager {
UITour.getConfiguration("fxa", resolve);
});
+ this.#updateState({
+ hasUpdatedFxAState: true,
+ });
+
if (fxaConfig.setup && fxaConfig.accountStateOK) {
let syncEnabled = !!fxaConfig.browserServices?.sync?.setup;
// If the user had already confirmed their sync choices, but then sync
diff --git a/kitsune/sumo/static/sumo/js/tests/form-wizard-sign-in-step-tests.js b/kitsune/sumo/static/sumo/js/tests/form-wizard-sign-in-step-tests.js
index 9d561e7fc39..2f68a356687 100644
--- a/kitsune/sumo/static/sumo/js/tests/form-wizard-sign-in-step-tests.js
+++ b/kitsune/sumo/static/sumo/js/tests/form-wizard-sign-in-step-tests.js
@@ -92,9 +92,59 @@ describe("sign-in-step custom element", () => {
assertFormElements(form, EXPECTED_FORM_ELEMENTS_WITH_FLOW_METRICS);
});
- it("should focus the email field automatically", () => {
+ it("should focus the email field automatically after the hasUpdatedFxAState property is set to true", () => {
let email = step.shadowRoot.querySelector("#email");
+ expect(step.shadowRoot.activeElement).to.not.equal(email);
+ expect(email.hasAttribute("pending-autofocus")).to.be.true;
+
+ const TEST_STATE = {
+ utm_source: "utm_source",
+ utm_campaign: "utm_campaign",
+ utm_medium: "utm_medium",
+ entrypoint: "entrypoint",
+ entrypoint_experiment: "entrypoint_experiment",
+ entrypoint_variation: "entrypoint_variation",
+ context: "context",
+ redirect_to: window.location.href,
+ redirect_immediately: true,
+ service: "sync",
+ action: "email",
+ hasUpdatedFxAState: true,
+ };
+ step.setState(TEST_STATE);
+
expect(step.shadowRoot.activeElement).to.equal(email);
+ expect(email.hasAttribute("pending-autofocus")).to.be.false;
+ });
+
+ it("should not focus the email field automatically if focus moved after the hasUpdatedFxAState property is set to true", () => {
+ let email = step.shadowRoot.querySelector("#email");
+ expect(step.shadowRoot.activeElement).to.not.equal(email);
+ expect(email.hasAttribute("pending-autofocus")).to.be.true;
+
+ let someButton = document.createElement("button");
+ someButton.textContent = "I'm focused first.";
+ document.body.append(someButton);
+ someButton.focus();
+
+ const TEST_STATE = {
+ utm_source: "utm_source",
+ utm_campaign: "utm_campaign",
+ utm_medium: "utm_medium",
+ entrypoint: "entrypoint",
+ entrypoint_experiment: "entrypoint_experiment",
+ entrypoint_variation: "entrypoint_variation",
+ context: "context",
+ redirect_to: window.location.href,
+ redirect_immediately: true,
+ service: "sync",
+ action: "email",
+ hasUpdatedFxAState: true,
+ };
+ step.setState(TEST_STATE);
+
+ expect(step.shadowRoot.activeElement).to.not.equal(email);
+ expect(email.hasAttribute("pending-autofocus")).to.be.false;
});
it("should set an email address if one is set in the state", () => {
diff --git a/kitsune/sumo/static/sumo/js/tests/form-wizard-tests.js b/kitsune/sumo/static/sumo/js/tests/form-wizard-tests.js
index 72046817103..a6652c0ec7f 100644
--- a/kitsune/sumo/static/sumo/js/tests/form-wizard-tests.js
+++ b/kitsune/sumo/static/sumo/js/tests/form-wizard-tests.js
@@ -251,7 +251,7 @@ describe("form-wizard custom element", () => {
it("should put the root into the disqualified state and populate the header/message", () => {
wizard.disqualify("need-fx-desktop");
- let root = wizard.shadowRoot.querySelector(".form-wizard-root");
+ let root = wizard.shadowRoot.querySelector("#form-wizard-root");
expect(root.hasAttribute("inert")).to.be.true;
let needFxDesktop = wizard.shadowRoot.querySelector(
diff --git a/kitsune/sumo/static/sumo/js/tests/switching-devices-wizard-manager-tests.js b/kitsune/sumo/static/sumo/js/tests/switching-devices-wizard-manager-tests.js
index 4d93f7790c4..fe68cc1211f 100644
--- a/kitsune/sumo/static/sumo/js/tests/switching-devices-wizard-manager-tests.js
+++ b/kitsune/sumo/static/sumo/js/tests/switching-devices-wizard-manager-tests.js
@@ -386,6 +386,7 @@ describe("k", () => {
context: "fx_desktop_v3",
redirect_to: window.location.href,
redirect_immediately: true,
+ hasUpdatedFxAState: true,
});
});
@@ -418,6 +419,7 @@ describe("k", () => {
sumoEmail: "test@example.com",
syncEnabled: false,
confirmedSyncChoices: false,
+ hasUpdatedFxAState: true,
};
const EXPECTED_PAYLOAD = {
service: "sync",
@@ -436,6 +438,7 @@ describe("k", () => {
redirect_to: window.location.href,
redirect_immediately: true,
linkHref: `${FAKE_FXA_ROOT}?service=sync&action=email&utm_source=support.mozilla.org&utm_campaign=migration&utm_medium=mozilla-websites&entrypoint=fx-new-device-sync&entrypoint_experiment=experiment&entrypoint_variation=variation&flow_id=${FAKE_FXA_FLOW_ID}&flow_begin_time=${FAKE_FXA_FLOW_BEGIN_TIME}&context=fx_desktop_v3&redirect_to=https%3A%2F%2Fexample.com%2F%23search&redirect_immediately=true`,
+ hasUpdatedFxAState: true,
};
expect(step.enter(TEST_STATE)).to.deep.equal(EXPECTED_PAYLOAD);
});
@@ -443,7 +446,9 @@ describe("k", () => {
it("should send the user to the configure-sync step immediately if UITour says that the user is signed in", async () => {
let setStepCalled = new Promise((resolve) => {
gSetStepStub.callsFake((name, payload) => {
- resolve({ name, payload });
+ if (name == "configure-sync") {
+ resolve({ name, payload });
+ }
});
});
diff --git a/kitsune/sumo/static/sumo/scss/form-wizard.styles.scss b/kitsune/sumo/static/sumo/scss/form-wizard.styles.scss
index 85c563dbd88..fa467b4302e 100644
--- a/kitsune/sumo/static/sumo/scss/form-wizard.styles.scss
+++ b/kitsune/sumo/static/sumo/scss/form-wizard.styles.scss
@@ -12,7 +12,7 @@
box-shadow: 0px 2px 6px rgba(58, 57, 68, 0.2);
}
-.form-wizard-root {
+#form-wizard-root {
display: flex;
flex-direction: column;
position: relative;
@@ -20,7 +20,7 @@
:host([disqualified-mobile]) .form-wizard-content,
:host([disqualified-mobile]) #progress,
-.form-wizard-root:not([inert]) > .form-wizard-disqualified {
+#form-wizard-root:not([inert]) > .form-wizard-disqualified {
display: none;
}