MDL-35918 javascript: dialogues screen reader support
authorRyan Wyllie <ryan@moodle.com>
Thu, 10 Sep 2015 08:17:19 +0000 (08:17 +0000)
committerRyan Wyllie <ryan@moodle.com>
Tue, 29 Sep 2015 04:48:57 +0000 (04:48 +0000)
The YUI notification dialogues will now set the appropriate
aria attributes when they are shown/hidden to allow screen
readers to navigate them correctly.

When the dialogue is shown all background elements will be marked
as hidden while the dialogue is marked as visible to a screen reader.

When the dialogue is closed the modified elements will be reverted
to their previous state and the dialogue will be marked as hidden to
a screen reader.

lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js

index f8a57da..384dfcf 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 32433e4..4fa7dd8 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index 3ed1ac0..78eb3ae 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index 0ff4f8b..ffede8a 100644 (file)
@@ -97,6 +97,9 @@ Y.extend(DIALOGUE, Y.Panel, {
         }
 
         if (this.get('modal')) {
+            // If we're a modal then make sure our container is ARIA
+            // hidden by default. ARIA visibility is managed for modal dialogues.
+            this.get(BASE).set('aria-hidden', 'true');
             this.plug(Y.M.core.LockScroll);
         }
 
@@ -335,6 +338,13 @@ Y.extend(DIALOGUE, Y.Panel, {
             this.lockScroll.enableScrollLock(this.shouldResizeFullscreen());
         }
 
+        // Only do accessibility hiding for modals because the ARIA spec
+        // says that all ARIA dialogues should be modal.
+        if (this.get('modal')) {
+            // Make this dialogue visible to screen readers.
+            this.setAccessibilityVisible();
+        }
+
         // Try and find a node to focus on using the focusOnShowSelector attribute.
         if (focusSelector !== null) {
             focusNode = this.get('boundingBox').one(focusSelector);
@@ -362,6 +372,11 @@ Y.extend(DIALOGUE, Y.Panel, {
             }
         }
 
+        if (this.get('modal')) {
+            // Hide this dialogue from screen readers.
+            this.setAccessibilityHidden();
+        }
+
         // Unlock scroll if the plugin is present.
         if (this.lockScroll) {
             this.lockScroll.disableScrollLock();
@@ -406,6 +421,73 @@ Y.extend(DIALOGUE, Y.Panel, {
         } else if (target === firstitem && direction === 'backward') {  // Tab+shift key.
             return lastitem.focus();
         }
+    },
+
+    /**
+     * Sets the appropriate aria attributes on this dialogue and the other
+     * elements in the DOM to ensure that screen readers are able to navigate
+     * the dialogue popup correctly.
+     *
+     * @method setAccessibilityVisible
+     */
+    setAccessibilityVisible: function() {
+        // Get the element that contains this dialogue because we need it
+        // to filter out from the document.body child elements.
+        var container = this.get(BASE);
+        // Keep a record of any elements we change so that they can be reverted later.
+        this.hiddenSiblings = [];
+
+        // We need to get a list containing each sibling element and the shallowest
+        // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
+        // the fact that this dialogue is always appended to the document body therefore
+        // it's siblings are the shallowest non-ancestral nodes. If that changes then
+        // this code should also be updated.
+        Y.one(document.body).get('children').each(function(node) {
+            // Skip the element that contains us.
+            if (node !== container) {
+                var hidden = node.get('aria-hidden');
+                // If they are already hidden we can ignore them.
+                if (hidden !== 'true') {
+                    // Save their current state.
+                    node.setData('previous-aria-hidden', hidden);
+                    this.hiddenSiblings.push(node);
+
+                    // Hide this node from screen readers.
+                    node.set('aria-hidden', 'true');
+                }
+            }
+        }, this);
+
+        // Make us visible to screen readers.
+        container.set('aria-hidden', 'false');
+    },
+
+    /**
+     * Restores the aria visibility on the DOM elements changed when displaying
+     * the dialogue popup and makes the dialogue aria hidden to allow screen
+     * readers to navigate the main page correctly when the dialogue is closed.
+     *
+     * @method setAccessibilityHidden
+     */
+    setAccessibilityHidden: function() {
+        var container = this.get(BASE);
+        container.set('aria-hidden', 'true');
+
+        // Restore the sibling nodes back to their original values.
+        Y.Array.each(this.hiddenSiblings, function(node) {
+            var previousValue = node.getData('previous-aria-hidden');
+            // If the element didn't previously have an aria-hidden attribute
+            // then we can just remove the one we set.
+            if (previousValue === null) {
+                node.removeAttribute('aria-hidden');
+            } else {
+                // Otherwise set it back to the old value (which will be false).
+                node.set('aria-hidden', previousValue);
+            }
+        });
+
+        // Clear the cache. No longer need to store these.
+        this.hiddenSiblings = [];
     }
 }, {
     NAME : DIALOGUE_NAME,