From 77fb832221f8ac3a7c66b1aca582b9bf17c00a99 Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Tue, 7 Jun 2016 14:31:41 +0200 Subject: [PATCH 1/7] Added focus capturing, redirecting it to the open confirmation dialog HFJ-1995 --- js/h5p-confirmation-dialog.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index f2b6c61..cb326c6 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -158,6 +158,9 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Wrapper element var wrapperElement; + // Focus capturing + var focusPredator; + /** * Set parent of confirmation dialog * @param {HTMLElement} wrapper @@ -168,6 +171,32 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { return this; }; + /** + * Capture the focus element, send it to confirmation button + * @param {Event} e Original focus event + */ + var captureFocus = function (e) { + if (!popupBackground.contains(e.target)) { + e.preventDefault(); + confirmButton.focus(); + } + }; + + /** + * Start capturing focus of parent and send it to dialog + */ + var startCapturingFocus = function () { + focusPredator = wrapperElement.parentNode || wrapperElement; + focusPredator.addEventListener('focus', captureFocus, true); + }; + + /** + * Clean up event listener for capturing focus + */ + var stopCapturingFocus = function () { + focusPredator.removeEventListener('focus', captureFocus, true); + }; + /** * Fit popup to container. Makes sure it doesn't overflow. * @params {number} [offsetTop] Offset of popup @@ -199,6 +228,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { */ this.show = function (offsetTop) { wrapperElement.appendChild(popupBackground); + startCapturingFocus(); popupBackground.classList.remove('hidden'); fitToContainer(offsetTop); setTimeout(function () { @@ -232,6 +262,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { popup.classList.add('hidden'); setTimeout(function () { popupBackground.classList.add('hidden'); + stopCapturingFocus(); wrapperElement.removeChild(popupBackground); }, 100); From 10e1a7ba65957792a71030b1917d79ecb1f41e26 Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Tue, 7 Jun 2016 15:09:59 +0200 Subject: [PATCH 2/7] Use DOM Level 2 events. HFJ-1999 --- js/h5p-confirmation-dialog.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index cb326c6..3c16214 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -71,12 +71,12 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); popup.setAttribute('role', 'dialog'); popupBackground.appendChild(popup); - popup.onkeydown = function (e) { + popup.addEventListener('keydown', function (e) { if (e.which === 27) {// Esc key // Exit dialog dialogCanceled(e); } - }; + }); // Popup header var header = document.createElement('div'); @@ -123,36 +123,36 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { exitButton.title = options.cancelText; // Cancel handler - cancelButton.onclick = dialogCanceled; - cancelButton.onkeydown = function (e) { + cancelButton.addEventListener('click', dialogCanceled); + cancelButton.addEventListener('keydown', function (e) { if (e.which === 32) { // Space dialogCanceled(e); } else if (e.which === 9 && e.shiftKey) { // Shift-tab flowTo(exitButton, e); } - }; + }); buttons.appendChild(cancelButton); // Confirm handler - confirmButton.onclick = dialogConfirmed; - confirmButton.onkeydown = function (e) { + confirmButton.addEventListener('click', dialogConfirmed); + confirmButton.addEventListener('keydown', function (e) { if (e.which === 32) { // Space dialogConfirmed(e); } - }; + }); buttons.appendChild(confirmButton); // Exit handler - exitButton.onclick = dialogCanceled; - exitButton.onkeydown = function (e) { + exitButton.addEventListener('click', dialogCanceled); + exitButton.addEventListener('keydown', function (e) { if (e.which === 32) { // Space dialogCanceled(e); } else if (e.which === 9 && !e.shiftKey) { // Tab flowTo(cancelButton, e); } - }; + }); popup.appendChild(exitButton); // Wrapper element From 8615deb23b8021cba4c29a55387ede2fc79f26e5 Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Wed, 8 Jun 2016 11:06:03 +0200 Subject: [PATCH 3/7] Hide elements that confirmation dialog covers from assistive technologies. HFJ-1995 --- js/h5p-confirmation-dialog.js | 71 ++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index 3c16214..ab551dd 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -161,6 +161,13 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Focus capturing var focusPredator; + // Maintains hidden state of elements + var wrapperSiblingsHidden = []; + var popupSiblingsHidden = []; + + // Element with focus before dialog + var previouslyFocused; + /** * Set parent of confirmation dialog * @param {HTMLElement} wrapper @@ -182,6 +189,44 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { } }; + /** + * Hide siblings of element from assistive technology + * + * @param {HTMLElement} element + * @returns {Array} The previous hidden state of all siblings + */ + var hideSiblings = function (element) { + var hiddenSiblings = []; + var siblings = element.parentNode.children; + var i; + for (i = 0; i < siblings.length; i += 1) { + // Preserve hidden state + hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ? + true : false; + + if (siblings[i] !== element) { + siblings[i].setAttribute('aria-hidden', true); + } + } + return hiddenSiblings; + }; + + /** + * Restores assistive technology state of element's siblings + * + * @param {HTMLElement} element + * @param {Array} hiddenSiblings Hidden state of all siblings + */ + var restoreSiblings = function (element, hiddenSiblings) { + var siblings = element.parentNode.children; + var i; + for (i = 0; i < siblings.length; i += 1) { + if (siblings[i] !== element && !hiddenSiblings[i]) { + siblings[i].removeAttribute('aria-hidden'); + } + } + }; + /** * Start capturing focus of parent and send it to dialog */ @@ -194,9 +239,26 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { * Clean up event listener for capturing focus */ var stopCapturingFocus = function () { + focusPredator.removeAttribute('aria-hidden'); focusPredator.removeEventListener('focus', captureFocus, true); }; + /** + * Hide siblings in underlay from assistive technologies + */ + var disableUnderlay = function () { + wrapperSiblingsHidden = hideSiblings(wrapperElement); + popupSiblingsHidden = hideSiblings(popupBackground); + }; + + /** + * Restore state of underlay for assistive technologies + */ + var restoreUnderlay = function () { + restoreSiblings(wrapperElement, wrapperSiblingsHidden); + restoreSiblings(popupBackground, popupSiblingsHidden); + }; + /** * Fit popup to container. Makes sure it doesn't overflow. * @params {number} [offsetTop] Offset of popup @@ -227,8 +289,11 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { * @returns {H5P.ConfirmationDialog} */ this.show = function (offsetTop) { + // Capture focused item + previouslyFocused = document.activeElement; wrapperElement.appendChild(popupBackground); startCapturingFocus(); + disableUnderlay(); popupBackground.classList.remove('hidden'); fitToContainer(offsetTop); setTimeout(function () { @@ -260,9 +325,13 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { this.hide = function () { popupBackground.classList.add('hiding'); popup.classList.add('hidden'); + + // Restore focus + stopCapturingFocus(); + previouslyFocused.focus(); + restoreUnderlay(); setTimeout(function () { popupBackground.classList.add('hidden'); - stopCapturingFocus(); wrapperElement.removeChild(popupBackground); }, 100); From 6caf44e54b69edd98f55733f522f5763e8304e76 Mon Sep 17 00:00:00 2001 From: Paal Joergensen Date: Wed, 8 Jun 2016 11:28:07 +0200 Subject: [PATCH 4/7] Added checks for mbstring PHP extension [HFJ-1996] --- h5p.classes.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/h5p.classes.php b/h5p.classes.php index 71e95c4..851515f 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2898,7 +2898,12 @@ class H5PContentValidator { // Check if string is within allowed length if (isset($semantics->maxLength)) { - $text = mb_substr($text, 0, $semantics->maxLength); + if (!extension_loaded('mbstring')) { + $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'error'); + } + else { + $text = mb_substr($text, 0, $semantics->maxLength); + } } // Check if string is according to optional regexp in semantics @@ -2948,7 +2953,11 @@ class H5PContentValidator { // file name, 2. testing against a returned error array that could // never be more than 1 element long anyway, 3. recreating the regex // for every file. - if (!preg_match($wl_regex, mb_strtolower($file))) { + if (!extension_loaded('mbstring')) { + $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'error'); + $valid = FALSE; + } + else if (!preg_match($wl_regex, mb_strtolower($file))) { $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $file, '%files-allowed' => $whitelist)), 'error'); $valid = FALSE; } From e10594fb495d3c525a6ba5f9a55223fb9572c17c Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Thu, 9 Jun 2016 11:26:11 +0200 Subject: [PATCH 5/7] Hide redundant confirmation dialog exit button for assistive technologies. HFJ-2004 --- js/h5p-confirmation-dialog.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index ab551dd..b6ff15f 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -120,6 +120,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Exit button var exitButton = document.createElement('button'); exitButton.classList.add('h5p-confirmation-dialog-exit'); + exitButton.setAttribute('aria-hidden', 'true'); exitButton.title = options.cancelText; // Cancel handler @@ -167,7 +168,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Element with focus before dialog var previouslyFocused; - + /** * Set parent of confirmation dialog * @param {HTMLElement} wrapper @@ -191,7 +192,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { /** * Hide siblings of element from assistive technology - * + * * @param {HTMLElement} element * @returns {Array} The previous hidden state of all siblings */ @@ -213,7 +214,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { /** * Restores assistive technology state of element's siblings - * + * * @param {HTMLElement} element * @param {Array} hiddenSiblings Hidden state of all siblings */ From 51851f14c340165f7e76779d21090942ad36a4f6 Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Thu, 9 Jun 2016 13:18:09 +0200 Subject: [PATCH 6/7] Use aria-labelledby to describe dialog, instead of alert. HFJ-2003 --- js/h5p-confirmation-dialog.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index b6ff15f..481429f 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -16,6 +16,10 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { function ConfirmationDialog(options) { EventDispatcher.call(this); var self = this; + + // Make sure confirmation dialogs have unique id + H5P.ConfirmationDialog.uniqueId += 1; + var uniqueId = H5P.ConfirmationDialog.uniqueId; // Default options options = options || {}; @@ -70,6 +74,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { var popup = document.createElement('div'); popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); popup.setAttribute('role', 'dialog'); + popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId); popupBackground.appendChild(popup); popup.addEventListener('keydown', function (e) { if (e.which === 27) {// Esc key @@ -97,8 +102,8 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Popup text var text = document.createElement('div'); text.classList.add('h5p-confirmation-dialog-text'); - text.setAttribute('role', 'alert'); text.innerHTML = options.dialogText; + text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId; body.appendChild(text); // Popup buttons @@ -346,3 +351,5 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { return ConfirmationDialog; }(H5P.EventDispatcher)); + +H5P.ConfirmationDialog.uniqueId = -1; From b3c55928e375a75f098be6be1344d61e1675663b Mon Sep 17 00:00:00 2001 From: Thomas Marstrander Date: Thu, 9 Jun 2016 15:26:15 +0200 Subject: [PATCH 7/7] Remove tabindex of exitButton in confirmation dialog. Trapped focus between confirm and cancel button. HFJ-2004 --- js/h5p-confirmation-dialog.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index 481429f..a06166b 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -16,7 +16,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { function ConfirmationDialog(options) { EventDispatcher.call(this); var self = this; - + // Make sure confirmation dialogs have unique id H5P.ConfirmationDialog.uniqueId += 1; var uniqueId = H5P.ConfirmationDialog.uniqueId; @@ -126,6 +126,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { var exitButton = document.createElement('button'); exitButton.classList.add('h5p-confirmation-dialog-exit'); exitButton.setAttribute('aria-hidden', 'true'); + exitButton.tabIndex = -1; exitButton.title = options.cancelText; // Cancel handler @@ -135,7 +136,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { dialogCanceled(e); } else if (e.which === 9 && e.shiftKey) { // Shift-tab - flowTo(exitButton, e); + flowTo(confirmButton, e); } }); buttons.appendChild(cancelButton); @@ -146,6 +147,9 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { if (e.which === 32) { // Space dialogConfirmed(e); } + else if (e.which === 9 && !e.shiftKey) { // Tab + flowTo(cancelButton, e); + } }); buttons.appendChild(confirmButton); @@ -155,9 +159,6 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { if (e.which === 32) { // Space dialogCanceled(e); } - else if (e.which === 9 && !e.shiftKey) { // Tab - flowTo(cancelButton, e); - } }); popup.appendChild(exitButton);