Index: modules/overlay/overlay-parent.js
===================================================================
RCS file: /cvs/drupal/drupal/modules/overlay/overlay-parent.js,v
retrieving revision 1.49
diff -u -r1.49 overlay-parent.js
--- modules/overlay/overlay-parent.js	8 Jul 2010 12:20:23 -0000	1.49
+++ modules/overlay/overlay-parent.js	20 Jul 2010 12:38:59 -0000
@@ -7,11 +7,18 @@
  */
 Drupal.behaviors.overlayParent = {
   attach: function (context, settings) {
+    if (Drupal.overlay.isOpen) {
+      Drupal.overlay.applyARIAPresentation(context);
+    }
+
     if (this.processed) {
       return;
     }
     this.processed = true;
 
+    // Initiate the overlay ARIA buffer.
+    Drupal.overlay.updateARIABuffer();
+
     $(window)
       // When the hash (URL fragment) changes, open the overlay if needed.
       .bind('hashchange.drupal-overlay', $.proxy(Drupal.overlay, 'eventhandlerOperateByURLFragment'))
@@ -77,6 +84,9 @@
   this.isOpening = false;
   this.isOpen = true;
   $(document.documentElement).addClass('overlay-open');
+  this.applyARIAPresentation();
+
+  $('#skip-link a').attr('href', '#overlay-main-content');
 
   // Allow other scripts to respond to this event.
   $(document).trigger('drupalOverlayOpen');
@@ -88,8 +98,17 @@
  * Create the underlying markup and behaviors for the overlay.
  */
 Drupal.overlay.create = function () {
-  this.$container = $(Drupal.theme('overlayContainer'))
-    .appendTo(document.body);
+  this.$container = $(Drupal.theme('overlayContainer'));
+  // Append the overlay container near the top of the page.
+  if ($('.region-page-top .overlay-displace-top').length) {
+    this.$container.insertAfter('.region-page-top');
+  }
+  else if ($('#skip-link').length) {
+    this.$container.insertAfter('#skip-link');
+  }
+  else {
+    this.$container.prependTo(document.body);
+  }
 
   // Overlay uses transparent iframes that cover the full parent window.
   // When the overlay is open the scrollbar of the parent window is hidden.
@@ -99,9 +118,11 @@
   // background. When the page is loaded the active and inactive iframes
   // are switched.
   this.activeFrame = this.$iframeA = $(Drupal.theme('overlayElement'))
+    .attr({ 'id': 'overlay-main-content', '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'));
@@ -119,8 +140,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)
@@ -156,8 +176,7 @@
   // entry using URL fragments.
   iframeDocument.location.replace(url);
 
-  // Immediately move the focus to the iframe.
-  this.inactiveFrame.focus();
+  this.updateARIABuffer();
   return true;
 };
 
@@ -185,15 +204,24 @@
   this.isOpen = false;
   $(document.documentElement).removeClass('overlay-open');
 
+  $('#skip-link a').attr('href', '#main-content');
+
   // Allow other scripts to respond to this event.
   $(document).trigger('drupalOverlayClose');
 
+  // Move focus to the element that was active right before opening the overlay.
+  if (this.lastActiveElement) {
+    $(this.lastActiveElement).focus();
+    this.lastActiveElement = null;
+  }
+
   // When the iframe is still loading don't destroy it immediately but after
   // the content is loaded (see Drupal.overlay.loadChild).
   if (!this.isLoading) {
     this.destroy();
     this.isClosing = false;
   }
+
   return true;
 };
 
