From b0877275aa18b03af4b2a6c74c15d885364a9a12 Mon Sep 17 00:00:00 2001 From: Robert Lange <robert.lange@uni-leipzig.de> Date: Thu, 24 Sep 2020 16:29:52 +0200 Subject: [PATCH] refs #17576 [master] adapt BARF keyboard and focus controllers from Micromodal * backported for Vufind 5 to keep focus in modal when tabbing with keyboard * also see https://github.com/vufind-org/vufind/pull/1667 --- themes/finc/js/lightbox.js | 105 +++++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/themes/finc/js/lightbox.js b/themes/finc/js/lightbox.js index 17e429708ba..1593b4d96b5 100644 --- a/themes/finc/js/lightbox.js +++ b/themes/finc/js/lightbox.js @@ -1,4 +1,4 @@ -/*global recaptchaOnLoad, resetCaptcha, VuFind, TEMPORARY BARF CK*/ +/*global grecaptcha, recaptchaOnLoad, resetCaptcha, VuFind, TEMPORARY BARF CK */ VuFind.register('lightbox', function Lightbox() { // State var _originalUrl = false; @@ -7,7 +7,7 @@ VuFind.register('lightbox', function Lightbox() { var refreshOnClose = false; var _modalParams = {}; // Elements - var _modal, _modalBody, _modalTitle, _clickedButton = null; + var _modal, _modalBody, _clickedButton = null; // Utilities function _storeClickedStatus() { _clickedButton = this; @@ -15,10 +15,11 @@ VuFind.register('lightbox', function Lightbox() { function _html(content) { _modalBody.html(content); // Set or update title if we have one - if (_lightboxTitle) { - _modalTitle.text(_lightboxTitle); - _lightboxTitle = false; + var $h2 = _modalBody.find("h2:first-of-type"); + if (_lightboxTitle && $h2) { + $h2.text(_lightboxTitle); } + _lightboxTitle = false; _modal.modal('handleUpdate'); } function _emit(msg, _details) { @@ -329,7 +330,7 @@ VuFind.register('lightbox', function Lightbox() { submit.attr('disabled', 'disabled'); } // Store custom title - _lightboxTitle = submit.data('lightboxTitle') || $(form).data('lightboxTitle') || ''; + _lightboxTitle = submit.data('lightbox-title') || $(form).data('lightbox-title') || false; // Get Lightbox content ajax({ url: $(form).attr('action') || _currentUrl, @@ -343,6 +344,86 @@ VuFind.register('lightbox', function Lightbox() { return false; }; + /** + * Keyboard and focus controllers + * Adapted from Micromodal + * - https://github.com/ghosh/Micromodal/blob/master/lib/src/index.js + * FIXME: backported for VuFind 5, remove with Vufind 7 + */ + var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])']; + function getFocusableNodes () { + var nodes = _modal[0].querySelectorAll(FOCUSABLE_ELEMENTS); + return Array.apply(null, nodes); + } + /** + * Tries to set focus on a node which is not a close trigger + * if no other nodes exist then focuses on first close trigger + */ + function setFocusToFirstNode() { + var focusableNodes = getFocusableNodes(); + + // no focusable nodes + if (focusableNodes.length === 0) return; + + // remove nodes on whose click, the modal closes + var nodesWhichAreNotCloseTargets = focusableNodes.filter(function(node) { + return !node.hasAttribute("data-lightbox-close") && ( + !node.hasAttribute("data-dismiss") || + node.getAttribute("data-dismiss") != "modal" + ); + }); + + if (nodesWhichAreNotCloseTargets.length > 0) nodesWhichAreNotCloseTargets[0].focus(); + if (nodesWhichAreNotCloseTargets.length === 0) focusableNodes[0].focus(); + } + + function retainFocus(event) { + var focusableNodes = getFocusableNodes(); + + // no focusable nodes + if (focusableNodes.length === 0) return; + + /** + * Filters nodes which are hidden to prevent + * focus leak outside modal + */ + focusableNodes = focusableNodes.filter(function(node) { + return (node.offsetParent !== null); + }); + + // if disableFocus is true + if (!_modal[0].contains(document.activeElement)) { + focusableNodes[0].focus(); + } else { + var focusedItemIndex = focusableNodes.indexOf(document.activeElement); + + if (event.shiftKey && focusedItemIndex === 0) { + focusableNodes[focusableNodes.length - 1].focus(); + event.preventDefault(); + } + + if (!event.shiftKey && focusableNodes.length > 0 && focusedItemIndex === focusableNodes.length - 1) { + focusableNodes[0].focus(); + event.preventDefault(); + } + } + } + function onKeydown(e) { + if (event.keyCode === 27) { // esc + close(); + } + if (event.keyCode === 9) { // tab + retainFocus(event); + } + } + function bindFocus() { + document.addEventListener('keydown', onKeydown); + setFocusToFirstNode(); + } + function unbindFocus() { + document.removeEventListener('keydown', onKeydown); + } + // Public: Attach listeners to the page function bind(el) { var target = el || document; @@ -389,24 +470,28 @@ VuFind.register('lightbox', function Lightbox() { _html(VuFind.translate('loading') + '...'); _originalUrl = false; _currentUrl = false; - _lightboxTitle = ''; + _lightboxTitle = false; _modalParams = {}; } function init() { _modal = $('#modal'); _modalBody = _modal.find('.modal-body'); - _modalTitle = _modal.find("#modal-title"); _modal.on('hide.bs.modal', function lightboxHide() { if (VuFind.lightbox.refreshOnClose) { VuFind.refreshPage(); + } else { + unbindFocus(); + this.setAttribute('aria-hidden', true); + _emit('VuFind.lightbox.closing'); } - this.setAttribute('aria-hidden', true); - _emit('VuFind.lightbox.closing'); }); _modal.on('hidden.bs.modal', function lightboxHidden() { VuFind.lightbox.reset(); _emit('VuFind.lightbox.closed'); }); + _modal.on("shown.bs.modal", function lightboxShown() { + bindFocus(); + }); VuFind.modal = function modalShortcut(cmd) { if (cmd === 'show') { -- GitLab