]> source.dussan.org Git - jquery-ui.git/commitdiff
Core: Partial fix for #3559: Proper implementation for :focusable and :tabbable selec...
authorScott González <scott.gonzalez@gmail.com>
Wed, 21 Jan 2009 03:25:02 +0000 (03:25 +0000)
committerScott González <scott.gonzalez@gmail.com>
Wed, 21 Jan 2009 03:25:02 +0000 (03:25 +0000)
tests/unit/core/core.html
tests/unit/core/core.js
tests/unit/core/selector.js [new file with mode: 0644]
ui/ui.core.js

index d0ff8fbad2bdbb9fee5847e42c511b4984c98363..cf1037027ae7cacb62a13dd639b95b241ea9853b 100644 (file)
        <script type="text/javascript" src="../../../external/simulate/jquery.simulate.js"></script>
 
        <script type="text/javascript" src="core.js"></script>
+       <script type="text/javascript" src="selector.js"></script>
 </head>
 <body>
 
 <div id="main">
-       <div id="wrap1">
-               <input id="input1-1" />
-               <input type="text" id="input1-2" />
-               <input type="checkbox" id="input1-3" />
-               <input type="radio" id="input1-4" />
-               <input type="button" id="input1-5" />
-               <input type="hidden" id="input1-6" />
-               <select id="input1-7"></select>
-               <textarea id="input1-8"></textarea>
-               <a href="#" id="anchor1-1">anchor</a>
-               <a id="anchor1-2">anchor</a>
+       <div>
+               <input id="visibleAncestor-inputTypeNone" />
+               <input type="text" id="visibleAncestor-inputTypeText" />
+               <input type="checkbox" id="visibleAncestor-inputTypeCheckbox" />
+               <input type="radio" id="visibleAncestor-inputTypeRadio" />
+               <input type="button" id="visibleAncestor-inputTypeButton" />
+               <input type="hidden" id="visibleAncestor-inputTypeHidden" />
+               <button id="visibleAncestor-button"></button>
+               <select id="visibleAncestor-select">
+                       <option>option</option>
+               </select>
+               <textarea id="visibleAncestor-textarea"></textarea>
+               <object id="visibleAncestor-object"></object>
+               <a href="#" id="visibleAncestor-anchorWithHref">anchor</a>
+               <a id="visibleAncestor-anchorWithoutHref">anchor</a>
+               <map>
+                       <area href="#" id="visibleAncestor-areaWithHref" alt="" />
+                       <area id="visibleAncestor-areaWithoutHref" alt="" />
+               </map>
+               <span id="visibleAncestor-span"></span>
+               <div id="visibleAncestor-div"></div>
+               <span id="visibleAncestor-spanWithTabindex" tabindex="1"></span>
+               <div id="visibleAncestor-divWithNegativeTabindex" tabindex="-1"></div>
        </div>
-       <div id="wrap2">
-               <input id="input2-1" disabled="disabled" />
-               <input type="text" id="input2-2" disabled="disabled" />
-               <input type="checkbox" id="input2-3" disabled="disabled" />
-               <input type="radio" id="input2-4" disabled="disabled" />
-               <input type="button" id="input2-5" disabled="disabled" />
-               <input type="hidden" id="input2-6" disabled="disabled" />
-               <select id="input2-7" disabled="disabled"></select>
-               <textarea id="input2-8" disabled="disabled"></textarea>
+       
+       <div>
+               <input id="disabledElement-inputTypeNone" disabled="disabled" />
+               <input type="text" id="disabledElement-inputTypeText" disabled="disabled" />
+               <input type="checkbox" id="disabledElement-inputTypeCheckbox" disabled="disabled" />
+               <input type="radio" id="disabledElement-inputTypeRadio" disabled="disabled" />
+               <input type="button" id="disabledElement-inputTypeButton" disabled="disabled" />
+               <input type="hidden" id="disabledElement-inputTypeHidden" disabled="disabled" />
+               <button id="disabledElement-button" disabled="disabled"></button>
+               <select id="disabledElement-select" disabled="disabled"></select>
+               <textarea id="disabledElement-textarea" disabled="disabled"></textarea>
        </div>
