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}} -
+ {{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|}} -