Issue #1913086 by jessebeach, Wim Leers, mgifford, nod_: Generalize the overlay tabbing management into a utility library.
parent
e16a4241c5
commit
17ad22fd27
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* @file
|
||||
* Manages page tabbing modifications made by modules.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// Do not process the window of the overlay.
|
||||
if (parent.Drupal.overlay && parent.Drupal.overlay.iframeWindow === window) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provides an API for managing page tabbing order modifications.
|
||||
*/
|
||||
function TabbingManager () {
|
||||
// Tabbing sets are stored as a stack. The active set is at the top of the
|
||||
// stack. We use a JavaScript array as if it were a stack; we consider the
|
||||
// first element to be the bottom and the last element to be the top. This
|
||||
// allows us to use JavaScript's built-in Array.push() and Array.pop()
|
||||
// methods.
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingManager class.
|
||||
*/
|
||||
$.extend(TabbingManager.prototype, {
|
||||
/**
|
||||
* Constrain tabbing to the specified set of elements only.
|
||||
*
|
||||
* Makes elements outside of the specified set of elements unreachable via the
|
||||
* tab key.
|
||||
*
|
||||
* @param jQuery elements
|
||||
* The set of elements to which tabbing should be constrained. Can also be
|
||||
* a jQuery-compatible selector string.
|
||||
*
|
||||
* @return TabbingContext
|
||||
*/
|
||||
constrain: function (elements) {
|
||||
// Deactivate all tabbingContexts to prepare for the new constraint. A
|
||||
// tabbingContext instance will only be reactivated if the stack is unwound
|
||||
// to it in the _unwindStack() method.
|
||||
for (var i = 0, il = this.stack.length; i < il; i++) {
|
||||
this.stack[i].deactivate();
|
||||
}
|
||||
|
||||
// The "active tabbing set" are the elements tabbing should be constrained
|
||||
// to.
|
||||
var $elements = $(elements).find(':tabbable').addBack(':tabbable');
|
||||
|
||||
var tabbingContext = new TabbingContext({
|
||||
// The level is the current height of the stack before this new
|
||||
// tabbingContext is pushed on top of the stack.
|
||||
level: this.stack.length,
|
||||
$tabbableElements: $elements
|
||||
});
|
||||
|
||||
this.stack.push(tabbingContext);
|
||||
|
||||
// Activates the tabbingContext; this will manipulate the DOM to constrain
|
||||
// tabbing.
|
||||
tabbingContext.activate();
|
||||
|
||||
// Allow modules to respond to the constrain event.
|
||||
$(document).trigger('drupalTabbingConstrained', tabbingContext);
|
||||
|
||||
return tabbingContext;
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores a former tabbingContext when an active tabbingContext is released.
|
||||
*
|
||||
* The TabbingManager stack of tabbingContext instances will be unwound from
|
||||
* the top-most released tabbingContext down to the first non-released
|
||||
* tabbingContext instance. This non-released instance is then activated.
|
||||
*/
|
||||
release: function () {
|
||||
// Unwind as far as possible: find the topmost non-released tabbingContext.
|
||||
var toActivate = this.stack.length - 1;
|
||||
while (toActivate >= 0 && this.stack[toActivate].released) {
|
||||
toActivate--;
|
||||
}
|
||||
|
||||
// Delete all tabbingContexts after the to be activated one. They have
|
||||
// already been deactivated, so their effect on the DOM has been reversed.
|
||||
this.stack.splice(toActivate + 1);
|
||||
|
||||
// Get topmost tabbingContext, if one exists, and activate it.
|
||||
if (toActivate >= 0) {
|
||||
this.stack[toActivate].activate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes all elements outside the of the tabbingContext's set untabbable.
|
||||
*
|
||||
* Elements made untabble have their original tabindex and autfocus values
|
||||
* stored so that they might be restored later when this tabbingContext
|
||||
* is deactivated.
|
||||
*
|
||||
* @param TabbingContext tabbingContext
|
||||
* The TabbingContext instance that has been activated.
|
||||
*/
|
||||
activate: function (tabbingContext) {
|
||||
var $set = tabbingContext.$tabbableElements;
|
||||
var level = tabbingContext.level;
|
||||
// Determine which elements are reachable via tabbing by default.
|
||||
var $disabledSet = $(':tabbable')
|
||||
// Exclude elements of the active tabbing set.
|
||||
.not($set);
|
||||
// Set the disabled set on the tabbingContext.
|
||||
tabbingContext.$disabledElements = $disabledSet;
|
||||
// Record the tabindex for each element, so we can restore it later.
|
||||
for (var i = 0, il = $disabledSet.length; i < il; i++) {
|
||||
this.recordTabindex($disabledSet.eq(i), level);
|
||||
}
|
||||
// Make all tabbable elements outside of the active tabbing set unreachable.
|
||||
$disabledSet
|
||||
.prop('tabindex', -1)
|
||||
.prop('autofocus', false);
|
||||
|
||||
// Set focus on an element in the tabbingContext's set of tabbable elements.
|
||||
// First, check if there is an element with an autofocus attribute. Select
|
||||
// the last one from the DOM order.
|
||||
var $hasFocus = $set.filter('[autofocus]').eq(-1);
|
||||
// If no element in the tabbable set has an autofocus attribute, select the
|
||||
// first element in the set.
|
||||
if ($hasFocus.length === 0) {
|
||||
$hasFocus = $set.eq(0);
|
||||
}
|
||||
$hasFocus.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores that tabbable state of a tabbingContext's disabled elements.
|
||||
*
|
||||
* Elements that were made untabble have their original tabindex and autfocus
|
||||
* values restored.
|
||||
*
|
||||
* @param TabbingContext tabbingContext
|
||||
* The TabbingContext instance that has been deactivated.
|
||||
*/
|
||||
deactivate: function (tabbingContext) {
|
||||
var $set = tabbingContext.$disabledElements;
|
||||
var level = tabbingContext.level;
|
||||
for (var i = 0, il = $set.length; i < il; i++) {
|
||||
this.restoreTabindex($set.eq(i), level);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Records the tabindex and autofocus values of an untabbable element.
|
||||
*
|
||||
* @param jQuery $set
|
||||
* The set of elements that have been disabled.
|
||||
* @param Number level
|
||||
* The stack level for which the tabindex attribute should be recorded.
|
||||
*/
|
||||
recordTabindex: function ($el, level) {
|
||||
var tabInfo = $el.data('drupalOriginalTabIndices') || {};
|
||||
tabInfo[level] = {
|
||||
tabindex: $el[0].getAttribute('tabindex'),
|
||||
autofocus: $el[0].hasAttribute('autofocus')
|
||||
};
|
||||
$el.data('drupalOriginalTabIndices', tabInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the tabindex and autofocus values of a reactivated element.
|
||||
*
|
||||
* @param jQuery $el
|
||||
* The element that is being reactivated.
|
||||
* @param Number level
|
||||
* The stack level for which the tabindex attribute should be restored.
|
||||
*/
|
||||
restoreTabindex: function ($el, level) {
|
||||
var tabInfo = $el.data('drupalOriginalTabIndices');
|
||||
if (tabInfo && tabInfo[level]) {
|
||||
var data = tabInfo[level];
|
||||
if (data.tabindex) {
|
||||
$el[0].setAttribute('tabindex', data.tabindex);
|
||||
}
|
||||
// If the element did not have a tabindex at this stack level then
|
||||
// remove it.
|
||||
else {
|
||||
$el[0].removeAttribute('tabindex');
|
||||
}
|
||||
if (data.autofocus) {
|
||||
$el[0].setAttribute('autofocus', 'autofocus');
|
||||
}
|
||||
|
||||
// Clean up $.data.
|
||||
if (level === 0) {
|
||||
// Remove all data.
|
||||
$el.removeData('drupalOriginalTabIndices');
|
||||
}
|
||||
else {
|
||||
// Remove the data for this stack level and higher.
|
||||
var levelToDelete = level;
|
||||
while (tabInfo.hasOwnProperty(levelToDelete)) {
|
||||
delete tabInfo[levelToDelete];
|
||||
levelToDelete++;
|
||||
}
|
||||
$el.data('drupalOriginalTabIndices', tabInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Stores a set of tabbable elements.
|
||||
*
|
||||
* This constraint can be removed with the release() method.
|
||||
*
|
||||
* @param Object options
|
||||
* A set of initiating values that include:
|
||||
* - Number level: The level in the TabbingManager's stack of this
|
||||
* tabbingContext.
|
||||
* - jQuery $tabbableElements: The DOM elements that should be reachable via
|
||||
* the tab key when this tabbingContext is active.
|
||||
* - jQuery $disabledElements: The DOM elements that should not be reachable
|
||||
* via the tab key when this tabbingContext is active.
|
||||
* - Boolean released: A released tabbingContext can never be activated again.
|
||||
* It will be cleaned up when the TabbingManager unwinds its stack.
|
||||
* - Boolean active: When true, the tabbable elements of this tabbingContext
|
||||
* will be reachable via the tab key and the disabled elements will not. Only
|
||||
* one tabbingContext can be active at a time.
|
||||
*/
|
||||
function TabbingContext (options) {
|
||||
$.extend(this, {
|
||||
level: null,
|
||||
$tabbableElements: $(),
|
||||
$disabledElements: $(),
|
||||
released: false,
|
||||
active: false
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingContext class.
|
||||
*/
|
||||
$.extend(TabbingContext.prototype, {
|
||||
/**
|
||||
* Releases this TabbingContext.
|
||||
*
|
||||
* Once a TabbingContext object is released, it can never be activated again.
|
||||
*/
|
||||
release: function () {
|
||||
if (!this.released) {
|
||||
this.deactivate();
|
||||
this.released = true;
|
||||
Drupal.tabbingManager.release(this);
|
||||
// Allow modules to respond to the tabbingContext release event.
|
||||
$(document).trigger('drupalTabbingContextReleased', this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Activates this TabbingContext.
|
||||
*/
|
||||
activate: function () {
|
||||
// A released TabbingContext object can never be activated again.
|
||||
if (!this.active && !this.released) {
|
||||
this.active = true;
|
||||
Drupal.tabbingManager.activate(this);
|
||||
// Allow modules to respond to the constrain event.
|
||||
$(document).trigger('drupalTabbingContextActivated', this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deactivates this TabbingContext.
|
||||
*/
|
||||
deactivate: function () {
|
||||
if (this.active) {
|
||||
this.active = false;
|
||||
Drupal.tabbingManager.deactivate(this);
|
||||
// Allow modules to respond to the constrain event.
|
||||
$(document).trigger('drupalTabbingContextDeactivated', this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Mark this behavior as processed on the first pass and return if it is
|
||||
// already processed.
|
||||
if (Drupal.tabbingManager) {
|
||||
return;
|
||||
}
|
||||
Drupal.tabbingManager = new TabbingManager();
|
||||
|
||||
|
||||
}(jQuery, Drupal));
|
|
@ -109,6 +109,7 @@ function contextual_library_info() {
|
|||
array('system', 'jquery'),
|
||||
array('system', 'jquery.once'),
|
||||
array('system', 'backbone'),
|
||||
array('system', 'drupal.tabbingmanager'),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -16,15 +16,19 @@
|
|||
*/
|
||||
Drupal.behaviors.contextualToolbar = {
|
||||
attach: function (context) {
|
||||
var that = this;
|
||||
$('body').once('contextualToolbar-init', function () {
|
||||
var options = $.extend({}, that.defaults);
|
||||
var $contextuals = $(context).find('.contextual-links');
|
||||
var $tab = $('.js .toolbar .bar .contextual-toolbar-tab');
|
||||
var model = new Drupal.contextualToolbar.models.EditToggleModel({
|
||||
isViewing: true
|
||||
isViewing: true,
|
||||
contextuals: $contextuals.get()
|
||||
});
|
||||
var view = new Drupal.contextualToolbar.views.EditToggleView({
|
||||
el: $tab,
|
||||
model: model
|
||||
model: model,
|
||||
strings: options.strings
|
||||
});
|
||||
|
||||
// Update the model based on overlay events.
|
||||
|
@ -53,6 +57,18 @@ Drupal.behaviors.contextualToolbar = {
|
|||
model.set('isViewing', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
defaults: {
|
||||
strings: {
|
||||
tabbingReleased: Drupal.t('Tabbing is no longer constrained by the Contextual module'),
|
||||
tabbingConstrained: Drupal.t('Tabbing is constrained to a set of @contextualsCount and the Edit mode toggle'),
|
||||
pressEsc: Drupal.t('Press the esc key to exit.'),
|
||||
contextualsCount: {
|
||||
singular: '@count contextual link',
|
||||
plural: '@count contextual links'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -66,7 +82,12 @@ Drupal.contextualToolbar.models.EditToggleModel = Backbone.Model.extend({
|
|||
// Indicates whether the toggle is currently in "view" or "edit" mode.
|
||||
isViewing: true,
|
||||
// Indicates whether the toggle should be visible or hidden.
|
||||
isVisible: false
|
||||
isVisible: false,
|
||||
// The set of elements that can be reached via the tab key when edit mode
|
||||
// is enabled.
|
||||
tabbingContext: null,
|
||||
// The set of contextual links stored as an Array.
|
||||
contextuals: []
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -77,12 +98,22 @@ Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({
|
|||
|
||||
events: { 'click': 'onClick' },
|
||||
|
||||
// Tracks whether the tabbing constraint announcement has been read once yet.
|
||||
announcedOnce: false,
|
||||
|
||||
/**
|
||||
* Implements Backbone Views' initialize().
|
||||
*/
|
||||
initialize: function () {
|
||||
|
||||
this.strings = this.options.strings;
|
||||
|
||||
this.model.on('change', this.render, this);
|
||||
this.model.on('change:isViewing', this.persist, this);
|
||||
this.model.on('change:isViewing', this.manageTabbing, this);
|
||||
|
||||
$(document)
|
||||
.on('keyup', $.proxy(this.onKeypress, this));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -97,11 +128,32 @@ Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({
|
|||
var isViewing = this.model.get('isViewing');
|
||||
this.$el.find('button')
|
||||
.toggleClass('active', !isViewing)
|
||||
.prop('aria-pressed', !isViewing);
|
||||
.attr('aria-pressed', !isViewing);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Limits tabbing to the contextual links and edit mode toolbar tab.
|
||||
*
|
||||
* @param Drupal.contextualToolbar.models.EditToggleModel model
|
||||
* An EditToggleModel Backbone model.
|
||||
* @param bool isViewing
|
||||
* The value of the isViewing attribute in the model.
|
||||
*/
|
||||
manageTabbing: function (model, isViewing) {
|
||||
var tabbingContext = this.model.get('tabbingContext');
|
||||
// Always release an existing tabbing context.
|
||||
if (tabbingContext) {
|
||||
tabbingContext.release();
|
||||
}
|
||||
// Create a new tabbing context when edit mode is enabled.
|
||||
if (!isViewing) {
|
||||
tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual'));
|
||||
this.model.set('tabbingContext', tabbingContext);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Model change handler; persists the isViewing value to localStorage.
|
||||
*
|
||||
|
@ -122,10 +174,55 @@ Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Passes state update messsages to Drupal.announce.
|
||||
*/
|
||||
announceTabbingConstraint: function () {
|
||||
var isViewing = this.model.get('isViewing');
|
||||
|
||||
if (!isViewing) {
|
||||
var contextuals = this.model.get('contextuals');
|
||||
Drupal.announce(Drupal.t(this.strings.tabbingConstrained, {
|
||||
'@contextualsCount': Drupal.formatPlural(contextuals.length, this.strings.contextualsCount.singular, this.strings.contextualsCount.plural)
|
||||
}));
|
||||
Drupal.announce(this.strings.pressEsc);
|
||||
}
|
||||
else {
|
||||
Drupal.announce(this.strings.tabbingReleased)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Responds to the edit mode toggle toolbar button; Toggles edit mode.
|
||||
*
|
||||
* @param jQuery.Event event
|
||||
*/
|
||||
onClick: function (event) {
|
||||
this.model.set('isViewing', !this.model.get('isViewing'));
|
||||
this.announceTabbingConstraint();
|
||||
this.announcedOnce = true;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Responds to esc and tab key press events.
|
||||
*
|
||||
* @param jQuery.Event event
|
||||
*/
|
||||
onKeypress: function (event) {
|
||||
// Respond to tab key press; Call render so the state announcement is read.
|
||||
// The first tab key press is tracked so that an annoucement about tabbing
|
||||
// constraints can be raised if edit mode is enabled when this page loads.
|
||||
if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) {
|
||||
this.announceTabbingConstraint();
|
||||
// Set announce to true so that this conditional block won't be run again.
|
||||
this.announcedOnce = true;
|
||||
}
|
||||
// Respond to the ESC key. Exit out of edit mode.
|
||||
if (event.keyCode === 27) {
|
||||
this.model.set('isViewing', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -53,10 +53,6 @@
|
|||
#overlay-title:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.overlay #skip-link {
|
||||
margin-top: -20px;
|
||||
}
|
||||
.overlay #skip-link a {
|
||||
color: #fff; /* This is white to contrast with the dark background behind it. */
|
||||
}
|
||||
|
@ -143,7 +139,7 @@
|
|||
*/
|
||||
#overlay-disable-message {
|
||||
background-color: #fff;
|
||||
margin: -20px auto 20px;
|
||||
margin: 0 auto 20px;
|
||||
width: 80%;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
|
|
@ -12,10 +12,6 @@
|
|||
*/
|
||||
Drupal.behaviors.overlayParent = {
|
||||
attach: function (context, settings) {
|
||||
if (Drupal.overlay.isOpen) {
|
||||
Drupal.overlay.makeDocumentUntabbable(context);
|
||||
}
|
||||
|
||||
if (this.processed) {
|
||||
return;
|
||||
}
|
||||
|
@ -94,7 +90,6 @@ Drupal.overlay.open = function (url) {
|
|||
this.isOpening = false;
|
||||
this.isOpen = true;
|
||||
$(document.documentElement).addClass('overlay-open');
|
||||
this.makeDocumentUntabbable();
|
||||
|
||||
// Allow other scripts to respond to this event.
|
||||
$(document).trigger('drupalOverlayOpen');
|
||||
|
@ -204,11 +199,12 @@ Drupal.overlay.close = function () {
|
|||
$(document.documentElement).removeClass('overlay-open');
|
||||
// Restore the original document title.
|
||||
document.title = this.originalTitle;
|
||||
this.makeDocumentTabbable();
|
||||
|
||||
// Allow other scripts to respond to this event.
|
||||
$(document).trigger('drupalOverlayClose');
|
||||
|
||||
Drupal.announce(Drupal.t('Tabbing is no longer constrained by the Overlay module.'));
|
||||
|
||||
// When the iframe is still loading don't destroy it immediately but after
|
||||
// the content is loaded (see Drupal.overlay.loadChild).
|
||||
if (!this.isLoading) {
|
||||
|
@ -301,15 +297,21 @@ Drupal.overlay.loadChild = function (event) {
|
|||
.attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() })).prop('tabindex', false);
|
||||
this.inactiveFrame = event.data.sibling;
|
||||
|
||||
Drupal.announce(Drupal.t('The overlay has been opened to @title', {'@title': iframeWindow.jQuery('#overlay-title').text()}));
|
||||
|
||||
// Load an empty document into the inactive iframe.
|
||||
(this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank');
|
||||
|
||||
// Move the focus to just before the "skip to main content" link inside
|
||||
// the overlay.
|
||||
this.activeFrame.focus();
|
||||
var skipLink = iframeWindow.jQuery('a:first');
|
||||
// Create a fake link before the skip link in order to give it focus.
|
||||
var skipLink = iframeWindow.jQuery('[href="#main-content"]');
|
||||
Drupal.overlay.setFocusBefore(skipLink, iframeWindow.document);
|
||||
|
||||
// Report these tabbables to the TabbingManger in the parent window.
|
||||
Drupal.overlay.releaseTabbing();
|
||||
Drupal.overlay.constrainTabbing($(iframeWindow.document).add('#toolbar-administration'));
|
||||
|
||||
Drupal.announce(Drupal.t('Tabbing is constrained to items in the administrative toolbar and the overlay.'));
|
||||
|
||||
// Allow other scripts to respond to this event.
|
||||
$(document).trigger('drupalOverlayLoad');
|
||||
}
|
||||
|
@ -343,7 +345,7 @@ Drupal.overlay.setFocusBefore = function ($element, document) {
|
|||
var $placeholder = $(placeholder).addClass('element-invisible').attr('href', '#');
|
||||
// Put the placeholder where it belongs, and set the document focus to it.
|
||||
$placeholder.insertBefore($element);
|
||||
$placeholder.focus();
|
||||
$placeholder.attr('autofocus', true);
|
||||
// Make the placeholder disappear as soon as it loses focus, so that it
|
||||
// doesn't appear in the tab order again.
|
||||
$placeholder.one('blur', function () {
|
||||
|
@ -833,80 +835,29 @@ Drupal.overlay.getPath = function (link) {
|
|||
|
||||
/**
|
||||
* Makes elements outside the overlay unreachable via the tab key.
|
||||
*
|
||||
* @param context
|
||||
* The part of the DOM that should have its tabindexes changed. Defaults to
|
||||
* the entire page.
|
||||
*/
|
||||
Drupal.overlay.makeDocumentUntabbable = function (context) {
|
||||
context = context || document.body;
|
||||
var $overlay, $tabbable, $hasTabindex;
|
||||
|
||||
// Determine which elements on the page already have a tabindex.
|
||||
$hasTabindex = $('[tabindex] :not(.overlay-element)', context);
|
||||
// Record the tabindex for each element, so we can restore it later.
|
||||
$hasTabindex.each(Drupal.overlay._recordTabindex);
|
||||
// Add the tabbable elements from the current context to any that we might
|
||||
// have previously recorded.
|
||||
Drupal.overlay._hasTabindex = $hasTabindex.add(Drupal.overlay._hasTabindex);
|
||||
|
||||
// Set tabindex to -1 on everything outside the overlay and toolbars, so that
|
||||
// the underlying page is unreachable.
|
||||
|
||||
// By default, browsers make a, area, button, input, object, select, textarea,
|
||||
// and iframe elements reachable via the tab key.
|
||||
$tabbable = $('a, area, button, input, object, select, textarea, iframe');
|
||||
// If another element (like a div) has a tabindex, it's also tabbable.
|
||||
$tabbable = $tabbable.add($hasTabindex);
|
||||
Drupal.overlay.constrainTabbing = function ($tabbables) {
|
||||
// If a tabset is already active, return without creating a new one.
|
||||
if (this.tabset && !this.tabset.released) {
|
||||
return;
|
||||
}
|
||||
// Leave links inside the overlay and toolbars alone.
|
||||
$overlay = $('.overlay-element, #overlay-container, #toolbar-administration').find('*');
|
||||
$tabbable = $tabbable.not($overlay);
|
||||
// We now have a list of everything in the underlying document that could
|
||||
// possibly be reachable via the tab key. Make it all unreachable.
|
||||
$tabbable.prop('tabindex', -1);
|
||||
this.tabset = Drupal.tabbingManager.constrain($tabbables);
|
||||
var self = this;
|
||||
$(document).on('drupalOverlayClose.tabbing', function () {
|
||||
self.tabset.release();
|
||||
$(document).off('drupalOverlayClose.tabbing');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores the original tabindex value of a group of elements.
|
||||
*
|
||||
* @param context
|
||||
* The part of the DOM that should have its tabindexes restored. Defaults to
|
||||
* the entire page.
|
||||
*/
|
||||
Drupal.overlay.makeDocumentTabbable = function (context) {
|
||||
var $needsTabindex;
|
||||
context = context || document.body;
|
||||
|
||||
// Make the underlying document tabbable again by removing all existing
|
||||
// tabindex attributes.
|
||||
$(context).find('[tabindex]').prop('tabindex', false);
|
||||
|
||||
// Restore the tabindex attributes that existed before the overlay was opened.
|
||||
$needsTabindex = $(Drupal.overlay._hasTabindex, context);
|
||||
$needsTabindex.each(Drupal.overlay._restoreTabindex);
|
||||
Drupal.overlay._hasTabindex = Drupal.overlay._hasTabindex.not($needsTabindex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Record the tabindex for an element, using $.data.
|
||||
*
|
||||
* Meant to be used as a jQuery.fn.each callback.
|
||||
*/
|
||||
Drupal.overlay._recordTabindex = function () {
|
||||
var $element = $(this);
|
||||
var tabindex = $(this).prop('tabindex');
|
||||
$element.data('drupalOverlayOriginalTabIndex', tabindex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Restore an element's original tabindex.
|
||||
*
|
||||
* Meant to be used as a jQuery.fn.each callback.
|
||||
*/
|
||||
Drupal.overlay._restoreTabindex = function () {
|
||||
var $element = $(this);
|
||||
var tabindex = $element.data('drupalOverlayOriginalTabIndex');
|
||||
$element.prop('tabindex', tabindex);
|
||||
Drupal.overlay.releaseTabbing = function () {
|
||||
if (this.tabset) {
|
||||
this.tabset.release();
|
||||
delete this.tabset;
|
||||
}
|
||||
};
|
||||
|
||||
$.extend(Drupal.theme, {
|
||||
|
|
|
@ -226,6 +226,7 @@ function overlay_library_info() {
|
|||
array('system', 'drupal'),
|
||||
array('system', 'drupalSettings'),
|
||||
array('system', 'drupal.displace'),
|
||||
array('system', 'drupal.tabbingmanager'),
|
||||
array('system', 'jquery.ui.core'),
|
||||
array('system', 'jquery.bbq'),
|
||||
),
|
||||
|
|
|
@ -1417,6 +1417,21 @@ function system_library_info() {
|
|||
),
|
||||
);
|
||||
|
||||
// Manages tab orders in the document.
|
||||
$libraries['drupal.tabbingmanager'] = array(
|
||||
'title' => 'Drupal tabbing manager',
|
||||
'version' => VERSION,
|
||||
'js' => array(
|
||||
'core/misc/tabbingmanager.js' => array('group', JS_LIBRARY),
|
||||
),
|
||||
'dependencies' => array(
|
||||
array('system', 'jquery'),
|
||||
// Depends on jQuery UI Core to use the ":tabbable" pseudo selector.
|
||||
array('system', 'jquery.ui.core'),
|
||||
array('system', 'drupal'),
|
||||
),
|
||||
);
|
||||
|
||||
// A utility function to limit calls to a function with a given time.
|
||||
$libraries['drupal.debounce'] = array(
|
||||
'title' => 'Drupal debounce',
|
||||
|
|
Loading…
Reference in New Issue