From c401d6411b3f494d4202b6e06ba2b0682ab84444 Mon Sep 17 00:00:00 2001
From: Martin Brennan <mjrbrennan@gmail.com>
Date: Fri, 3 Sep 2021 13:04:24 +1000
Subject: [PATCH] A11Y: Improve create account modal for screen readers
 (#14234)

Improves the create account modal for screen readers by doing the following:

* Making the `modal-alert` section into an `aria-role="alert"` region and making it show and hide using height instead of display:none so screen readers pick it up. Made a change so the field-related error messages are always shown beneath the field.
* Add `aria-invalid` and `aria-describedby` attributes to each field in the modal, so the screen reader will read out the error hint on error. This necessitated an Ember component extension to allow both the `aria-*` attributes to be bound and to render on `{{input}}`.
* Moved the social login buttons to the right in the HTML structure so they are not read out first.
* Added `aria-label` attributes to the login buttons so they can have different content for screen readers.
* In some cases for modals, the title that should be used for the `aria-labelledby` attribute is within the modal content and not the discourse-modal-title title. This introduces a new titleAriaElementId property to the d-modal component that is then used by the create-account modal to read out the title

------

This is the same as e0d2de73d89cdea13e9681b2daaa52074ee510a5 but
fixes the Ember-input-component-extension to use the public
Ember components TextField and TextArea instead of the private
TextSupport so the extension works in both normal Ember and
Ember CLI.
---
 .../discourse/app/components/d-modal-body.js  | 25 ++++--
 .../discourse/app/components/d-modal.js       | 18 ++--
 .../app/controllers/create-account.js         | 30 ++++---
 .../discourse/app/controllers/login.js        |  5 +-
 .../ember-input-component-extension.js        | 15 ++++
 .../discourse/app/lib/show-modal.js           |  4 +
 .../discourse/app/mixins/name-validation.js   |  8 +-
 .../app/mixins/password-validation.js         | 10 ++-
 .../app/mixins/username-validation.js         |  8 +-
 .../discourse/app/models/login-method.js      |  5 ++
 .../discourse/app/routes/application.js       | 13 ++-
 .../app/templates/components/d-modal.hbs      |  2 +-
 .../templates/components/login-buttons.hbs    |  2 +-
 .../discourse/app/templates/modal.hbs         |  1 +
 .../app/templates/modal/create-account.hbs    | 82 +++++++++++++------
 .../create-account-user-fields-test.js        |  8 +-
 app/assets/stylesheets/common/base/login.scss |  5 +-
 config/locales/client.en.yml                  |  6 ++
 18 files changed, 178 insertions(+), 69 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/app/initializers/ember-input-component-extension.js

diff --git a/app/assets/javascripts/discourse/app/components/d-modal-body.js b/app/assets/javascripts/discourse/app/components/d-modal-body.js
index 2ea7d11324d..1e46c4442a8 100644
--- a/app/assets/javascripts/discourse/app/components/d-modal-body.js
+++ b/app/assets/javascripts/discourse/app/components/d-modal-body.js
@@ -8,7 +8,10 @@ export default Component.extend({
 
   didInsertElement() {
     this._super(...arguments);
-    $("#modal-alert").hide();
+    this._modalAlertElement = document.getElementById("modal-alert");
+    if (this._modalAlertElement) {
+      this._modalAlertElement.innerHTML = "";
+    }
 
     let fixedParent = $(this.element).closest(".d-modal.fixed-modal");
     if (fixedParent.length) {
@@ -55,10 +58,10 @@ export default Component.extend({
   },
 
   _clearFlash() {
-    const modalAlert = document.getElementById("modal-alert");
-    if (modalAlert) {
-      modalAlert.style.display = "none";
-      modalAlert.classList.remove(
+    if (this._modalAlertElement) {
+      this._modalAlertElement.innerHTML = "";
+      this._modalAlertElement.classList.remove(
+        "alert",
         "alert-error",
         "alert-info",
         "alert-success",
@@ -69,10 +72,14 @@ export default Component.extend({
 
   _flash(msg) {
     this._clearFlash();
+    if (!this._modalAlertElement) {
+      return;
+    }
 
-    $("#modal-alert")
-      .addClass(`alert alert-${msg.messageClass || "success"}`)
-      .html(msg.text || "")
-      .fadeIn();
+    this._modalAlertElement.classList.add(
+      "alert",
+      `alert-${msg.messageClass || "success"}`
+    );
+    this._modalAlertElement.innerHTML = msg.text || "";
   },
 });
diff --git a/app/assets/javascripts/discourse/app/components/d-modal.js b/app/assets/javascripts/discourse/app/components/d-modal.js
index a08ec543e95..941a0ca556e 100644
--- a/app/assets/javascripts/discourse/app/components/d-modal.js
+++ b/app/assets/javascripts/discourse/app/components/d-modal.js
@@ -1,8 +1,7 @@
-import { computed } from "@ember/object";
 import Component from "@ember/component";
 import I18n from "I18n";
 import { next, schedule } from "@ember/runloop";
-import { bind, on } from "discourse-common/utils/decorators";
+import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
 
 export default Component.extend({
   classNameBindings: [
@@ -21,6 +20,7 @@ export default Component.extend({
   submitOnEnter: true,
   dismissable: true,
   title: null,
+  titleAriaElementId: null,
   subtitle: null,
   role: "dialog",
   headerClass: null,
@@ -41,9 +41,17 @@ export default Component.extend({
   // Inform screenreaders of the modal
   "aria-modal": "true",
 
-  ariaLabelledby: computed("title", function () {
-    return this.title ? "discourse-modal-title" : null;
-  }),
+  @discourseComputed("title", "titleAriaElementId")
+  ariaLabelledby(title, titleAriaElementId) {
+    if (titleAriaElementId) {
+      return titleAriaElementId;
+    }
+    if (title) {
+      return "discourse-modal-title";
+    }
+
+    return;
+  },
 
   @on("didInsertElement")
   setUp() {
diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js
index 52a3f7230a7..b58957450dc 100644
--- a/app/assets/javascripts/discourse/app/controllers/create-account.js
+++ b/app/assets/javascripts/discourse/app/controllers/create-account.js
@@ -140,16 +140,19 @@ export default Controller.extend(
       "serverAccountEmail",
       "serverEmailValidation",
       "accountEmail",
-      "rejectedEmails.[]"
+      "rejectedEmails.[]",
+      "forceValidationReason"
     )
     emailValidation(
       serverAccountEmail,
       serverEmailValidation,
       email,
-      rejectedEmails
+      rejectedEmails,
+      forceValidationReason
     ) {
       const failedAttrs = {
         failed: true,
+        ok: false,
         element: document.querySelector("#new-account-email"),
       };
 
@@ -162,6 +165,9 @@ export default Controller.extend(
         return EmberObject.create(
           Object.assign(failedAttrs, {
             message: I18n.t("user.email.required"),
+            reason: forceValidationReason
+              ? I18n.t("user.email.required")
+              : null,
           })
         );
       }
@@ -426,6 +432,7 @@ export default Controller.extend(
       createAccount() {
         this.clearFlash();
 
+        this.set("forceValidationReason", true);
         const validation = [
           this.emailValidation,
           this.usernameValidation,
@@ -435,23 +442,22 @@ export default Controller.extend(
         ].find((v) => v.failed);
 
         if (validation) {
-          if (validation.message) {
-            this.flash(validation.message, "error");
-          }
-
           const element = validation.element;
-          if (element.tagName === "DIV") {
-            if (element.scrollIntoView) {
-              element.scrollIntoView();
+          if (element) {
+            if (element.tagName === "DIV") {
+              if (element.scrollIntoView) {
+                element.scrollIntoView();
+              }
+              element.click();
+            } else {
+              element.focus();
             }
-            element.click();
-          } else {
-            element.focus();
           }
 
           return;
         }
 
+        this.set("forceValidationReason", false);
         this.performAccountCreation();
       },
     },
diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js
index fe6b32b757a..a3a8934e522 100644
--- a/app/assets/javascripts/discourse/app/controllers/login.js
+++ b/app/assets/javascripts/discourse/app/controllers/login.js
@@ -428,7 +428,10 @@ export default Controller.extend(ModalFunctionality, {
     });
 
     next(() => {
-      showModal("createAccount", { modalClass: "create-account" });
+      showModal("createAccount", {
+        modalClass: "create-account",
+        titleAriaElementId: "create-account-title",
+      });
     });
   },
 });
diff --git a/app/assets/javascripts/discourse/app/initializers/ember-input-component-extension.js b/app/assets/javascripts/discourse/app/initializers/ember-input-component-extension.js
new file mode 100644
index 00000000000..902f7958c62
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/initializers/ember-input-component-extension.js
@@ -0,0 +1,15 @@
+import TextField from "@ember/component/text-field";
+import TextArea from "@ember/component/text-area";
+
+export default {
+  name: "ember-input-component-extensions",
+
+  initialize() {
+    TextField.reopen({
+      attributeBindings: ["aria-describedby", "aria-invalid"],
+    });
+    TextArea.reopen({
+      attributeBindings: ["aria-describedby", "aria-invalid"],
+    });
+  },
+};
diff --git a/app/assets/javascripts/discourse/app/lib/show-modal.js b/app/assets/javascripts/discourse/app/lib/show-modal.js
index db3067fbcf5..dde3094b315 100644
--- a/app/assets/javascripts/discourse/app/lib/show-modal.js
+++ b/app/assets/javascripts/discourse/app/lib/show-modal.js
@@ -47,6 +47,10 @@ export default function (name, opts) {
     modalController.set("title", null);
   }
 
+  if (opts.titleAriaElementId) {
+    modalController.set("titleAriaElementId", opts.titleAriaElementId);
+  }
+
   if (opts.panels) {
     modalController.setProperties({
       panels: opts.panels,
diff --git a/app/assets/javascripts/discourse/app/mixins/name-validation.js b/app/assets/javascripts/discourse/app/mixins/name-validation.js
index 52623f537e3..65287c97325 100644
--- a/app/assets/javascripts/discourse/app/mixins/name-validation.js
+++ b/app/assets/javascripts/discourse/app/mixins/name-validation.js
@@ -15,12 +15,14 @@ export default Mixin.create({
   },
 
   // Validate the name.
-  @discourseComputed("accountName")
-  nameValidation() {
-    if (this.siteSettings.full_name_required && isEmpty(this.accountName)) {
+  @discourseComputed("accountName", "forceValidationReason")
+  nameValidation(accountName, forceValidationReason) {
+    if (this.siteSettings.full_name_required && isEmpty(accountName)) {
       return EmberObject.create({
         failed: true,
+        ok: false,
         message: I18n.t("user.name.required"),
+        reason: forceValidationReason ? I18n.t("user.name.required") : null,
         element: document.querySelector("#new-account-name"),
       });
     }
diff --git a/app/assets/javascripts/discourse/app/mixins/password-validation.js b/app/assets/javascripts/discourse/app/mixins/password-validation.js
index feb020cbf7a..eea9b942de3 100644
--- a/app/assets/javascripts/discourse/app/mixins/password-validation.js
+++ b/app/assets/javascripts/discourse/app/mixins/password-validation.js
@@ -33,7 +33,8 @@ export default Mixin.create({
     "rejectedPasswords.[]",
     "accountUsername",
     "accountEmail",
-    "passwordMinLength"
+    "passwordMinLength",
+    "forceValidationReason"
   )
   passwordValidation(
     password,
@@ -41,10 +42,12 @@ export default Mixin.create({
     rejectedPasswords,
     accountUsername,
     accountEmail,
-    passwordMinLength
+    passwordMinLength,
+    forceValidationReason
   ) {
     const failedAttrs = {
       failed: true,
+      ok: false,
       element: document.querySelector("#new-account-password"),
     };
 
@@ -67,6 +70,9 @@ export default Mixin.create({
       return EmberObject.create(
         Object.assign(failedAttrs, {
           message: I18n.t("user.password.required"),
+          reason: forceValidationReason
+            ? I18n.t("user.password.required")
+            : null,
         })
       );
     }
diff --git a/app/assets/javascripts/discourse/app/mixins/username-validation.js b/app/assets/javascripts/discourse/app/mixins/username-validation.js
index b8f14bc818d..522b40ad5f0 100644
--- a/app/assets/javascripts/discourse/app/mixins/username-validation.js
+++ b/app/assets/javascripts/discourse/app/mixins/username-validation.js
@@ -11,6 +11,7 @@ function failedResult(attrs) {
   let result = EmberObject.create({
     shouldCheck: false,
     failed: true,
+    ok: false,
     element: document.querySelector("#new-account-username"),
   });
   result.setProperties(attrs);
@@ -60,7 +61,12 @@ export default Mixin.create({
     }
 
     if (isEmpty(username)) {
-      return failedResult({ message: I18n.t("user.username.required") });
+      return failedResult({
+        message: I18n.t("user.username.required"),
+        reason: this.forceValidationReason
+          ? I18n.t("user.username.required")
+          : null,
+      });
     }
 
     if (username.length < this.siteSettings.min_username_length) {
diff --git a/app/assets/javascripts/discourse/app/models/login-method.js b/app/assets/javascripts/discourse/app/models/login-method.js
index efe7c438bc8..bebb069b93d 100644
--- a/app/assets/javascripts/discourse/app/models/login-method.js
+++ b/app/assets/javascripts/discourse/app/models/login-method.js
@@ -13,6 +13,11 @@ const LoginMethod = EmberObject.extend({
     return this.title_override || I18n.t(`login.${this.name}.title`);
   },
 
+  @discourseComputed
+  screenReaderTitle() {
+    return this.title_override || I18n.t(`login.${this.name}.sr_title`);
+  },
+
   @discourseComputed
   prettyName() {
     return this.pretty_name_override || I18n.t(`login.${this.name}.name`);
diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js
index 4f9558c292f..a283404cf58 100644
--- a/app/assets/javascripts/discourse/app/routes/application.js
+++ b/app/assets/javascripts/discourse/app/routes/application.js
@@ -267,11 +267,18 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
       const returnPath = encodeURIComponent(window.location.pathname);
       window.location = getURL("/session/sso?return_path=" + returnPath);
     } else {
-      this._autoLogin("createAccount", "create-account", { signup: true });
+      this._autoLogin("createAccount", "create-account", {
+        signup: true,
+        titleAriaElementId: "create-account-title",
+      });
     }
   },
 
-  _autoLogin(modal, modalClass, { notAuto = null, signup = false } = {}) {
+  _autoLogin(
+    modal,
+    modalClass,
+    { notAuto = null, signup = false, titleAriaElementId = null } = {}
+  ) {
     const methods = findAll();
 
     if (!this.siteSettings.enable_local_logins && methods.length === 1) {
@@ -279,7 +286,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
         signup: signup,
       });
     } else {
-      showModal(modal);
+      showModal(modal, { titleAriaElementId });
       this.controllerFor("modal").set("modalClass", modalClass);
       if (notAuto) {
         notAuto();
diff --git a/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs b/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs
index 7067f487ef8..a0da06d06a7 100644
--- a/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/d-modal.hbs
@@ -30,7 +30,7 @@
         {{/if}}
       </div>
 
-      <div id="modal-alert"></div>
+      <div id="modal-alert" role="alert"></div>
 
       {{yield}}
 
diff --git a/app/assets/javascripts/discourse/app/templates/components/login-buttons.hbs b/app/assets/javascripts/discourse/app/templates/components/login-buttons.hbs
index bc2bc0aba98..c4e553f5b1d 100644
--- a/app/assets/javascripts/discourse/app/templates/components/login-buttons.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/login-buttons.hbs
@@ -1,5 +1,5 @@
 {{#each buttons as |b|}}
-  <button type="button" class="btn btn-social {{b.name}}" {{action externalLogin b}}>
+  <button type="button" class="btn btn-social {{b.name}}" {{action externalLogin b}} aria-label={{b.screenReaderTitle}}>
     {{#if b.isGoogle}}
       <svg class="fa d-icon d-icon-custom-google-oauth2 svg-icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/></svg>
     {{else if b.icon}}
diff --git a/app/assets/javascripts/discourse/app/templates/modal.hbs b/app/assets/javascripts/discourse/app/templates/modal.hbs
index d266b5fc1f0..44b6f315125 100644
--- a/app/assets/javascripts/discourse/app/templates/modal.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal.hbs
@@ -1,6 +1,7 @@
 {{#d-modal
   modalClass=modalClass
   title=title
+  titleAriaElementId=titleAriaElementId
   subtitle=subtitle
   panels=panels
   selectedPanel=selectedPanel
diff --git a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs
index c33f956d23f..f8688d623be 100644
--- a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs
@@ -1,17 +1,12 @@
 {{#create-account email=accountEmail disabled=submitDisabled action=(action "createAccount")}}
   {{#unless complete}}
     {{plugin-outlet name="create-account-before-modal-body"}}
-    {{#d-modal-body class=modalBodyClasses}}
+    {{#d-modal-body class=modalBodyClasses preventModalAlertHiding=true}}
       <div class="create-account-form">
-        <div class="login-welcome-header">
+        <div class="login-welcome-header" id="create-account-title">
           <h1 class="login-title">{{i18n "create_account.header_title"}}</h1> <img src={{wavingHandURL}} alt="" class="waving-hand">
           <p class="login-subheader">{{i18n "create_account.subheader_title"}}</p>
         </div>
-        {{#unless hasAuthOptions}}
-          <div class="create-account-login-buttons">
-            {{login-buttons externalLogin=(action "externalLogin")}}
-          </div>
-        {{/unless}}
         {{#if showCreateForm}}
 
           <div class="login-form">
@@ -22,7 +17,18 @@
                 </div>
               {{/if}}
               <div class="input-group create-account-email">
-                {{input type="email" disabled=emailDisabled value=accountEmail id="new-account-email" name="email" class=(value-entered accountEmail) autofocus="autofocus" focusOut=(action "checkEmailAvailability")}}
+                {{input
+                  type="email"
+                  disabled=emailDisabled
+                  value=accountEmail
+                  id="new-account-email"
+                  name="email"
+                  class=(value-entered accountEmail)
+                  autofocus="autofocus"
+                  focusOut=(action "checkEmailAvailability")
+                  aria-describedby="account-email-validation"
+                  aria-invalid=emailValidation.failed
+                }}
                 <label class="alt-placeholder" for="new-account-email">
                   {{i18n "user.email.title"}}
                   {{~#if userFields~}}
@@ -34,8 +40,17 @@
               </div>
 
               <div class="input-group">
-                {{input value=accountUsername disabled=usernameDisabled class=(value-entered accountUsername) id="new-account-username" name="username" maxlength=maxUsernameLength
-                autocomplete="discourse"}}
+                {{input
+                  value=accountUsername
+                  disabled=usernameDisabled
+                  class=(value-entered accountUsername)
+                  id="new-account-username"
+                  name="username"
+                  maxlength=maxUsernameLength
+                  aria-describedby="username-validation"
+                  aria-invalid=usernameValidation.failed
+                  autocomplete="discourse"
+                }}
                 <label class="alt-placeholder" for="new-account-username">
                   {{i18n "user.username.title"}}
                   {{~#if userFields~}}
@@ -49,7 +64,14 @@
 
               <div class="input-group">
                 {{#if fullnameRequired}}
-                  {{text-field disabled=nameDisabled value=accountName id="new-account-name" class=(value-entered accountName)}}
+                  {{text-field
+                    disabled=nameDisabled
+                    value=accountName
+                    id="new-account-name"
+                    class=(value-entered accountName)
+                    aria-describedby="fullname-validation"
+                    aria-invalid=nameValidation.failed
+                  }}
                   <label class="alt-placeholder" for="new-account-name">
                     {{i18n "user.name.title"}}
                     {{#if siteSettings.full_name_required}}
@@ -59,26 +81,35 @@
                     {{/if}}
                   </label>
 
-                  {{input-tip validation=nameValidation}}
+                  {{input-tip validation=nameValidation id="fullname-validation"}}
                   <span class="more-info">{{nameInstructions}}</span>
                 {{/if}}
               </div>
 
               {{plugin-outlet
-              name="create-account-before-password"
-                noTags=true
-                args=(hash
-                  accountName=accountName
-                  accountUsername=accountUsername
-                  accountPassword=accountPassword
-                  userFields=userFields
-                  authOptions=authOptions
-                )
+                  name="create-account-before-password"
+                  noTags=true
+                  args=(hash
+                    accountName=accountName
+                    accountUsername=accountUsername
+                    accountPassword=accountPassword
+                    userFields=userFields
+                    authOptions=authOptions
+                  )
               }}
 
               <div class="input-group">
                 {{#if passwordRequired}}
-                  {{password-field value=accountPassword class=(value-entered accountPassword) type="password" id="new-account-password" autocomplete="current-password" capsLockOn=capsLockOn}}
+                  {{password-field
+                    value=accountPassword
+                    class=(value-entered accountPassword)
+                    type="password"
+                    id="new-account-password"
+                    autocomplete="current-password"
+                    capsLockOn=capsLockOn
+                    aria-describedby="password-validation"
+                    aria-invalid=passwordValidation.failed
+                  }}
                   <label class="alt-placeholder" for="new-account-password">
                     {{i18n "user.password.title"}}
                     {{~#if userFields~}}
@@ -86,7 +117,7 @@
                     {{/if}}
                   </label>
 
-                  {{input-tip validation=passwordValidation}}
+                  {{input-tip validation=passwordValidation id="password-validation"}}
                   <span class="more-info">{{passwordInstructions}}</span>
                   <div class="caps-lock-warning {{unless capsLockOn " hidden"}}">
                     {{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
@@ -156,6 +187,11 @@
           {{plugin-outlet name="create-account-after-modal-footer" tagName=""}}
 
         {{/if}}
+        {{#unless hasAuthOptions}}
+          <div class="create-account-login-buttons">
+            {{login-buttons externalLogin=(action "externalLogin")}}
+          </div>
+        {{/unless}}
 
         {{#if skipConfirmation}}
           {{loading-spinner size="large"}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
index 02adf36a8de..a5548ab424a 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
@@ -2,7 +2,6 @@ import {
   acceptance,
   exists,
   query,
-  queryAll,
 } from "discourse/tests/helpers/qunit-helpers";
 import { click, fillIn, visit } from "@ember/test-helpers";
 import { test } from "qunit";
@@ -39,9 +38,8 @@ acceptance("Create Account - User Fields", function (needs) {
     assert.ok(exists(".user-field"), "it has at least one user field");
 
     await click(".modal-footer .btn-primary");
-    assert.ok(exists("#modal-alert"), "it shows the required field alert");
     assert.equal(
-      queryAll("#modal-alert").text(),
+      query("#account-email-validation").innerText.trim(),
       "Please enter an email address"
     );
 
@@ -63,12 +61,8 @@ acceptance("Create Account - User Fields", function (needs) {
     );
 
     await click(".modal-footer .btn-primary");
-    assert.equal(query("#modal-alert").style.display, "");
-
     await fillIn(".user-field input[type=text]:nth-of-type(1)", "Barky");
     await click(".user-field input[type=checkbox]");
-
     await click(".modal-footer .btn-primary");
-    assert.equal(query("#modal-alert").style.display, "none");
   });
 });
diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss
index 16544563415..1fa25dedf8f 100644
--- a/app/assets/stylesheets/common/base/login.scss
+++ b/app/assets/stylesheets/common/base/login.scss
@@ -60,7 +60,10 @@
     min-height: 35px;
   }
   #modal-alert:empty {
-    display: none;
+    min-height: 0px;
+    padding: 0px;
+    overflow: hidden;
+    display: inline;
   }
   .login-welcome-header {
     z-index: z("modal", "content");
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 06ce6dc9f17..f531052cad7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1897,21 +1897,27 @@ en:
       google_oauth2:
         name: "Google"
         title: "with Google"
+        sr_title: "Login with Google"
       twitter:
         name: "Twitter"
         title: "with Twitter"
+        sr_title: "Login with Twitter"
       instagram:
         name: "Instagram"
         title: "with Instagram"
+        sr_title: "Login with Instagram"
       facebook:
         name: "Facebook"
         title: "with Facebook"
+        sr_title: "Login with Facebook"
       github:
         name: "GitHub"
         title: "with GitHub"
+        sr_title: "Login with GitHub"
       discord:
         name: "Discord"
         title: "with Discord"
+        sr_title: "Login with Discord"
       second_factor_toggle:
         totp: "Use an authenticator app instead"
         backup_code: "Use a backup code instead"