@@ -202,8 +230,10 @@
  */
 Drupal.overlay.destroy = function () {
   $([document, window]).unbind('.drupal-overlay-open');
-  this.$iframeA.unbind('.drupal-overlay');
-  this.$iframeB.unbind('.drupal-overlay');
+  // Remove iframes first to make sure they are removed from the ARIA buffer;
+  // FF does not update the virtual buffer correctly.
+  this.$iframeA.unbind('.drupal-overlay').remove();
+  this.$iframeB.unbind('.drupal-overlay').remove();
   this.$container.remove();
 
   this.$container = null;
@@ -267,21 +297,33 @@
 
   this.isLoading = false;
   $(document.documentElement).removeClass('overlay-loading');
-  event.data.sibling.removeClass('overlay-active');
+  event.data.sibling.removeClass('overlay-active').removeAttr('id').attr({ 'tabindex': -1, 'role': 'presentation' });
 
   // Only continue when overlay is still open and not closing.
   if (this.isOpen && !this.isClosing) {
     // And child document is an actual overlayChild.
     if (iframeWindow.Drupal && iframeWindow.Drupal.overlayChild) {
+      var overlayTitle = Drupal.t('Overlay containing the administrative page @title', { '@title': 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')
-        // Add a title attribute to the iframe for accessibility.
-        .attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() }));
+        // Activate iframe and add a title attribute to the iframe in case the
+        // dialog role is not supported.
+        .removeAttr('role')
+        .attr({ 'id': 'overlay-main-content', 'tabindex': 0, 'title': overlayTitle });
+
       this.inactiveFrame = event.data.sibling;
 
       // Load an empty document into the inactive iframe.
       (this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank');
 
+      this.updateARIABuffer();
+      this.activeFrame.focus();
+      iframeWindow.jQuery('a:first').focus();
+
       // Allow other scripts to respond to this event.
       $(document).trigger('drupalOverlayLoad');
     }
@@ -467,9 +509,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(/#[^#]*$/, ''), '');
+    // 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) {
+        $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.
@@ -479,6 +535,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.
@@ -608,37 +666,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
@@ -792,6 +819,108 @@
 };
 
 /**
+ * Updates ARIA buffer.
+ *
+ * @see http://juicystudio.com/article/improving-ajax-applications-for-jaws-users.php
+ */
+Drupal.overlay.updateARIABuffer = function () {
+  if (!Drupal.overlay.$ariabuffer) {
+    Drupal.overlay.$ariabuffer = $('<input id="overlay-ariabuffer" name="overlay-ariabuffer" type="hidden" value="0"/>').appendTo(document.body);
+  }
+  Drupal.overlay.ariabuffer = (Drupal.overlay.ariabuffer == 1) ? 0 : 1;
+  Drupal.overlay.$ariabuffer.val(Drupal.overlay.ariabuffer);
+};
+
+/**
+ * Set the presentation ARIA role to all elements in context.
+ *
+ * @param context
+ *   A DOM Element or jQuery to use as context.
+ */
+Drupal.overlay.applyARIAPresentation = function (context) {
+  var self = this,
+      regexpLinkables = /^(a|area)$/,
+      regexpFormElements = /^(input|select|textarea|button|object)$/,
+      regexpBlocks = /^(a|address|h1|h2|h3|h4|h5|h6|p|li|td|th)$/,
+      fallbackMessage = Drupal.t('You have left the overlay and are now browsing the underlying page.');
+
+  // This function is highly optimized and uses as less jQuery as possible as
+  // it is being used to traverse the entire DOM.
+  var traverse = function (element) {
+    var id = element.getAttribute('id');
+    if (id === 'skip-link' || id === 'overlay-container' || id === 'overlay-ariabuffer'
+        || (' ' + element.className + ' ').indexOf(' overlay-displace-') > -1) {
+      return;
+    }
+
+    var nodeName = element.nodeName.toLowerCase(),
+        tabIndex = element.getAttribute('tabIndex'),
+        tabbable = (tabIndex !== null &&  tabIndex >= 0)
+          || (regexpLinkables.test(nodeName) && element.href)
+          || (regexpFormElements.test(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, len=element.childNodes.length; i<len; i++) {
+        if (element.childNodes[i].nodeType === 1) traverse(element.childNodes[i]);
+      }
+    }
+
+    // Only continue if this element isn't processed before.
+    if (tabIndex !== '-1' && role !== 'presentation') {
+      // In case ARIA is not supported add some textual hints to the underlying
+      // page to help screen readers understand that the page is being displayed
+      // beneath the overlay.
+      if (regexpBlocks.test(nodeName)) {
+        var $fallback = $('<span class="element-invisible overlay-aria-fallback" role="presentation">' + fallbackMessage + '</span>').prependTo(element);
+      }
+
+      // Restore attributes when the overlay closes.
+      $(document).bind('drupalOverlayClose.drupal-overlay', function () {
+        if (tabbable) {
+          if (tabIndex !== null) {
+            element.setAttribute('tabIndex', tabIndex);
+          }
+          else {
+            element.removeAttribute('tabIndex');
+          }
+        }
+
+        if (role !== null) {
+          element.setAttribute('role', tabIndex);
+        }
+        else {
+          element.removeAttribute('role');
+        }
+
+        if ($fallback) {
+          $fallback.remove();
+        }
+
+        self.updateARIABuffer();
+      });
+    }
+  };
+
+  $(context || document.body).each(function () {
+    if (context && $(this).closest('#overlay-container, #overlay-ariabuffer, .overlay-displace-top, .overlay-displace-bottom').length) {
+      return;
+    }
+
+    for (var i=0, len=this.childNodes.length; i<len; i++) {
+      if (this.childNodes[i].nodeType === 1) traverse(this.childNodes[i]);
+    }
+  });
+
+  self.updateARIABuffer();
+};
+
+/**
  * Theme function to create the overlay iframe element.
  */
 Drupal.theme.prototype.overlayContainer = function () {
@@ -802,7 +931,7 @@
  * 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);
Index: modules/overlay/overlay.tpl.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/overlay/overlay.tpl.php,v
retrieving revision 1.3
diff -u -r1.3 overlay.tpl.php
--- modules/overlay/overlay.tpl.php	11 Jul 2010 22:03:45 -0000	1.3
+++ modules/overlay/overlay.tpl.php	20 Jul 2010 12:38:59 -0000
@@ -27,7 +27,7 @@
       <h1 id="overlay-title"<?php print $title_attributes; ?>><?php print $title; ?></h1>
     </div>
     <div id="overlay-close-wrapper">
-      <a id="overlay-close" href="#" class="overlay-close"><span class="element-invisible"><?php print t('Close overlay'); ?></span></a>
+      <a id="overlay-close" href="#" class="overlay-close"  title="<?php print t('Close overlay and return to the previous non-administrative page.'); ?>"><span class="element-invisible"><?php print t('Close overlay'); ?></span></a>
     </div>
     <?php if ($tabs): ?><ul id="overlay-tabs"><?php print render($tabs); ?></ul><?php endif; ?>
   </div>
