diff options
author | jamiewalch@google.com <jamiewalch@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-10 18:40:48 +0000 |
---|---|---|
committer | jamiewalch@google.com <jamiewalch@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-10 18:40:48 +0000 |
commit | ac4cac20044f97f9f4d2b8801be120c40d0d3f0d (patch) | |
tree | 614aed8d2b6e530f39043792d8cd67a26d9c6cd1 /remoting | |
parent | b6adedd93b1a72252cd84fd824f354130c4b694e (diff) | |
download | chromium_src-ac4cac20044f97f9f4d2b8801be120c40d0d3f0d.zip chromium_src-ac4cac20044f97f9f4d2b8801be120c40d0d3f0d.tar.gz chromium_src-ac4cac20044f97f9f4d2b8801be120c40d0d3f0d.tar.bz2 |
Constrain keyboard navigation while a modal dialog is active.
BUG=126200
TEST=Open and close various modal dialogs (IT2Me client and host, Me2Me host config) and make sure that the control tab ordering is what you would expect.
Review URL: https://chromiumcodereview.appspot.com/10384097
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@136351 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/webapp/jscompiler_hacks.js | 20 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 4 | ||||
-rw-r--r-- | remoting/webapp/ui_mode.js | 108 |
3 files changed, 117 insertions, 15 deletions
diff --git a/remoting/webapp/jscompiler_hacks.js b/remoting/webapp/jscompiler_hacks.js index 007bbb3..ff6dddc 100644 --- a/remoting/webapp/jscompiler_hacks.js +++ b/remoting/webapp/jscompiler_hacks.js @@ -44,5 +44,25 @@ HTMLEmbedElement.prototype.height; /** @type {number} */ HTMLEmbedElement.prototype.width; +/** @constructor */ +var MutationRecord = function() {}; + +/** @type {string} */ +MutationRecord.prototype.attributeName; + +/** @type {Element} */ +MutationRecord.prototype.target; + +/** @type {string} */ +MutationRecord.prototype.type; + +/** @constructor + @param {function(Array.<MutationRecord>):void} callback */ +var WebKitMutationObserver = function(callback) {}; + +/** @param {Element} element + @param {Object} options */ +WebKitMutationObserver.prototype.observe = function(element, options) {}; + // This string is replaced with the actual value in build-webapp.py. var OAUTH2_USE_OFFICIAL_CLIENT_ID = false; diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index 62d1618..21649fb 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -59,6 +59,8 @@ remoting.init = function() { window.addEventListener('paste', pluginGotPaste_, false); window.addEventListener('copy', pluginGotCopy_, false); + remoting.initModalDialogs(); + if (isHostModeSupported_()) { var noShare = document.getElementById('chrome-os-no-share'); noShare.parentNode.removeChild(noShare); @@ -293,4 +295,4 @@ function jsonParseSafe(jsonString) { } catch (err) { return undefined; } -}
\ No newline at end of file +} diff --git a/remoting/webapp/ui_mode.js b/remoting/webapp/ui_mode.js index 115d689..ea1862d 100644 --- a/remoting/webapp/ui_mode.js +++ b/remoting/webapp/ui_mode.js @@ -50,13 +50,18 @@ remoting.AppMode = { /** * @param {Element} element The element to check. - * @param {string} attr The attribute on the element to check. - * @param {string} mode The mode to check for. - * @return {boolean} True if |mode| is found within the element's |attr|. + * @param {string} attrName The attribute on the element to check. + * @param {Array.<string>} modes The modes to check for. + * @return {boolean} True if any mode in |modes| is found within the attribute. */ -remoting.hasModeAttribute = function(element, attr, mode) { - return element.getAttribute(attr).match( - new RegExp('(\\s|^)' + mode + '(\\s|$)')) != null; +remoting.hasModeAttribute = function(element, attrName, modes) { + var attr = element.getAttribute(attrName); + for (var i = 0; i < modes.length; ++i) { + if (attr.match(new RegExp('(\\s|^)' + modes[i] + '(\\s|$)')) != null) { + return true; + } + } + return false; }; /** @@ -71,17 +76,18 @@ remoting.updateModalUi = function(mode, attr) { for (var i = 1; i < modes.length; ++i) modes[i] = modes[i - 1] + '.' + modes[i]; var elements = document.querySelectorAll('[' + attr + ']'); + // Hide elements first so that we don't end up trying to show two modal + // dialogs at once (which would break keyboard-navigation confinement). for (var i = 0; i < elements.length; ++i) { var element = /** @type {Element} */ elements[i]; - var hidden = true; - for (var m = 0; m < modes.length; ++m) { - if (remoting.hasModeAttribute(element, attr, modes[m])) { - hidden = false; - break; - } + if (!remoting.hasModeAttribute(element, attr, modes)) { + element.hidden = true; } - element.hidden = hidden; - if (!hidden) { + } + for (var i = 0; i < elements.length; ++i) { + var element = /** @type {Element} */ elements[i]; + if (remoting.hasModeAttribute(element, attr, modes)) { + element.hidden = false; var autofocusNode = element.querySelector('[autofocus]'); if (autofocusNode) { autofocusNode.focus(); @@ -154,3 +160,77 @@ remoting.resetInfographics = function() { remoting.showOrHideIt2MeUi(); remoting.showOrHideMe2MeUi(); } + + +/** + * Initialize all modal dialogs (class kd-modaldialog), adding event handlers + * to confine keyboard navigation to child controls of the dialog when it is + * shown and restore keyboard navigation when it is hidden. + */ +remoting.initModalDialogs = function() { + var dialogs = document.querySelectorAll('.kd-modaldialog'); + var observer = new WebKitMutationObserver(confineOrRestoreFocus_); + var options = { + subtree: false, + attributes: true + }; + for (var i = 0; i < dialogs.length; ++i) { + observer.observe(dialogs[i], options); + } +}; + +/** + * @param {Array.<MutationRecord>} mutations The set of mutations affecting + * an observed node. + */ +function confineOrRestoreFocus_(mutations) { + // The list of mutations can include duplicates, so reduce it to a canonical + // show/hide list. + /** @type {Array.<Element>} */ + var shown = []; + /** @type {Array.<Element>} */ + var hidden = []; + for (var i = 0; i < mutations.length; ++i) { + var mutation = mutations[i]; + if (mutation.type == 'attributes' && + mutation.attributeName == 'hidden') { + var node = mutation.target; + if (node.hidden && hidden.indexOf(node) == -1) { + hidden.push(node); + } else if (!node.hidden && shown.indexOf(node) == -1) { + shown.push(node); + } + } + } + var kSavedAttributeName = 'data-saved-tab-index'; + // If any dialogs have been dismissed, restore all the tabIndex attributes. + if (hidden.length != 0) { + var elements = document.querySelectorAll('[' + kSavedAttributeName + ']'); + for (var i = 0 ; i < elements.length; ++i) { + var element = /** @type {Element} */ elements[i]; + element.tabIndex = element.getAttribute(kSavedAttributeName); + element.removeAttribute(kSavedAttributeName); + } + } + // If any dialogs have been shown, confine keyboard navigation to the first + // one. We don't have nested modal dialogs, so this will suffice for now. + if (shown.length != 0) { + var selector = '[tabIndex],a,area,button,input,select,textarea'; + var disable = document.querySelectorAll(selector); + var except = shown[0].querySelectorAll(selector); + for (var i = 0; i < disable.length; ++i) { + var element = /** @type {Element} */ disable[i]; + var removeFromKeyboardNavigation = true; + for (var j = 0; j < except.length; ++j) { // No indexOf on NodeList + if (element == except[j]) { + removeFromKeyboardNavigation = false; + break; + } + } + if (removeFromKeyboardNavigation) { + element.setAttribute(kSavedAttributeName, element.tabIndex); + element.tabIndex = -1; + } + } + } +} |