]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1280363 - [a11y] Make the Actions menu button accessible for keyboard and screen...
authorDavid Lawrence <dkl@mozilla.com>
Tue, 21 Feb 2017 21:56:56 +0000 (21:56 +0000)
committerDavid Lawrence <dkl@mozilla.com>
Tue, 21 Feb 2017 21:56:56 +0000 (21:56 +0000)
extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
extensions/BugModal/web/bug_modal.js
extensions/BugModal/web/comments.js
extensions/BugModal/web/dropdown.css [new file with mode: 0644]
extensions/BugModal/web/dropdown.js [new file with mode: 0644]

index c658f064236eb1cf6066275fa5d9d928512dbbaa..49817b6a1b32bfc5ed4425c821cab89b342e31e8 100644 (file)
   [% END %]
   <button type="button" id="comment-tags-btn" style="display:none" class="minor">Tags &#9662;</button>
   <button type="button" id="view-menu-btn" class="minor">View &#9662;</button>
+  <div class="dropdown">
+    <button type="button" role="button" id="comment-tags-btn" arai-haspopup="true" aria-label="Tags Menu"
+      aria-expanded="false" aria-controls="comment-tags-menu" class="dropdown-button minor">Tags &#9662;</button>
+    <ul id="comment-tags-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
+      <li class="dropdown-separator" role="presentation">
+        <a role="menuitem" tabindex="-1" data-comment-tag="">Reset</a>
+      </li>
+    </ul>
+  </div>
+  <div class="dropdown">
+    <button type="button" role="button" id="view-menu-btn" arai-haspopup="true" aria-label="View Menu"
+      aria-expanded="false" aria-controls="view-menu" class="dropdown-button minor">View &#9662;</button>
+    <ul id="view-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
+      <li class="dropdown-separator" role="presentation">
+        <a id="view-reset" role="menuitem" tabindex="-1">Reset</a>
+      </li>
+      <li role="presentation">
+        <a id="view-collapse-all" role="menuitem" tabindex="-1">Collapse All</a>
+      </li>
+      <li role="presentation">
+        <a id="view-expand-all" role="menuitem" tabindex="-1">Expand All</a>
+      </li>
+      <li class="dropdown-separator" role="presentation">
+        <a id="view-comments-only" role="menuitem" tabindex="-1">Comments Only</a>
+      </li>
+      <li role="presentation">
+        <a id="view-toggle-cc" role="menuitem" tabindex="-1">Show CC Changes</a>
+      </li>
+      [% IF treeherder %]
+        <li role="presentation">
+          <a id="view-toggle-treeherder" role="menuitem" data-userid="[% treeherder.id FILTER none %]">Hide Treeherder Comments</a>
+        </li>
+      [% END %]
+    </ul>
+   </div>
 </div>
 