-       <div id="wrap3">
-               <div id="wrap3-1" style="display: none;">
-                       <input id="input3-1" />
-                       <a href="#" id="anchor3-1">anchor</a>
+       
+       <div>
+               <div id="displayNoneAncestor" style="display: none;">
+                       <input id="displayNoneAncestor-input" />
+                       <span tabindex="1" id="displayNoneAncestor-span"></span>
                </div>
-               <div id="wrap3-2" style="visibility: hidden;">
-                       <input id="input3-2" />
-                       <a href="#" id="anchor3-2">anchor</a>
+               
+               <div id="visibilityHiddenAncestor" style="visibility: hidden;">
+                       <input id="visibilityHiddenAncestor-input" />
+                       <span tabindex="1" id="visibilityHiddenAncestor-span"></span>
                </div>
-               <input id="input3-3" style="display: none;">
-               <input id="input3-4" style="visibility: hidden;">
+               
+               <input id="displayNone-input" style="display: none;" />
+               <input id="visibilityHidden-input" style="visibility: hidden;" />
+               
+               <span tabindex="1" id="displayNone-span" style="display: none;"></span>
+               <span tabindex="1" id="visibilityHidden-span" style="visibility: hidden;"></span>
        </div>
-       <div id="wrap4">
-               <input id="input4-1" tabindex="0" />
-               <input id="input4-2" tabindex="10" />
-               <input id="input4-3" tabindex="-1" />
-               <input id="input4-4" tabindex="-50" />
+       
+       <div>
+               <input id="inputTabindex0" tabindex="0" />
+               <input id="inputTabindex10" tabindex="10" />
+               <input id="inputTabindex-1" tabindex="-1" />
+               <input id="inputTabindex-50" tabindex="-50" />
+               
+               <span id="spanTabindex0" tabindex="0"></span>
+               <span id="spanTabindex10" tabindex="10"></span>
+               <span id="spanTabindex-1" tabindex="-1"></span>
+               <span id="spanTabindex-50" tabindex="-50"></span>
        </div>
+       
+       <div>
+               <input id="inputTabindexfoo" tabindex="foo" />
+               <input id="inputTabindex3foo" tabindex="3foo" />
+               
+               <span id="spanTabindexfoo" tabindex="foo"></span>
+               <span id="spanTabindex3foo" tabindex="3foo"></span>
+       </div>
+       
        <div id="aria"></div>
 </div>
 
