Index: modules/overlay/overlay-parent.js
===================================================================
RCS file: /cvs/drupal/drupal/modules/overlay/overlay-parent.js,v
retrieving revision 1.54
diff -u -r1.54 overlay-parent.js
--- modules/overlay/overlay-parent.js	18 Aug 2010 02:25:50 -0000	1.54
+++ modules/overlay/overlay-parent.js	18 Sep 2010 11:50:46 -0000
@@ -7,6 +7,10 @@
  */
 Drupal.behaviors.overlayParent = {
   attach: function (context, settings) {
+    if (Drupal.overlay.isOpen) {
+      Drupal.overlay.applyARIAPresentation(context);
+    }
+
     if (this.processed) {
       return;
     }
@@ -50,7 +54,11 @@
   isOpen: false,
   isOpening: false,
   isClosing: false,
-  isLoading: false
+  isLoading: false,
+  // Properties used to determine ARIA presentation role.
+  _linkables: {'a': true, 'area': true},
+  _formElements: {'input': true, 'select': true, 'textarea': true, 'button': true, 'object': true},
+  _blocks: {'address': true, 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, 'p': true, 'li': true, 'td': true, 'th': true}
 };
 
 Drupal.overlay.prototype = {};
@@ -79,6 +87,8 @@
   this.isOpening = false;
   this.isOpen = true;
   $(document.documentElement).addClass('overlay-open');
+  
+  $('#skip-link a').attr('href', '#overlay-container');
 
   // Allow other scripts to respond to this event.
   $(document).trigger('drupalOverlayOpen');
@@ -90,8 +100,23 @@
  * Create the underlying markup and behaviors for the overlay.
  */
 Drupal.overlay.create = function () {
-  this.$container = $(Drupal.theme('overlayContainer'))
-    .appendTo(document.body);
+  var self = this;
+  this.$context = $('<div id="overlay-context" tabindex="0" role="img" aria-disabled="true" aria-labelledby="overlay-context-label"></div>');
+  if ($('.region-page-top').length) {
+    $('.region-page-top').nextUntil('.region-page-bottom').wrapAll(this.$context);
+  }
+  else if ($('#skip-link').length) {
+    $('#skip-link').nextUntil('.region-page-bottom').wrapAll(this.$context);
+  }
+  else {
+    $(document.body).wrapInner(this.$context);
+  }
+  this.$context = $('#overlay-context')
+    .delegate('*', 'click keypress keydown keyup focus', $.proxy(this, 'eventhandlerContextAccess'))
+    .prepend('<div id="overlay-context-label" class="element-invisible">' + Drupal.t('This is the underlying page beneath the overlay and is currently unaccessible. Close the overlay to regain access.') + '</div>');
+  this.applyARIAPresentation(this.$context[0]);
+
+  this.$container = $(Drupal.theme('overlayContainer')).insertBefore(this.$context);
 
   // Overlay uses transparent iframes that cover the full parent window.
   // When the overlay is open the scrollbar of the parent window is hidden.
@@ -101,9 +126,11 @@
   // background. When the page is loaded the active and inactive iframes
   // are switched.
   this.activeFrame = this.$iframeA = $(Drupal.theme('overlayElement'))
+    .attr({ 'tabindex': -1, 'role': 'presentation' })
     .appendTo(this.$container);
 
   this.inactiveFrame = this.$iframeB = $(Drupal.theme('overlayElement'))
+    .attr({ 'tabindex': -1, 'role': 'presentation' })
     .appendTo(this.$container);
 
   this.$iframeA.bind('load.drupal-overlay', { self: this.$iframeA[0], sibling: this.$iframeB }, $.proxy(this, 'loadChild'));
@@ -121,8 +148,7 @@
     .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRefreshPage'))
     .bind('drupalOverlayBeforeClose' + eventClass +
           ' drupalOverlayBeforeLoad' + eventClass +
-          ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'))
-    .bind('keydown' + eventClass, $.proxy(this, 'eventhandlerRestrictKeyboardNavigation'));
+          ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'));
 
   if ($('.overlay-displace-top, .overlay-displace-bottom').length) {
     $(document)
@@ -189,6 +215,14 @@
   // Restore the original document title.
   document.title = this.originalTitle;
 
+  $('#skip-link a').attr('href', '#main-content');
+
+  // Move focus to the element that was active right before opening the overlay.
+  if (this.lastActiveElement) {
+    $(this.lastActiveElement).focus();
+    this.lastActiveElement = null;
+  }
+  
   // Allow other scripts to respond to this event.
   $(document).trigger('drupalOverlayClose');
 
@@ -206,10 +240,13 @@
  */
 Drupal.overlay.destroy = function () {
   $([document, window]).unbind('.drupal-overlay-open');
-  this.$iframeA.unbind('.drupal-overlay');
-  this.$iframeB.unbind('.drupal-overlay');
-  this.$container.remove();
+  this.$iframeA.unbind('.drupal-overlay').remove();
+  this.$iframeB.unbind('.drupal-overlay').remove();
+  this.$context.find('#overlay-context-label').remove();
+  this.$context.replaceWith(this.$context.contents());
+  this.$container.remove(); 
 
+  this.$context = null;
   this.$container = null;
   this.$iframeA = null;
   this.$iframeB = null;
@@ -271,7 +308,7 @@
 
   this.isLoading = false;
   $(document.documentElement).removeClass('overlay-loading');
-  event.data.sibling.removeClass('overlay-active');
+  event.data.sibling.removeClass('overlay-active').attr({ 'tabindex': -1 });
 
   // Only continue when overlay is still open and not closing.
   if (this.isOpen && !this.isClosing) {
@@ -280,10 +317,12 @@
       // Replace the document title with title of iframe.
       document.title = iframeWindow.document.title;
 
-      this.activeFrame = $(iframe)
-        .addClass('overlay-active')
-        // Add a title attribute to the iframe for accessibility.
-        .attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() }));
+      var overlayTitle = iframeWindow.jQuery('#overlay-title').text();
+      // Add a label to the dialog (container has the dialog role).
+      this.$container.attr('aria-label', overlayTitle);
+
+      this.activeFrame = $(iframe).addClass('overlay-active').attr({ 'tabindex': 0 });
+
       this.inactiveFrame = event.data.sibling;
 
       // Load an empty document into the inactive iframe.
@@ -291,6 +330,10 @@
 
       // Allow other scripts to respond to this event.
       $(document).trigger('drupalOverlayLoad');
+
+      // Set focus to the first tab focusable element within the dialog.
+      // http://www.w3.org/WAI/PF/aria-practices/#modal_dialog
+      this.activeFrame.focus();
     }
     else {
       window.location = iframeWindow.location.href.replace(/([?&]?)render=overlay&?/g, '$1').replace(/\?$/, '');
@@ -463,10 +506,17 @@
     return;
   }
 
+  var target = $target[0];
+  var href = target.href;
+  
   // Close the overlay when the link contains the overlay-close class.
   if ($target.hasClass('overlay-close')) {
-    // Clearing the overlay URL fragment will close the overlay.
-    $.bbq.removeState('overlay');
+    $target.attr({ 'href': window.location.href.replace(window.location.hash, ''), 'target': '_parent' });
+    if (event.button == 0 && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
+      // Clearing the overlay URL fragment will close the overlay.
+      $.bbq.removeState('overlay');
+      event.preventDefault();
+    }
     return;
   }
 
@@ -474,9 +524,23 @@
   var href = target.href;
   // Only handle links that have an href attribute and use the http(s) protocol.
   if (href != undefined && href != '' && target.protocol.match(/^https?\:/)) {
-    var anchor = href.replace(target.ownerDocument.location.href, '');
-    // Skip anchor links.
-    if (anchor.length == 0 || anchor.charAt(0) == '#') {
+    var anchor = href.replace(target.ownerDocument.location.href.replace(target.ownerDocument.location.hash, ''), '');
+    // Skip links identical to current location.
+    if (!anchor.length) {
+      return;
+    }
+    else if (anchor.charAt(0) == '#') {
+      // Mimic default anchor handling but don't change document's location.
+      if (this.isOpen && target.ownerDocument === document) {
+        $anchor = $(anchor);
+        if ($anchor.length && $anchor.closest('#overlay-container, .overlay-displace-top, .overlay-displace-bottom').length) {
+          if ($anchor.is(':focusable')) {
+            $anchor.focus();
+          }
+          $(window).scrollTop($anchor.position().top);
+        }
+        event.preventDefault();
+      }
       return;
     }
     // Open admin links in the overlay.
@@ -491,6 +555,8 @@
       // pressing the ALT, CTRL, META (Command key on the Macintosh keyboard)
       // or SHIFT key.
       if (event.button == 0 && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
+        // Store the clicked link as the last active element.
+        this.lastActiveElement = target;
         // Redirect to a fragmentized href. This will trigger a hashchange event.
         this.redirect(href);
         // Prevent default action and further propagation of the event.
@@ -625,37 +691,6 @@
 };
 
 /**
- * Event handler: makes sure that when the overlay is open no elements (except
- * for elements inside any displaced elements) of the parent document are
- * reachable through keyboard (TAB) navigation.
- *
- * @param event
- *   Event being triggered, with the following restrictions:
- *   - event.type: keydown
- *   - event.currentTarget: document
- */
-Drupal.overlay.eventhandlerRestrictKeyboardNavigation = function (event) {
-  if (!this.$tabbables) {
-    this.$tabbables = $(':tabbable');
-  }
-
-  if (event.keyCode && event.keyCode == $.ui.keyCode.TAB) {
-    // Whenever the focus is not inside the overlay (or a displaced element)
-    // move the focus along until it is.
-    var direction = event.shiftKey ? -1 : 1;
-    var current = this.$tabbables.index(event.target);
-    var $allowedParent = '#overlay-container, .overlay-displace-top, .overlay-displace-bottom';
-    if (current != -1 && this.$tabbables[current + direction] && !this.$tabbables.eq(current + direction).closest($allowedParent).length) {
-      while (this.$tabbables[current + direction] && !this.$tabbables.eq(current + direction).closest($allowedParent).length) {
-        current = current + direction;
-      }
-      // Move focus.
-      this.$tabbables.eq(current).focus();
-    }
-  }
-};
-
-/**
  * Event handler: dispatches events to the overlay document.
  *
  * @param event
@@ -813,17 +848,120 @@
 };
 
 /**
+ * Applies the ARIA presentation role to a set of elements.
+ *
+ * @param context
+ *   A DOM element or jQuery object to which to apply the presentation role.
+ *   Defaults to the document body.
+ */
+Drupal.overlay.applyARIAPresentation = function (context) {
+  context = $(context || document.body);
+  context.each(function () {
+    if (context && $(this).closest('#overlay-container, .overlay-displace-top, .overlay-displace-bottom').length) {
+      return;
+    }
+
+    for (var i = 0; i < this.childNodes.length; i++) {
+      if (this.childNodes[i].nodeType === 1) {
+        Drupal.overlay._applyARIAPresentation(this.childNodes[i]);
+      }
+    }
+  });
+};
+
+/**
+ * Applies the ARIA presentation role to an element, where appropriate.
+ *
+ * @param element
+ *   The DOM element to which the presentation role should be applied. Note that
+ *   this function is recursive, so the element and all its children will
+ *   be examined to determine whether they should receive the presentation role.
+ */
+Drupal.overlay._applyARIAPresentation = function (element) {
+  var self = Drupal.overlay;
+
+  var nodeName = element.nodeName.toLowerCase();
+  var tabIndex = element.getAttribute('tabIndex');
+  var tabbable = (tabIndex !== null && tabIndex >= 0)
+      || (self._linkables[nodeName] && element.href)
+      || (self._formElements[nodeName] && !element.disabled);
+  if (tabbable) {
+    element.setAttribute('tabIndex', -1);
+  }
+
+  var role = element.getAttribute('role');
+  element.setAttribute('role', 'presentation');
+
+  if (element.childNodes) {
+    for (var i = 0; i < element.childNodes.length; i++) {
+      if (element.childNodes[i].nodeType === 1) {
+        Drupal.overlay._applyARIAPresentation(element.childNodes[i]);
+      }
+    }
+  }
+
+  // Only continue if this element hasn't been processed before.
+  if (tabIndex !== '-1' && role !== 'presentation') {
+    // Restore attributes when the overlay closes.
+    $(document).bind('drupalOverlayClose.drupal-overlay-open', {'tabbable': tabbable, 'tabIndex': tabIndex, 'element': element, 'role': role }, Drupal.overlay.eventhandlerRestoreContext);
+  }
+};
+
+/**
+ * Event handler; restores an element's original ARIA role on overlay close.
+ */
+Drupal.overlay.eventhandlerRestoreContext = function (event) {
+  var tabbable = event.data.tabbable,
+      tabIndex = event.data.tabIndex,
+      element = event.data.element,
+      role = event.data.role;
+      
+  if (tabbable) {
+    if (tabIndex !== null) {
+      element.setAttribute('tabIndex', tabIndex);
+    }
+    else {
+      element.removeAttribute('tabIndex');
+    }
+  }
+
+  if (role !== null) {
+    element.setAttribute('role', tabIndex);
+  }
+  else {
+    element.removeAttribute('role');
+  }
+};
+
+/**
+ * Event handler; restrict access to overlay's context while overlay is open.
+ */
+Drupal.overlay.eventhandlerContextAccess = function (event) {
+  event.target.blur();
+  return false;
+};
+
+/**
  * Theme function to create the overlay iframe element.
  */
 Drupal.theme.prototype.overlayContainer = function () {
-  return '<div id="overlay-container" role="dialog"><div class="overlay-modal-background"></div></div>';
+  var output = '';
+  
+  output += '<div id="overlay-container" role="dialog" aria-describedby="overlay-container-tooltip">';
+  output += '<div id="overlay-container-tooltip" class="element-invisible" role="tooltip">';
+  output += Drupal.t('This dialog contains administrative pages and is displayed on top of the last non-administrative page. To close the overlay use the close button or press ESCAPE.');
+  output += '</div>';
+  output += '<div class="overlay-modal-background"></div>';
+  output += '</div>';
+  
+  return output;
 };
 
 /**
  * Theme function to create an overlay iframe element.
  */
 Drupal.theme.prototype.overlayElement = function (url) {
-  return '<iframe class="overlay-element" frameborder="0" scrolling="auto" allowtransparency="true" role="document"></iframe>';
+  return '<iframe class="overlay-element" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
 };
 
 })(jQuery);
