From da237792ecb4d6d03607fd5d455ecbadafb246d3 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 10 Dec 2020 22:03:21 +0800 Subject: [PATCH] MDL-70288 theme_boost: Manage aria-describedby on form validation * The aria-describedby attribute can be a list of element IDs that describe the element. On form validation, the ID of the error message container is added to this attribute which may already be containing another ID. So we need to properly add/remove the error message ID so that we don't delete any existing ID(s) in the aria-describedby attribute during form validation. --- .../amd/build/form-display-errors.min.js | 2 +- .../amd/build/form-display-errors.min.js.map | 2 +- theme/boost/amd/src/form-display-errors.js | 35 +++++++++++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/theme/boost/amd/build/form-display-errors.min.js b/theme/boost/amd/build/form-display-errors.min.js index e169aac1e44..fa6511c5681 100644 --- a/theme/boost/amd/build/form-display-errors.min.js +++ b/theme/boost/amd/build/form-display-errors.min.js @@ -1,2 +1,2 @@ -define ("theme_boost/form-display-errors",["jquery","core/event"],function(a,b){return{enhance:function enhance(c){var d=document.getElementById(c);if(!d){return}a(d).on(b.Events.FORM_FIELD_VALIDATION,function(b,c){b.preventDefault();var e=a(d).closest(".form-group"),f=e.find(".form-control-feedback");if("TEXTAREA"==a(d).prop("tagName")&&e.find("[contenteditable]")){d=e.find("[contenteditable]")}if(""!==c){e.addClass("has-danger");e.data("client-validation-error",!0);a(d).addClass("is-invalid");a(d).attr("aria-describedby",f.attr("id"));a(d).attr("aria-invalid",!0);f.attr("tabindex",0);f.html(c);if(!f.is(":visible")){f.show();f.focus()}}else{if(!0===e.data("client-validation-error")){e.removeClass("has-danger");e.data("client-validation-error",!1);a(d).removeClass("is-invalid");a(d).removeAttr("aria-describedby");a(d).attr("aria-invalid",!1);f.hide()}}});var e=d.closest("form");if(e&&!("boostFormErrorsEnhanced"in e.dataset)){e.addEventListener("submit",function(){var b=a(".form-control-feedback:visible");if(b.length){b[0].focus()}});e.dataset.boostFormErrorsEnhanced=1}}}}); +define ("theme_boost/form-display-errors",["jquery","core/event"],function(a,b){return{enhance:function enhance(c){var d=document.getElementById(c);if(!d){return}a(d).on(b.Events.FORM_FIELD_VALIDATION,function(b,c){b.preventDefault();var e=a(d).closest(".form-group"),f=e.find(".form-control-feedback"),g=f.attr("id"),h=a(d).attr("aria-describedby");if("undefined"==typeof h){h=""}var i=[];if(h.length){i=h.split(" ")}var j=i.indexOf(g);if("TEXTAREA"==a(d).prop("tagName")&&e.find("[contenteditable]")){d=e.find("[contenteditable]")}if(""!==c){e.addClass("has-danger");e.data("client-validation-error",!0);a(d).addClass("is-invalid");if(-1===j){i.push(g);a(d).attr("aria-describedby",i.join(" "))}a(d).attr("aria-invalid",!0);f.attr("tabindex",0);f.html(c);if(!f.is(":visible")){f.show();f.focus()}}else{if(!0===e.data("client-validation-error")){e.removeClass("has-danger");e.data("client-validation-error",!1);a(d).removeClass("is-invalid");if(-1.\n\n/**\n * Custom form error event handler to manipulate the bootstrap markup and show\n * nicely styled errors in an mform.\n *\n * @module theme_boost/form-display-errors\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/event'], function($, Event) {\n return {\n enhance: function(elementid) {\n var element = document.getElementById(elementid);\n if (!element) {\n // Some elements (e.g. static) don't have a form field.\n // Hence there is no validation. So, no setup required here.\n return;\n }\n\n $(element).on(Event.Events.FORM_FIELD_VALIDATION, function(event, msg) {\n event.preventDefault();\n var parent = $(element).closest('.form-group');\n var feedback = parent.find('.form-control-feedback');\n\n // Sometimes (atto) we have a hidden textarea backed by a real contenteditable div.\n if (($(element).prop(\"tagName\") == 'TEXTAREA') && parent.find('[contenteditable]')) {\n element = parent.find('[contenteditable]');\n }\n if (msg !== '') {\n parent.addClass('has-danger');\n parent.data('client-validation-error', true);\n $(element).addClass('is-invalid');\n $(element).attr('aria-describedby', feedback.attr('id'));\n $(element).attr('aria-invalid', true);\n feedback.attr('tabindex', 0);\n feedback.html(msg);\n\n // Only display and focus when the error was not already visible.\n // This is so that, when tabbing around the form, you don't get stuck.\n if (!feedback.is(':visible')) {\n feedback.show();\n feedback.focus();\n }\n\n } else {\n if (parent.data('client-validation-error') === true) {\n parent.removeClass('has-danger');\n parent.data('client-validation-error', false);\n $(element).removeClass('is-invalid');\n $(element).removeAttr('aria-describedby');\n $(element).attr('aria-invalid', false);\n feedback.hide();\n }\n }\n });\n\n var form = element.closest('form');\n if (form && !('boostFormErrorsEnhanced' in form.dataset)) {\n form.addEventListener('submit', function() {\n var visibleError = $('.form-control-feedback:visible');\n if (visibleError.length) {\n visibleError[0].focus();\n }\n });\n form.dataset.boostFormErrorsEnhanced = 1;\n }\n }\n };\n});\n"],"file":"form-display-errors.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/form-display-errors.js"],"names":["define","$","Event","enhance","elementid","element","document","getElementById","on","Events","FORM_FIELD_VALIDATION","event","msg","preventDefault","parent","closest","feedback","find","feedbackId","attr","describedBy","describedByIds","length","split","feedbackIndex","indexOf","prop","addClass","data","push","join","html","is","show","focus","removeClass","splice","removeAttr","hide","form","dataset","addEventListener","visibleError","boostFormErrorsEnhanced"],"mappings":"AAuBAA,OAAM,mCAAC,CAAC,QAAD,CAAW,YAAX,CAAD,CAA2B,SAASC,CAAT,CAAYC,CAAZ,CAAmB,CAChD,MAAO,CACHC,OAAO,CAAE,iBAASC,CAAT,CAAoB,CACzB,GAAIC,CAAAA,CAAO,CAAGC,QAAQ,CAACC,cAAT,CAAwBH,CAAxB,CAAd,CACA,GAAI,CAACC,CAAL,CAAc,CAGV,MACH,CAEDJ,CAAC,CAACI,CAAD,CAAD,CAAWG,EAAX,CAAcN,CAAK,CAACO,MAAN,CAAaC,qBAA3B,CAAkD,SAASC,CAAT,CAAgBC,CAAhB,CAAqB,CACnED,CAAK,CAACE,cAAN,GADmE,GAE/DC,CAAAA,CAAM,CAAGb,CAAC,CAACI,CAAD,CAAD,CAAWU,OAAX,CAAmB,aAAnB,CAFsD,CAG/DC,CAAQ,CAAGF,CAAM,CAACG,IAAP,CAAY,wBAAZ,CAHoD,CAI7DC,CAAU,CAAGF,CAAQ,CAACG,IAAT,CAAc,IAAd,CAJgD,CAO/DC,CAAW,CAAGnB,CAAC,CAACI,CAAD,CAAD,CAAWc,IAAX,CAAgB,kBAAhB,CAPiD,CAQnE,GAA2B,WAAvB,QAAOC,CAAAA,CAAX,CAAwC,CACpCA,CAAW,CAAG,EACjB,CAED,GAAIC,CAAAA,CAAc,CAAG,EAArB,CACA,GAAID,CAAW,CAACE,MAAhB,CAAwB,CACpBD,CAAc,CAAGD,CAAW,CAACG,KAAZ,CAAkB,GAAlB,CACpB,CAED,GAAMC,CAAAA,CAAa,CAAGH,CAAc,CAACI,OAAf,CAAuBP,CAAvB,CAAtB,CAGA,GAAmC,UAA9B,EAAAjB,CAAC,CAACI,CAAD,CAAD,CAAWqB,IAAX,CAAgB,SAAhB,CAAD,EAA8CZ,CAAM,CAACG,IAAP,CAAY,mBAAZ,CAAlD,CAAoF,CAChFZ,CAAO,CAAGS,CAAM,CAACG,IAAP,CAAY,mBAAZ,CACb,CACD,GAAY,EAAR,GAAAL,CAAJ,CAAgB,CACZE,CAAM,CAACa,QAAP,CAAgB,YAAhB,EACAb,CAAM,CAACc,IAAP,CAAY,yBAAZ,KACA3B,CAAC,CAACI,CAAD,CAAD,CAAWsB,QAAX,CAAoB,YAApB,EAEA,GAAsB,CAAC,CAAnB,GAAAH,CAAJ,CAA0B,CACtBH,CAAc,CAACQ,IAAf,CAAoBX,CAApB,EACAjB,CAAC,CAACI,CAAD,CAAD,CAAWc,IAAX,CAAgB,kBAAhB,CAAoCE,CAAc,CAACS,IAAf,CAAoB,GAApB,CAApC,CACH,CACD7B,CAAC,CAACI,CAAD,CAAD,CAAWc,IAAX,CAAgB,cAAhB,KACAH,CAAQ,CAACG,IAAT,CAAc,UAAd,CAA0B,CAA1B,EACAH,CAAQ,CAACe,IAAT,CAAcnB,CAAd,EAIA,GAAI,CAACI,CAAQ,CAACgB,EAAT,CAAY,UAAZ,CAAL,CAA8B,CAC1BhB,CAAQ,CAACiB,IAAT,GACAjB,CAAQ,CAACkB,KAAT,EACH,CAEJ,CApBD,IAoBO,CACH,GAAI,KAAApB,CAAM,CAACc,IAAP,CAAY,yBAAZ,CAAJ,CAAqD,CACjDd,CAAM,CAACqB,WAAP,CAAmB,YAAnB,EACArB,CAAM,CAACc,IAAP,CAAY,yBAAZ,KACA3B,CAAC,CAACI,CAAD,CAAD,CAAW8B,WAAX,CAAuB,YAAvB,EAEA,GAAoB,CAAC,CAAjB,CAAAX,CAAJ,CAAwB,CACpBH,CAAc,CAACe,MAAf,CAAsBZ,CAAtB,CAAqC,CAArC,CACH,CAED,GAAIH,CAAc,CAACC,MAAnB,CAA2B,CAEvBF,CAAW,CAAGC,CAAc,CAACS,IAAf,CAAoB,GAApB,CAAd,CAEA7B,CAAC,CAACI,CAAD,CAAD,CAAWc,IAAX,CAAgB,kBAAhB,CAAoCC,CAApC,CACH,CALD,IAKO,CAEHnB,CAAC,CAACI,CAAD,CAAD,CAAWgC,UAAX,CAAsB,kBAAtB,CACH,CACDpC,CAAC,CAACI,CAAD,CAAD,CAAWc,IAAX,CAAgB,cAAhB,KACAH,CAAQ,CAACsB,IAAT,EACH,CACJ,CACJ,CAlED,EAoEA,GAAIC,CAAAA,CAAI,CAAGlC,CAAO,CAACU,OAAR,CAAgB,MAAhB,CAAX,CACA,GAAIwB,CAAI,EAAI,EAAE,2BAA6BA,CAAAA,CAAI,CAACC,OAApC,CAAZ,CAA0D,CACtDD,CAAI,CAACE,gBAAL,CAAsB,QAAtB,CAAgC,UAAW,CACvC,GAAIC,CAAAA,CAAY,CAAGzC,CAAC,CAAC,gCAAD,CAApB,CACA,GAAIyC,CAAY,CAACpB,MAAjB,CAAyB,CACrBoB,CAAY,CAAC,CAAD,CAAZ,CAAgBR,KAAhB,EACH,CACJ,CALD,EAMAK,CAAI,CAACC,OAAL,CAAaG,uBAAb,CAAuC,CAC1C,CACJ,CAvFE,CAyFV,CA1FK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Custom form error event handler to manipulate the bootstrap markup and show\n * nicely styled errors in an mform.\n *\n * @module theme_boost/form-display-errors\n * @copyright 2016 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/event'], function($, Event) {\n return {\n enhance: function(elementid) {\n var element = document.getElementById(elementid);\n if (!element) {\n // Some elements (e.g. static) don't have a form field.\n // Hence there is no validation. So, no setup required here.\n return;\n }\n\n $(element).on(Event.Events.FORM_FIELD_VALIDATION, function(event, msg) {\n event.preventDefault();\n var parent = $(element).closest('.form-group');\n var feedback = parent.find('.form-control-feedback');\n const feedbackId = feedback.attr('id');\n\n // Get current aria-describedby value.\n let describedBy = $(element).attr('aria-describedby');\n if (typeof describedBy === \"undefined\") {\n describedBy = '';\n }\n // Split aria-describedby attribute into an array of IDs if necessary.\n let describedByIds = [];\n if (describedBy.length) {\n describedByIds = describedBy.split(\" \");\n }\n // Find the the feedback container in the aria-describedby attribute.\n const feedbackIndex = describedByIds.indexOf(feedbackId);\n\n // Sometimes (atto) we have a hidden textarea backed by a real contenteditable div.\n if (($(element).prop(\"tagName\") == 'TEXTAREA') && parent.find('[contenteditable]')) {\n element = parent.find('[contenteditable]');\n }\n if (msg !== '') {\n parent.addClass('has-danger');\n parent.data('client-validation-error', true);\n $(element).addClass('is-invalid');\n // Append the feedback ID to the aria-describedby attribute if it doesn't exist yet.\n if (feedbackIndex === -1) {\n describedByIds.push(feedbackId);\n $(element).attr('aria-describedby', describedByIds.join(\" \"));\n }\n $(element).attr('aria-invalid', true);\n feedback.attr('tabindex', 0);\n feedback.html(msg);\n\n // Only display and focus when the error was not already visible.\n // This is so that, when tabbing around the form, you don't get stuck.\n if (!feedback.is(':visible')) {\n feedback.show();\n feedback.focus();\n }\n\n } else {\n if (parent.data('client-validation-error') === true) {\n parent.removeClass('has-danger');\n parent.data('client-validation-error', false);\n $(element).removeClass('is-invalid');\n // If the aria-describedby attribute contains the error container's ID, remove it.\n if (feedbackIndex > -1) {\n describedByIds.splice(feedbackIndex, 1);\n }\n // Check the remaining element IDs in the aria-describedby attribute.\n if (describedByIds.length) {\n // If there's at least one, combine them with a blank space and update the aria-describedby attribute.\n describedBy = describedByIds.join(\" \");\n // Put back the new describedby attribute.\n $(element).attr('aria-describedby', describedBy);\n } else {\n // If there's none, remove the aria-describedby attribute.\n $(element).removeAttr('aria-describedby');\n }\n $(element).attr('aria-invalid', false);\n feedback.hide();\n }\n }\n });\n\n var form = element.closest('form');\n if (form && !('boostFormErrorsEnhanced' in form.dataset)) {\n form.addEventListener('submit', function() {\n var visibleError = $('.form-control-feedback:visible');\n if (visibleError.length) {\n visibleError[0].focus();\n }\n });\n form.dataset.boostFormErrorsEnhanced = 1;\n }\n }\n };\n});\n"],"file":"form-display-errors.min.js"} \ No newline at end of file diff --git a/theme/boost/amd/src/form-display-errors.js b/theme/boost/amd/src/form-display-errors.js index a8391847a5a..4820397e7f2 100644 --- a/theme/boost/amd/src/form-display-errors.js +++ b/theme/boost/amd/src/form-display-errors.js @@ -35,6 +35,20 @@ define(['jquery', 'core/event'], function($, Event) { event.preventDefault(); var parent = $(element).closest('.form-group'); var feedback = parent.find('.form-control-feedback'); + const feedbackId = feedback.attr('id'); + + // Get current aria-describedby value. + let describedBy = $(element).attr('aria-describedby'); + if (typeof describedBy === "undefined") { + describedBy = ''; + } + // Split aria-describedby attribute into an array of IDs if necessary. + let describedByIds = []; + if (describedBy.length) { + describedByIds = describedBy.split(" "); + } + // Find the the feedback container in the aria-describedby attribute. + const feedbackIndex = describedByIds.indexOf(feedbackId); // Sometimes (atto) we have a hidden textarea backed by a real contenteditable div. if (($(element).prop("tagName") == 'TEXTAREA') && parent.find('[contenteditable]')) { @@ -44,7 +58,11 @@ define(['jquery', 'core/event'], function($, Event) { parent.addClass('has-danger'); parent.data('client-validation-error', true); $(element).addClass('is-invalid'); - $(element).attr('aria-describedby', feedback.attr('id')); + // Append the feedback ID to the aria-describedby attribute if it doesn't exist yet. + if (feedbackIndex === -1) { + describedByIds.push(feedbackId); + $(element).attr('aria-describedby', describedByIds.join(" ")); + } $(element).attr('aria-invalid', true); feedback.attr('tabindex', 0); feedback.html(msg); @@ -61,7 +79,20 @@ define(['jquery', 'core/event'], function($, Event) { parent.removeClass('has-danger'); parent.data('client-validation-error', false); $(element).removeClass('is-invalid'); - $(element).removeAttr('aria-describedby'); + // If the aria-describedby attribute contains the error container's ID, remove it. + if (feedbackIndex > -1) { + describedByIds.splice(feedbackIndex, 1); + } + // Check the remaining element IDs in the aria-describedby attribute. + if (describedByIds.length) { + // If there's at least one, combine them with a blank space and update the aria-describedby attribute. + describedBy = describedByIds.join(" "); + // Put back the new describedby attribute. + $(element).attr('aria-describedby', describedBy); + } else { + // If there's none, remove the aria-describedby attribute. + $(element).removeAttr('aria-describedby'); + } $(element).attr('aria-invalid', false); feedback.hide(); }