index 4be475486bd8e6985b23476baf100042df66ef2c..53f392370759596f06698f5512137046a75e2c66 100644 (file)
@@ -3,56 +3,6 @@
  */
 (function($) {
 
-module("selectors");
-
-test("tabbable - enabled elements", function() {
-       expect(10);
-
-       ok( $('#input1-1').is(':tabbable'), 'input, no type');
-       ok( $('#input1-2').is(':tabbable'), 'input, type text');
-       ok( $('#input1-3').is(':tabbable'), 'input, type checkbox');
-       ok( $('#input1-4').is(':tabbable'), 'input, type radio');
-       ok( $('#input1-5').is(':tabbable'), 'input, type button');
-       ok(!$('#input1-6').is(':tabbable'), 'input, type hidden');
-       ok( $('#input1-7').is(':tabbable'), 'select');
-       ok( $('#input1-8').is(':tabbable'), 'textarea');
-       ok( $('#anchor1-1').is(':tabbable'), 'anchor with href');
-       ok(!$('#anchor1-2').is(':tabbable'), 'anchor without href');
-});
-
-test("tabbable - disabled elements", function() {
-       expect(8);
-
-       ok(!$('#input2-1').is(':tabbable'), 'input, no type');
-       ok(!$('#input2-2').is(':tabbable'), 'input, type text');
-       ok(!$('#input2-3').is(':tabbable'), 'input, type checkbox');
-       ok(!$('#input2-4').is(':tabbable'), 'input, type radio');
-       ok(!$('#input2-5').is(':tabbable'), 'input, type button');
-       ok(!$('#input2-6').is(':tabbable'), 'input, type hidden');
-       ok(!$('#input2-7').is(':tabbable'), 'select');
-       ok(!$('#input2-8').is(':tabbable'), 'textarea');
-});
-
-test("tabbable - hidden styles", function() {
-       expect(6);
-
-       ok(!$('#input3-1').is(':tabbable'), 'input, hidden wrapper - display: none');
-       ok(!$('#anchor3-1').is(':tabbable'), 'anchor, hidden wrapper - display: none');
-       ok(!$('#input3-2').is(':tabbable'), 'input, hidden wrapper - visibility: hidden');
-       ok(!$('#anchor3-2').is(':tabbable'), 'anchor, hidden wrapper - visibility: hidden');
-       ok(!$('#input3-3').is(':tabbable'), 'input, display: none');
-       ok(!$('#input3-4').is(':tabbable'), 'input, visibility: hidden');
-});
-
-test("tabbable - tabindex", function() {
-       expect(4);
-
-       ok( $('#input4-1').is(':tabbable'), 'input, tabindex 0');
-       ok( $('#input4-2').is(':tabbable'), 'input, tabindex 10');
-       ok(!$('#input4-3').is(':tabbable'), 'input, tabindex -1');
-       ok(!$('#input4-4').is(':tabbable'), 'input, tabindex -50');
-});
-
 module('jQuery extensions');
 
 test("attr - aria", function() {
diff --git a/tests/unit/core/selector.js b/tests/unit/core/selector.js
new file mode 100644 (file)
index 0000000..73df0c3
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * core unit tests
+ */
+(function($) {
+
+module("selectors");
+
+function isFocusable(selector, msg) {
+       ok($(selector).is(':focusable'), msg);
+}
+
+function isNotFocusable(selector, msg) {
+       ok($(selector).length && !$(selector).is(':focusable'), msg);
+}
+
+function isTabbable(selector, msg) {
+       ok($(selector).is(':tabbable'), msg);
+}
+
+function isNotTabbable(selector, msg) {
+       ok($(selector).length && !$(selector).is(':tabbable'), msg);
+}
+
+test("focusable - visible, enabled elements", function() {
+       expect(18);
+       
+       isFocusable('#visibleAncestor-inputTypeNone', 'input, no type');
+       isFocusable('#visibleAncestor-inputTypeText', 'input, type text');
+       isFocusable('#visibleAncestor-inputTypeCheckbox', 'input, type checkbox');
+       isFocusable('#visibleAncestor-inputTypeRadio', 'input, type radio');
+       isFocusable('#visibleAncestor-inputTypeButton', 'input, type button');
+       isNotFocusable('#visibleAncestor-inputTypeHidden', 'input, type hidden');
+       isFocusable('#visibleAncestor-button', 'button');
+       isFocusable('#visibleAncestor-select', 'select');
+       isFocusable('#visibleAncestor-textarea', 'textarea');
+       isFocusable('#visibleAncestor-object', 'object');
+       isFocusable('#visibleAncestor-anchorWithHref', 'anchor with href');
+       isNotFocusable('#visibleAncestor-anchorWithoutHref', 'anchor without href');
+       isFocusable('#visibleAncestor-areaWithHref', 'area with href');
+       isNotFocusable('#visibleAncestor-areaWithoutHref', 'area without href');
+       isNotFocusable('#visibleAncestor-span', 'span');
+       isNotFocusable('#visibleAncestor-div', 'div');
+       isFocusable("#visibleAncestor-spanWithTabindex", 'span with tabindex');
+       isFocusable("#visibleAncestor-divWithNegativeTabindex", 'div with tabindex');
+});
+
+test("focusable - disabled elements", function() {
+       expect(9);
+       
+       isNotFocusable('#disabledElement-inputTypeNone', 'input, no type');
+       isNotFocusable('#disabledElement-inputTypeText', 'input, type text');
+       isNotFocusable('#disabledElement-inputTypeCheckbox', 'input, type checkbox');
+       isNotFocusable('#disabledElement-inputTypeRadio', 'input, type radio');
+       isNotFocusable('#disabledElement-inputTypeButton', 'input, type button');
+       isNotFocusable('#disabledElement-inputTypeHidden', 'input, type hidden');
+       isNotFocusable('#disabledElement-button', 'button');
+       isNotFocusable('#disabledElement-select', 'select');
+       isNotFocusable('#disabledElement-textarea', 'textarea');
+});
+
+test("focusable - hidden styles", function() {
+       expect(8);
+       
+       isNotFocusable('#displayNoneAncestor-input', 'input, display: none parent');
+       isNotFocusable('#displayNoneAncestor-span', 'span with tabindex, display: none parent');
+       
+       isNotFocusable('#visibilityHiddenAncestor-input', 'input, visibility: hidden parent');
+       isNotFocusable('#visibilityHiddenAncestor-span', 'span with tabindex, visibility: hidden parent');
+       
+       isNotFocusable('#displayNone-input', 'input, display: none');
+       isNotFocusable('#visibilityHidden-input', 'input, visibility: hidden');
+       
+       isNotFocusable('#displayNone-span', 'span with tabindex, display: none');
+       isNotFocusable('#visibilityHidden-span', 'span with tabindex, visibility: hidden');
+});
+
+test("focusable -  natively tabbable with various tabindex", function() {
+       expect(4);
+       
+       isFocusable('#inputTabindex0', 'input, tabindex 0');
+       isFocusable('#inputTabindex10', 'input, tabindex 10');
+       isFocusable('#inputTabindex-1', 'input, tabindex -1');
+       isFocusable('#inputTabindex-50', 'input, tabindex -50');
+});
+
+test("focusable -  not natively tabbable with various tabindex", function() {
+       expect(4);
+       
+       isFocusable('#spanTabindex0', 'span, tabindex 0');
+       isFocusable('#spanTabindex10', 'span, tabindex 10');
+       isFocusable('#spanTabindex-1', 'span, tabindex -1');
+       isFocusable('#spanTabindex-50', 'span, tabindex -50');
+});
+
+test("focusable - invalid tabindex", function() {
+       expect(4);
+       
+       isFocusable('#inputTabindexfoo', 'input, tabindex foo');
+       isFocusable('#inputTabindex3foo', 'input, tabindex 3foo');
+       isNotFocusable('#spanTabindexfoo', 'span tabindex foo');
+       isNotFocusable('#spanTabindex3foo', 'span, tabindex 3foo');
+});
+
+test("tabbable - visible, enabled elements", function() {
+       expect(18);
+       
+       isTabbable('#visibleAncestor-inputTypeNone', 'input, no type');
+       isTabbable('#visibleAncestor-inputTypeText', 'input, type text');
+       isTabbable('#visibleAncestor-inputTypeCheckbox', 'input, type checkbox');
+       isTabbable('#visibleAncestor-inputTypeRadio', 'input, type radio');
+       isTabbable('#visibleAncestor-inputTypeButton', 'input, type button');
+       isNotTabbable('#visibleAncestor-inputTypeHidden', 'input, type hidden');
+       isTabbable('#visibleAncestor-button', 'button');
+       isTabbable('#visibleAncestor-select', 'select');
+       isTabbable('#visibleAncestor-textarea', 'textarea');
+       isTabbable('#visibleAncestor-object', 'object');
+       isTabbable('#visibleAncestor-anchorWithHref', 'anchor with href');
+       isNotTabbable('#visibleAncestor-anchorWithoutHref', 'anchor without href');
+       isTabbable('#visibleAncestor-areaWithHref', 'area with href');
+       isNotTabbable('#visibleAncestor-areaWithoutHref', 'area without href');
+       isNotTabbable('#visibleAncestor-span', 'span');
+       isNotTabbable('#visibleAncestor-div', 'div');
+       isTabbable("#visibleAncestor-spanWithTabindex", 'span with tabindex');
+       isNotTabbable("#visibleAncestor-divWithNegativeTabindex", 'div with tabindex');
+});
+
+test("Tabbable - disabled elements", function() {
+       expect(9);
+       
+       isNotTabbable('#disabledElement-inputTypeNone', 'input, no type');
+       isNotTabbable('#disabledElement-inputTypeText', 'input, type text');
+       isNotTabbable('#disabledElement-inputTypeCheckbox', 'input, type checkbox');
+       isNotTabbable('#disabledElement-inputTypeRadio', 'input, type radio');
+       isNotTabbable('#disabledElement-inputTypeButton', 'input, type button');
+       isNotTabbable('#disabledElement-inputTypeHidden', 'input, type hidden');
+       isNotTabbable('#disabledElement-button', 'button');
+       isNotTabbable('#disabledElement-select', 'select');
+       isNotTabbable('#disabledElement-textarea', 'textarea');
+});
+
+test("Tabbable - hidden styles", function() {
+       expect(8);
+       
+       isNotTabbable('#displayNoneAncestor-input', 'input, display: none parent');
+       isNotTabbable('#displayNoneAncestor-span', 'span with tabindex, display: none parent');
+       
+       isNotTabbable('#visibilityHiddenAncestor-input', 'input, visibility: hidden parent');
+       isNotTabbable('#visibilityHiddenAncestor-span', 'span with tabindex, visibility: hidden parent');
+       
+       isNotTabbable('#displayNone-input', 'input, display: none');
+       isNotTabbable('#visibilityHidden-input', 'input, visibility: hidden');
+       
+       isNotTabbable('#displayNone-span', 'span with tabindex, display: none');
+       isNotTabbable('#visibilityHidden-span', 'span with tabindex, visibility: hidden');
+});
+
+test("Tabbable -  natively tabbable with various tabindex", function() {
+       expect(4);
+       
+       isTabbable('#inputTabindex0', 'input, tabindex 0');
+       isTabbable('#inputTabindex10', 'input, tabindex 10');
+       isNotTabbable('#inputTabindex-1', 'input, tabindex -1');
+       isNotTabbable('#inputTabindex-50', 'input, tabindex -50');
+});
+
+test("Tabbable -  not natively tabbable with various tabindex", function() {
+       expect(4);
+       
+       isTabbable('#spanTabindex0', 'span, tabindex 0');
+       isTabbable('#spanTabindex10', 'span, tabindex 10');
+       isNotTabbable('#spanTabindex-1', 'span, tabindex -1');
+       isNotTabbable('#spanTabindex-50', 'span, tabindex -50');
+});
+
+test("Tabbable - invalid tabindex", function() {
+       expect(4);
+       
+       isTabbable('#inputTabindexfoo', 'input, tabindex foo');
+       isTabbable('#inputTabindex3foo', 'input, tabindex 3foo');
+       isNotTabbable('#spanTabindexfoo', 'span tabindex foo');
+       isNotTabbable('#spanTabindex3foo', 'span, tabindex 3foo');
+});
+
+})(jQuery);
index 7185ee38e954366a5b1259624ea12ae23c09ae14..ce93ee778f45b13f293b1c776b04607a7e6355e2 100644 (file)
@@ -199,30 +199,22 @@ $.extend($.expr[':'], {
                return !!$.data(elem, match[3]);
        },
 
-       // TODO: add support for object, area
-       tabbable: function(elem) {
-               var nodeName = elem.nodeName.toLowerCase();
-               function isVisible(element) {
-                       return !($(element).is(':hidden') || $(element).parents(':hidden').length);
-               }
-
-               return (
-                       // in tab order
-                       elem.tabIndex >= 0 &&
-
-                       ( // filter node types that participate in the tab order
-
-                               // anchor tag
-                               ('a' == nodeName && elem.href) ||
-
-                               // enabled form element
-                               (/input|select|textarea|button/.test(nodeName) &&
-                                       'hidden' != elem.type && !elem.disabled)
-                       ) &&
+       focusable: function(element) {
+               var nodeName = element.nodeName.toLowerCase(),
+                       tabIndex = $.attr(element, 'tabindex');
+               return (/input|select|textarea|button|object/.test(nodeName)
+                       ? !element.disabled
+                       : 'a' == nodeName || 'area' == nodeName
+                               ? element.href || !isNaN(tabIndex)
+                               : !isNaN(tabIndex))
+                       // the element and all of its ancestors must be visible
+                       // the browser may report that the area is hidden
+                       && !$(element)['area' == nodeName ? 'parents' : 'closest'](':hidden').length;
+       },
 
-                       // visible on page
-                       isVisible(elem)
-               );
+       tabbable: function(element) {
+               var tabIndex = $.attr(element, 'tabindex');
+               return (isNaN(tabIndex) || tabIndex >= 0) && $(element).is(':focusable');
        }
 });