diff --git a/resources/js/ext.uls.dialog.js b/resources/js/ext.uls.dialog.js index 1b52102c..013a99c2 100644 --- a/resources/js/ext.uls.dialog.js +++ b/resources/js/ext.uls.dialog.js @@ -1,10 +1,141 @@ +/*! + * A simple dialog to be used inside ULS. + * + * @private + * @since 2020.01 + * + * Copyright (C) 2019-2020 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris, + * Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland and other + * contributors. See CREDITS for a list. + * + * UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't + * have to do anything special to choose one license or the other and you don't + * have to notify anyone which license you are using. You are free to use + * UniversalLanguageSelector in commercial projects as long as the copyright + * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. + * + * @file + * @ingroup Extensions + * @licence GNU General Public Licence 2.0 or later + * @licence MIT License + */ + ( function () { 'use strict'; var ULSDialog = function ( options ) { var $dialog = options.container, hasOverlay = options.hasOverlay, - $overlay; + $overlay, + // Source: https://github.com/ghosh/Micromodal/blob/master/lib/src/index.js#L4 + FOCUSABLE_NODES = [ + '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() { + return $dialog.find( FOCUSABLE_NODES.join( ', ' ) ); + } + + function isElementInDialog( targetElement ) { + return $dialog.get( 0 ).contains( targetElement ); + } + + function focusOverlay() { + if ( $overlay ) { + $overlay.get( 0 ).focus(); + } + } + + function focusFirstNodeOrOverlay( $focusableNodes ) { + if ( $focusableNodes === undefined ) { + $focusableNodes = getFocusableNodes(); + } + + if ( $focusableNodes.length ) { + $focusableNodes.get( 0 ).focus(); + } else { + focusOverlay(); + } + } + + function maintainFocus( event ) { + var $focusableNodes = getFocusableNodes(), + focusedItemIndex; + + if ( !hasOverlay ) { + // overlay is not present, so let tabbing flow as normal. + return; + } + + if ( !$focusableNodes.length ) { + // no focusable node in the dialog, focus on the overlay. + focusOverlay(); + return; + } + + if ( !isElementInDialog( document.activeElement ) ) { + focusFirstNodeOrOverlay( $focusableNodes ); + } else { + focusedItemIndex = $focusableNodes.index( document.activeElement ); + + if ( event.shiftKey && focusedItemIndex === 0 ) { + $focusableNodes.get( -1 ).focus(); + event.preventDefault(); + } else if ( !event.shiftKey && focusedItemIndex === $focusableNodes.length - 1 ) { + focusFirstNodeOrOverlay( $focusableNodes ); + event.preventDefault(); + } + } + } + + function handleFirstFocus( event ) { + if ( !hasOverlay ) { + // Overlay is not present, so let tabbing flow as normal. + return; + } + + if ( isElementInDialog( event.target ) ) { + return; + } + + focusFirstNodeOrOverlay(); + } + + function onKeydown( event ) { + switch ( event.key ) { + case 'Esc': + case 'Escape': + // eslint-disable-next-line no-use-before-define + close(); + event.preventDefault(); + break; + case 'Tab': + maintainFocus( event ); + break; + } + } + + function addEvents() { + $( document ) + .on( 'keydown', onKeydown ) + .on( 'focusin', handleFirstFocus ); + } + + function removeEvents() { + $( document ) + .off( 'keydown', onKeydown ) + .off( 'focusin', handleFirstFocus ); + } function showOverlay() { if ( $overlay ) { @@ -22,11 +153,14 @@ function open() { $dialog.show(); + addEvents(); showOverlay(); + focusFirstNodeOrOverlay(); } function close() { $dialog.hide(); + removeEvents(); hideOverlay(); } @@ -39,6 +173,7 @@ if ( !$overlay ) { $overlay = $( '
' ) .addClass( 'uls-overlay' ) + .prop( 'tabindex', '-1' ) .on( 'click', close ) .appendTo( document.body ); } diff --git a/resources/js/ext.uls.setlang.js b/resources/js/ext.uls.setlang.js index 5f887718..82f54873 100644 --- a/resources/js/ext.uls.setlang.js +++ b/resources/js/ext.uls.setlang.js @@ -1,3 +1,25 @@ +/*! + * Loaded when setlang query paramter is set on the page. + * + * @private + * @since 2020.01 + * + * Copyright (C) 2019-2020 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris, + * Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland and other + * contributors. See CREDITS for a list. + * + * UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't + * have to do anything special to choose one license or the other and you don't + * have to notify anyone which license you are using. You are free to use + * UniversalLanguageSelector in commercial projects as long as the copyright + * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. + * + * @file + * @ingroup Extensions + * @licence GNU General Public Licence 2.0 or later + * @licence MIT License + */ + ( function () { 'use strict'; var $cancelBtn, $acceptBtn;