diff options
author | Scott González <scott.gonzalez@gmail.com> | 2009-01-21 03:25:02 +0000 |
---|---|---|
committer | Scott González <scott.gonzalez@gmail.com> | 2009-01-21 03:25:02 +0000 |
commit | f80d9eb465e428b3900bc0b741392f14ecd859f0 (patch) | |
tree | d7e62555d0ebc4c05bf3ec881bff78428ade71c5 | |
parent | debb342662dd669224e56e358e87fc37605b9d70 (diff) | |
download | jquery-ui-f80d9eb465e428b3900bc0b741392f14ecd859f0.tar.gz jquery-ui-f80d9eb465e428b3900bc0b741392f14ecd859f0.zip |
Core: Partial fix for #3559: Proper implementation for :focusable and :tabbable selectors.
-rw-r--r-- | tests/unit/core/core.html | 104 | ||||
-rw-r--r-- | tests/unit/core/core.js | 50 | ||||
-rw-r--r-- | tests/unit/core/selector.js | 184 | ||||
-rw-r--r-- | ui/ui.core.js | 38 |
4 files changed, 269 insertions, 107 deletions
diff --git a/tests/unit/core/core.html b/tests/unit/core/core.html index d0ff8fbad..cf1037027 100644 --- a/tests/unit/core/core.html +++ b/tests/unit/core/core.html @@ -12,50 +12,86 @@ <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> diff --git a/tests/unit/core/core.js b/tests/unit/core/core.js index 4be475486..53f392370 100644 --- a/tests/unit/core/core.js +++ b/tests/unit/core/core.js @@ -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 index 000000000..73df0c3fc --- /dev/null +++ b/tests/unit/core/selector.js @@ -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); diff --git a/ui/ui.core.js b/ui/ui.core.js index 7185ee38e..ce93ee778 100644 --- a/ui/ui.core.js +++ b/ui/ui.core.js @@ -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'); } }); |