-<menu id="view-menu" type="context" style="display:none">
-  <menuitem id="view-reset" label="Reset"></menuitem>
-  <hr>
-  <menuitem id="view-collapse-all" label="Collapse All"></menuitem>
-  <menuitem id="view-expand-all" label="Expand All"></menuitem>
-  <menuitem id="view-comments-only" label="Comments Only"></menuitem>
-  <hr>
-  <menuitem id="view-toggle-cc" label="Show CC Changes"></menuitem>
-  [% IF treeherder %]
-    <menuitem id="view-toggle-treeherder" label="Hide Treeherder Comments" data-userid="[% treeherder.id FILTER none %]"></menuitem>
-  [% END %]
-</menu>
-
 [%
   PROCESS bug/time.html.tmpl;
 
index 4b23df786f8d4ae4f9ff8ebe53404e6c5c907077..6373e1f5226c79cf1b7f92421cef55017d4aae60 100644 (file)
           [% is_cced ? "Stop Following" : "Follow" %]
         </button>
       [% END %]
-      <button type="button" id="action-menu-btn" class="minor">&#9662;</button>
-      <menu id="action-menu" type="context" style="display:none">
-        <menuitem id="action-reset" label="Reset Sections"></menuitem>
-        <menuitem id="action-expand-all" label="Expand All Sections"></menuitem>
-        <menuitem id="action-collapse-all" label="Collapse All Sections"></menuitem>
-        <hr>
-        [% IF user.id %]
-          <menuitem id="action-add-comment" label="Add Comment"></menuitem>
-        [% END %]
-        <menuitem id="action-last-comment" label="Last Comment"></menuitem>
-        <hr>
-        <menuitem id="action-history" label="History"></menuitem>
-      </menu>
+      <div class="dropdown">
+        <button id="action-menu-btn" aria-haspopup="true" aria-label="Actions Menu"
+          aria-expanded="false" aria-controls="action-menu" class="dropdown-button minor">&#9662;</button>
+        <ul class="dropdown-content" id="action-menu" role="menu" style="display:none;">
+          <li role="presentation">
+            <a id="action-reset" role="menuitem" tabindex="-1">Reset Sections</a>
+          </li>
+          <li role="presentation">
+            <a id="action-expand-all" role="menuitem" tabindex="-1">Expand All Sections</a>
+          </li>
+          <li class="dropdown-separator" role="presentation">
+            <a id="action-collapse-all" role="menuitem" tabindex="-1">Collapse All Sections</a>
+          </li>
+          [% IF user.id %]
+            <li role="presentation">
+              <a id="action-add-comment" role="menuitem" tabindex="-1">Add Comment</a>
+            </li>
+          [% END %]
+          <li class="dropdown-separator" role="presentation">
+            <a id="action-last-comment" role="menuitem" tabindex="-1">Last Comment</a>
+          </li>
+          <li role="presentation">
+            <a id="action-history" role="menuitem" tabindex="-1">History</a>
+          </li>
+        </ul>
+      </div>
     </div>
     <div id="user-guide">
       <a title="User guide for [% terms.Bugzilla %]" href="https://wiki.mozilla.org/BMO/UserGuide">Get help with this page</a>
 <div id="bottom-actions">
   <div id="bottom-right-actions">
     <button type="button" id="top-btn" class="minor">Top &uarr;</button>
-    <button type="button" id="format-btn" class="minor">Format &#9662;</button>
+    <div class="dropdown">
+      <button id="format-btn" aria-haspopup="true" aria-label="Format [% terms.Bug %] Menu"
+        aria-expanded="false" aria-controls="format-menu" class="dropdown-button minor">Format [% terms.Bug %] &#9652;</button>
+      <ul class="dropdown-content menu-up" id="format-menu" role="menu" style="display:none;">
+        <li role="presentation">
+          <a href="show_bug.cgi?format=multiple&amp;id=[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">For Printing</a>
+        </li>
+        <li role="presentation">
+          <a href="show_bug.cgi?ctype=xml&amp;id=[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">XML</a>
+        </li>
+        <li role="presentation">
+          <a href="show_bug.cgi?format=default&amp;id=[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">Legacy</a>
+        </li>
+        [% IF bug.groups_in.size == 0 %]
+          <li role="presentation">
+            <a href="rest/bug/[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">JSON</a>
+          </li>
+        [% END %]
+      </ul>
+    </div>
     [% IF user.id %]
-      <button type="button" id="new-bug-btn" class="minor">New/Clone [% terms.Bug %] &#9662;</button>
+      <div class="dropdown">
+        <button id="new-bug-btn" aria-haspopup="true" aria-label="New/Clone [% terms.Bug %] Menu"
+          aria-expanded="false" aria-controls="new-bug-menu" class="dropdown-button minor">New/Clone [% terms.Bug %] &#9652;</button>
+        <ul class="dropdown-content menu-up" id="new-bug-menu" role="menu" style="display:none;">
+          <li role="presentation">
+            <a href="enter_bug.cgi" role="menuitem" tabindex="-1" target="_blank">
+              Create a new [% terms.bug %]</a>
+          </li>
+          <li role="presentation">
+            <a href="enter_bug.cgi?product=[% bug.product FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; in this product</a>
+          </li>
+          <li class="dropdown-separator" role="presentation">
+            <a href="enter_bug.cgi?product=[% bug.product FILTER uri %]&amp;component=[% bug.component FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; in this component</a>
+          </li>
+          <li role="presentation">
+            <a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;blocked=[% bug.id FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; that blocks this [% terms.bug %]</a>
+          </li>
+          <li class="dropdown-separator" role="presentation">
+            <a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;dependson=[% bug.id FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; that depends on this [% terms.bug %]</a>
+          </li>
+          <li role="presentation">
+            <a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;cloned_bug_id=[% bug.id FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; as a clone of this [% terms.bug %]</a>
+          </li>
+          <li role="presentation">
+            <a href="enter_bug.cgi?format=__default__&amp;cloned_bug_id=[% terms.bug FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; as a clone, in a different product</a>
+          </li>
+        </ul>
+      </div>
     [% END %]
   </div>
 </div>
index e5070bcf51977d9aeeb9d1433addf1f7c5f769aa..3231ab311955e0e6ff3f603cd908fbfb402edda7 100644 (file)
@@ -52,6 +52,7 @@
     "extensions/ProdCompSearch/web/js/prod_comp_search.js",
     "extensions/BugModal/web/bug_modal.js",
     "extensions/BugModal/web/comments.js",
+    "extensions/BugModal/web/dropdown.js",
     "extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js",
     "js/bugzilla-readable-status-min.js",
     "js/field.js",
   );
   jquery.push(
     "datetimepicker",
-    "contextMenu",
     "visibility"
   );
   style_urls.push(
     "extensions/BugModal/web/bug_modal.css",
+    "extensions/BugModal/web/dropdown.css",
     "skins/custom/bug_groups.css",
-    "js/jquery/plugins/datetimepicker/datetimepicker.css",
-    "js/jquery/plugins/contextMenu/contextMenu.css"
+    "js/jquery/plugins/datetimepicker/datetimepicker.css"
   );
 
   IF user.in_group('canconfirm');
index 5c630ba07cbf8de93d0889dcf7cf61c30345a215..4c28936cc0903bf0bb075d210447fa36ece59eb2 100644 (file)
@@ -41,12 +41,11 @@ END;
           width="[% gravatar_size FILTER none %]" height="[% gravatar_size FILTER none %]">
       [% END %]
       [% UNLESS gravatar_only %]
-        <a class="email [%= "disabled" UNLESS u.is_enabled %] [%= "show_usermenu" IF user.id %]"
+        <a class="email [%= "disabled" UNLESS u.is_enabled %]"
           [% IF user.id %]
             href="mailto:[% u.email FILTER html %]"
-            data-user-id="[% u.id FILTER html %]"
-            data-user-email="[% u.email FILTER html %]"
-            data-show-edit="[% user.in_group('editusers') || user.bless_groups.size > 9 ? 'true' : 'false' %]"
+            onclick="return show_usermenu([% u.id FILTER none %], '[% u.email FILTER js %]',
+              [% user.in_group('editusers') || user.bless_groups.size > 0 ? "true" : "false" %])"
             title="[% u.identity FILTER html %]"
           [% ELSE %]
             href="user_profile?user_id=[% u.id FILTER none %]"
index 894745016e27a9bf94a3f3a6124ffa17abbd29dc..f65c12be315cc01e781ca7fd9a0ea15e76ef1588 100644 (file)
@@ -377,13 +377,7 @@ $(function() {
             }
         });
 
-    // action button menu
-
-    $.contextMenu({
-        selector: '#action-menu-btn',
-        trigger: 'left',
-        items: $.contextMenu.fromMenu($('#action-menu'))
-    });
+    // action button actions
 
     // reset
     $('#action-reset')
@@ -1006,99 +1000,6 @@ $(function() {
         BUGZILLA.remaining_time = $('#remaining_time').val();
     });
 
-    // new bug button
-    $.contextMenu({
-        selector: '#new-bug-btn',
-        trigger: 'left',
-        items: [
-            {
-                name: 'Create a new Bug',
-                callback: function() {
-                    window.open('enter_bug.cgi', '_blank');
-                }
-            },
-            {
-                name: '\u2026 in this product',
-                callback: function() {
-                    window.open('enter_bug.cgi?product=' + encodeURIComponent($('#product').val()), '_blank');
-                }
-            },
-            {
-                name: '\u2026 in this component',
-                callback: function() {
-                    window.open('enter_bug.cgi?' +
-                                'product=' + encodeURIComponent($('#product').val()) +
-                                '&component=' + encodeURIComponent($('#component').val()), '_blank');
-                }
-            },
-            {
-                name: '\u2026 that blocks this bug',
-                callback: function() {
-                    window.open('enter_bug.cgi?format=__default__' +
-                                '&product=' + encodeURIComponent($('#product').val()) +
-                                '&blocked=' + BUGZILLA.bug_id, '_blank');
-                }
-            },
-            {
-                name: '\u2026 that depends on this bug',
-                callback: function() {
-                    window.open('enter_bug.cgi?format=__default__' +
-                                '&product=' + encodeURIComponent($('#product').val()) +
-                                '&dependson=' + BUGZILLA.bug_id, '_blank');
-                }
-            },
-            {
-                name: '\u2026 as a clone of this bug',
-                callback: function() {
-                    window.open('enter_bug.cgi?format=__default__' +
-                                '&product=' + encodeURIComponent($('#product').val()) +
-                                '&cloned_bug_id=' + BUGZILLA.bug_id, '_blank');
-                }
-            },
-            {
-                name: '\u2026 as a clone, in a different product',
-                callback: function() {
-                    window.open('enter_bug.cgi?format=__default__' +
-                                '&cloned_bug_id=' + BUGZILLA.bug_id, '_blank');
-                }
-            },
-        ]
-    });
-
-    var format_items = [
-        {
-        name: 'For Printing',
-            callback: function() {
-                window.location.href = 'show_bug.cgi?format=multiple&id=' + BUGZILLA.bug_id;
-            }
-        },
-        {
-            name: 'XML',
-            callback: function() {
-                window.location.href = 'show_bug.cgi?ctype=xml&id=' + BUGZILLA.bug_id;
-            }
-        },
-        {
-            name: 'Legacy',
-            callback: function() {
-                window.location.href = 'show_bug.cgi?format=default&id=' + BUGZILLA.bug_id;
-            }
-        }
-    ];
-    if (!BUGZILLA.bug_secure) {
-        format_items.push({
-            name: 'JSON',
-            callback: function() {
-                window.location.href = 'rest/bug/' + BUGZILLA.bug_id;
-            }
-        });
-    }
-    $.contextMenu({
-        selector: '#format-btn',
-        trigger: 'left',
-        items: format_items
-    });
-
     // "reset to default" checkboxes
     $('#product, #component')
         .change(function(event) {
index 7eb933cfc691252a7b44fedf4b79cff2865cd9dc..04894506ee3348c0f29662b5459bb287c87b840d 100644 (file)
@@ -189,12 +189,6 @@ $(function() {
             }
         });
 
-    $.contextMenu({
-        selector: '#view-menu-btn',
-        trigger: 'left',
-        items: $.contextMenu.fromMenu($('#view-menu'))
-    });
-
     function updateTagsMenu() {
         var tags = [];
         $('.comment-tags').each(function() {
@@ -218,21 +212,24 @@ $(function() {
         }
         btn.show();
 
-        var menuItems = [
-            { name: 'Reset', tag: '' },
-            "--"
-        ];
+        // clear out old li items. Always leave the first one (Reset)
+        var $li = $('#comment-tags-menu li');
+        for (var i = 1, l = $li.length; i < l; i++) {
+            $li.eq(i).remove();
+        }
+
+        // add new li items
         $.each(tagNames, function(key, value) {
-            menuItems.push({ name: value + ' (' + tags[value] + ')', tag: value });
+            $('#comment-tags-menu')
+                .append($('<li role="presentation">')
+                    .append($('<a role="menuitem" tabindex="-1" data-comment-tag="' + value + '">')
+                        .append(value + ' (' + tags[value] + ')')));
         });
 
-        $.contextMenu('destroy', '#comment-tags-btn');
-        $.contextMenu({
-            selector: '#comment-tags-btn',
-            trigger: 'left',
-            items: menuItems,
-            callback: function(key, opt) {
-                var tag = opt.commands[key].tag;
+        $('a[data-comment-tag]').each(function() {
+            $(this).click(function() {
+                var $that = $(this);
+                var tag = $that.data('comment-tag');
                 if (tag === '') {
                     $('.change-spinner:visible').each(function() {
                         toggleChange($(this), 'reset');
@@ -241,17 +238,17 @@ $(function() {
                 }
                 var firstComment = false;
                 $('.change-spinner:visible').each(function() {
-                    var that = $(this);
-                    var commentTags = tagsFromDom(that.parents('.comment').find('.comment-tags'));
+                    var $that = $(this);
+                    var commentTags = tagsFromDom($that.parents('.comment').find('.comment-tags'));
                     var hasTag = $.inArrayIn(tag, commentTags) >= 0;
-                    toggleChange(that, hasTag ? 'show' : 'hide');
+                    toggleChange($that, hasTag ? 'show' : 'hide');
                     if (hasTag && !firstComment) {
-                        firstComment = that;
+                        firstComment = $that;
                     }
                 });
                 if (firstComment)
                     $.scrollTo(firstComment);
-            }
+            });
         });
     }
 
diff --git a/extensions/BugModal/web/dropdown.css b/extensions/BugModal/web/dropdown.css
new file mode 100644 (file)
index 0000000..977a7a5
--- /dev/null
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* The container <div> - needed to position the dropdown content */
+.dropdown {
+    position: relative;
+    display: inline-block;
+}
+
+/* Dropdown Content (Hidden by Default) */
+.dropdown-content {
+    position: absolute;
+    background-color: #eee;
+    min-width: 120px;
+    z-index: 1;
+    text-align: left;
+    margin: 0;
+    padding: 0;
+    border: 1px solid #ddd;
+    -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+    -moz-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+    list-style: none;
+}
+
+.dropdown-content.menu-up {
+    bottom: 100%;
+}
+
+.dropdown-separator {
+    border-bottom: 1px solid #ddd;
+}
+
+/* Links inside the dropdown */
+.dropdown-content a {
+    white-space: nowrap;
+    background-color: #eee;
+    color: black !important;
+    padding: 4px 8px;
+    text-decoration: none !important;
+    display: block;
+}
+
+/* Change color of dropdown links on hover */
+.dropdown-content li .active {
+    text-decoration: none;
+    background-color: #39f;
+}
diff --git a/extensions/BugModal/web/dropdown.js b/extensions/BugModal/web/dropdown.js
new file mode 100644 (file)
index 0000000..3198c09
--- /dev/null
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+$(function() {
+    'use strict';
+
+    $(window).click(function(e) {
+        // clicking dropdown button opens or closes the dropdown content
+        if (!$(e.target).hasClass('dropdown-button')) {
+            $('.dropdown-button').each(function() {
+                toggleDropDown(e, $(this), $('#' + $(this).attr('aria-controls')), 1);
+            });
+        }
+    }).keydown(function(e) {
+        // Escape key hides the dropdown if visible
+        if (e.keyCode == 27) {
+            $('.dropdown-button').each(function() {
+                var $button = $(this);
+                if ($button.siblings('.dropdown-content').is(':visible')) {
+                    toggleDropDown(e, $button, $('#' + $button.attr('aria-controls')), 1);
+                    $button.focus();
+                }
+            });
+        }
+        // allow arrow up and down keys to choose one of the dropdown items if menu visible
+        if (e.keyCode == 38 || e.keyCode == 40) {
+            $('.dropdown-content').each(function() {
+                var $content = $(this);
+                if ($content.is(':visible')) {
+                    e.preventDefault();
+                    e.stopPropagation();
+                    var $li = $content.find('li');
+                    // if none focused select the first or last
+                    var $any_focused = $content.find('a:focus');
+                    if ($any_focused.length == 0) {
+                        var index = e.keyCode == 40 ? 0 : $li.length - 1;
+                        var $link = $li.eq(index).find('a');
+                        $link.addClass('active').focus();
+                        return;
+                    }
+                    // otherwise move up or down the list based on arrow key pressed
+                    var inc  = e.keyCode == 40 ? 1 : -1;
+                    var move = $content.find('a:focus').parent('li').index() + inc;
+                    var $link = $li.eq(move % $li.length).find('a');
+                    $content.find('a').removeClass('active');
+                    $link.addClass('active').focus();
+                }
+            });
+        }
+
+        // enter clicks on a link
+        if (e.keyCode == 13) {
+            $('.dropdown-content:visible a.active').trigger('click');
+        }
+    });
+
+    $('.dropdown-content a').hover(
+        function(){ $(this).addClass('active')  },
+        function(){ $(this).removeClass('active')  }
+    );
+
+    $('.dropdown').each(function() {
+        var $div     = $(this);
+        var $button  = $div.find('.dropdown-button');
+        var $content = $div.find('.dropdown-content');
+        $button.click(function(e) {
+            toggleDropDown(e, $button, $content);
+        }).keydown(function(e) {
+            // allow enter to toggle menu
+            if (e.keyCode == 13) {
+                toggleDropDown(e, $button, $content);
+            }
+        });
+    });
+
+    function toggleDropDown(e, $button, $content, hide_only) {
+        // If clicking a real link we do not want to prevent default behavior
+        if (!$(e.target).is('.dropdown-content a') || $(e.target).is('.dropbown-button')) {
+            e.preventDefault();
+        }
+        e.stopPropagation();
+        // clear all active links
+        $content.find('a').removeClass('active');
+        if ($content.is(':visible')) {
+            $content.hide();
+            $button.attr('aria-expanded', false);
+        }
+        // if not using Escape or clicking outside the dropdown div, then we are hiding
+        else if (!hide_only) {
+            $content.show();
+            $button.attr('aria-expanded', true);
+        }
+    }
+});