2
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
8
YUI.add('node-focusmanager', function(Y) {
11
* <p>The Focus Manager Node Plugin makes it easy to manage focus among
12
* a Node's descendants. Primarily intended to help with widget development,
13
* the Focus Manager Node Plugin can be used to improve the keyboard
14
* accessibility of widgets.</p>
17
* When designing widgets that manage a set of descendant controls (i.e. buttons
18
* in a toolbar, tabs in a tablist, menuitems in a menu, etc.) it is important to
19
* limit the number of descendants in the browser's default tab flow. The fewer
20
* number of descendants in the default tab flow, the easier it is for keyboard
21
* users to navigate between widgets by pressing the tab key. When a widget has
22
* focus it should provide a set of shortcut keys (typically the arrow keys)
23
* to move focus among its descendants.
27
* To this end, the Focus Manager Node Plugin makes it easy to define a Node's
28
* focusable descendants, define which descendant should be in the default tab
29
* flow, and define the keys that move focus among each descendant.
30
* Additionally, as the CSS
31
* <a href="http://www.w3.org/TR/CSS21/selector.html#x38"><code>:focus</code></a>
32
* pseudo class is not supported on all elements in all
33
* <a href="http://developer.yahoo.com/yui/articles/gbs/">A-Grade browsers</a>,
34
* the Focus Manager Node Plugin provides an easy, cross-browser means of
38
* @module node-focusmanager
41
// Frequently used strings
43
var ACTIVE_DESCENDANT = "activeDescendant",
45
DISABLED = "disabled",
46
TAB_INDEX = "tabIndex",
48
FOCUS_CLASS = "focusClass",
49
CIRCULAR = "circular",
52
ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change",
55
// Collection of keys that, when pressed, cause the browser viewport
77
* The NodeFocusManager class is a plugin for a Node instance. The class is used
78
* via the <a href="Node.html#method_plug"><code>plug</code></a> method of Node
79
* and should not be instantiated directly.
81
* @class NodeFocusManager
83
NodeFocusManager = function () {
85
NodeFocusManager.superclass.constructor.apply(this, arguments);
90
NodeFocusManager.ATTRS = {
93
* Boolean indicating that one of the descendants is focused.
109
* String representing the CSS selector used to define the descendant Nodes
110
* whose focus should be managed.
112
* @attribute descendants
117
getter: function (value) {
119
return this.get(HOST).all(value);
127
* <p>Node, or index of the Node, representing the descendant that is either
128
* focused or is focusable (<code>tabIndex</code> attribute is set to 0).
129
* The value cannot represent a disabled descendant Node. Use a value of -1
130
* to remove all descendant Nodes from the default tab flow.
131
* If no value is specified, the active descendant will be inferred using
132
* the following criteria:</p>
134
* <li>Examining the <code>tabIndex</code> attribute of each descendant and
135
* using the first descendant whose <code>tabIndex</code> attribute is set
137
* <li>If no default can be inferred then the value is set to either 0 or
138
* the index of the first enabled descendant.</li>
141
* @attribute activeDescendant
146
setter: function (value) {
148
var isNumber = Lang.isNumber,
149
INVALID_VALUE = Y.Attribute.INVALID_VALUE,
150
descendantsMap = this._descendantsMap,
151
descendants = this._descendants,
157
if (isNumber(value)) {
159
returnValue = nodeIndex;
161
else if ((value instanceof Y.Node) && descendantsMap) {
163
nodeIndex = descendantsMap[value.get(ID)];
165
if (isNumber(nodeIndex)) {
166
returnValue = nodeIndex;
170
// The user passed a reference to a Node that wasn't one
171
// of the descendants.
172
returnValue = INVALID_VALUE;
178
returnValue = INVALID_VALUE;
184
oNode = descendants.item(nodeIndex);
186
if (oNode && oNode.get("disabled")) {
188
// Setting the "activeDescendant" attribute to the index
189
// of a disabled descendant is invalid.
190
returnValue = INVALID_VALUE;
204
* Object literal representing the keys to be used to navigate between the
205
* next/previous descendant. The format for the attribute's value is
206
* <code>{ next: "down:40", previous: "down:38" }</code>. The value for the
207
* "next" and "previous" properties are used to attach
208
* <a href="event/#keylistener"><code>key</code></a> event listeners. See
209
* the <a href="event/#keylistener">Using the key Event</a> section of
210
* the Event documentation for more information on "key" event listeners.
229
* String representing the name of class applied to the focused active
230
* descendant Node. Can also be an object literal used to define both the
231
* class name, and the Node to which the class should be applied. If using
232
* an object literal, the format is:
233
* <code>{ className: "focus", fn: myFunction }</code>. The function
234
* referenced by the <code>fn</code> property in the object literal will be
235
* passed a reference to the currently focused active descendant Node.
237
* @attribute focusClass
238
* @type String|Object
244
* Boolean indicating if focus should be set to the first/last descendant
245
* when the end or beginning of the descendants has been reached.
247
* @attribute circular
256
Y.extend(NodeFocusManager, Y.Plugin.Base, {
258
// Protected properties
260
// Boolean indicating if the NodeFocusManager is active.
263
// NodeList representing the descendants selected via the
264
// "descendants" attribute.
267
// Object literal mapping the IDs of each descendant to its index in the
268
// "_descendants" NodeList.
269
_descendantsMap: null,
271
// Reference to the Node instance to which the focused class (defined
272
// by the "focusClass" attribute) is currently applied.
275
// Number representing the index of the last descendant Node.
278
// Array of handles for event handlers used for a NodeFocusManager instance.
279
_eventHandlers: null,
286
* @method _initDescendants
287
* @description Sets the <code>tabIndex</code> attribute of all of the
288
* descendants to -1, except the active descendant, whose
289
* <code>tabIndex</code> attribute is set to 0.
292
_initDescendants: function () {
294
var descendants = this.get("descendants"),
298
nActiveDescendant = this.get(ACTIVE_DESCENDANT),
305
if (Lang.isUndefined(nActiveDescendant)) {
306
nActiveDescendant = -1;
312
nDescendants = descendants.size();
315
if (nDescendants > 1) {
317
for (i = 0; i < nDescendants; i++) {
319
oNode = descendants.item(i);
321
if (nFirstEnabled === -1 && !oNode.get(DISABLED)) {
326
// If the user didn't specify a value for the
327
// "activeDescendant" attribute try to infer it from
330
// Need to pass "2" when using "getAttribute" for IE to get
331
// the attribute value as it is set in the markup.
332
// Need to use "parseInt" because IE always returns the
333
// value as a number, whereas all other browsers return
334
// the attribute as a string when accessed
335
// via "getAttribute".
337
if (nActiveDescendant < 0 &&
338
parseInt(oNode.getAttribute(TAB_INDEX, 2), 10) === 0) {
340
nActiveDescendant = i;
344
oNode.set(TAB_INDEX, -1);
353
descendantsMap[sID] = i;
358
// If the user didn't specify a value for the
359
// "activeDescendant" attribute and no default value could be
360
// determined from the markup, then default to 0.
362
if (nActiveDescendant < 0) {
363
nActiveDescendant = 0;
367
oNode = descendants.item(nActiveDescendant);
369
// Check to make sure the active descendant isn't disabled,
370
// and fall back to the first enabled descendant if it is.
372
if (!oNode || oNode.get(DISABLED)) {
373
oNode = descendants.item(nFirstEnabled);
374
nActiveDescendant = nFirstEnabled;
377
this._lastNodeIndex = nDescendants - 1;
378
this._descendants = descendants;
379
this._descendantsMap = descendantsMap;
381
this.set(ACTIVE_DESCENDANT, nActiveDescendant);
383
// Need to set the "tabIndex" attribute here, since the
384
// "activeDescendantChange" event handler used to manage
385
// the setting of the "tabIndex" attribute isn't wired up yet.
387
oNode.set(TAB_INDEX, 0);
397
* @method _isDescendant
398
* @description Determines if the specified Node instance is a descendant
399
* managed by the Focus Manager.
400
* @param node {Node} Node instance to be checked.
401
* @return {Boolean} Boolean indicating if the specified Node instance is a
402
* descendant managed by the Focus Manager.
405
_isDescendant: function (node) {
407
return (node.get(ID) in this._descendantsMap);
413
* @method _removeFocusClass
414
* @description Removes the class name representing focus (as specified by
415
* the "focusClass" attribute) from the Node instance to which it is
419
_removeFocusClass: function () {
421
var oFocusedNode = this._focusedNode,
422
focusClass = this.get(FOCUS_CLASS),
426
sClassName = Lang.isString(focusClass) ?
427
focusClass : focusClass.className;
430
if (oFocusedNode && sClassName) {
431
oFocusedNode.removeClass(sClassName);
438
* @method _detachKeyHandler
439
* @description Detaches the "key" event handlers used to support the "keys"
443
_detachKeyHandler: function () {
445
var prevKeyHandler = this._prevKeyHandler,
446
nextKeyHandler = this._nextKeyHandler;
448
if (prevKeyHandler) {
449
prevKeyHandler.detach();
452
if (nextKeyHandler) {
453
nextKeyHandler.detach();
460
* @method _preventScroll
461
* @description Prevents the viewport from scolling when the user presses
462
* the up, down, left, or right key.
465
_preventScroll: function (event) {
467
if (scrollKeys[event.keyCode]) {
468
event.preventDefault();
475
* @method _preventScroll
476
* @description Fires the click event if the enter key is pressed while
477
* focused on an HTML element that is not natively clickable.
480
_fireClick: function (event) {
482
var oTarget = event.target,
483
sNodeName = oTarget.get("nodeName").toLowerCase();
485
if (event.keyCode === 13 && (!clickableElements[sNodeName] ||
486
(sNodeName === "a" && !oTarget.getAttribute("href")))) {
489
oTarget.simulate("click");
497
* @method _attachKeyHandler
498
* @description Attaches the "key" event handlers used to support the "keys"
502
_attachKeyHandler: function () {
504
this._detachKeyHandler();
506
var sNextKey = this.get("keys.next"),
507
sPrevKey = this.get("keys.previous"),
508
oNode = this.get(HOST),
509
aHandlers = this._eventHandlers;
512
this._prevKeyHandler =
513
Y.on(KEY, Y.bind(this._focusPrevious, this), oNode, sPrevKey);
517
this._nextKeyHandler =
518
Y.on(KEY, Y.bind(this._focusNext, this), oNode, sNextKey);
522
// In Opera it is necessary to call the "preventDefault" method in
523
// response to the user pressing the arrow keys in order to prevent
524
// the viewport from scrolling when the user is moving focus among
525
// the focusable descendants.
528
aHandlers.push(oNode.on("keypress", this._preventScroll, this));
532
// For all browsers except Opera: HTML elements that are not natively
533
// focusable but made focusable via the tabIndex attribute don't
534
// fire a click event when the user presses the enter key. It is
535
// possible to work around this problem by simplying dispatching a
536
// click event in response to the user pressing the enter key.
539
aHandlers.push(oNode.on("keypress", this._fireClick, this));
546
* @method _detachEventHandlers
547
* @description Detaches all event handlers used by the Focus Manager.
550
_detachEventHandlers: function () {
552
this._detachKeyHandler();
554
var aHandlers = this._eventHandlers;
558
Y.Array.each(aHandlers, function (handle) {
562
this._eventHandlers = null;
570
* @method _detachEventHandlers
571
* @description Attaches all event handlers used by the Focus Manager.
574
_attachEventHandlers: function () {
576
var descendants = this._descendants,
581
if (descendants && descendants.size() > 1) {
583
aHandlers = this._eventHandlers || [];
584
oDocument = this.get(HOST).get("ownerDocument");
587
if (aHandlers.length === 0) {
590
aHandlers.push(oDocument.on("focus", this._onDocFocus, this));
592
aHandlers.push(oDocument.on("mousedown",
593
this._onDocMouseDown, this));
596
this.after("keysChange", this._attachKeyHandler));
599
this.after("descendantsChange", this._initDescendants));
602
this.after(ACTIVE_DESCENDANT_CHANGE,
603
this._afterActiveDescendantChange));
606
// For performance: defer attaching all key-related event
607
// handlers until the first time one of the specified
608
// descendants receives focus.
610
handle = this.after("focusedChange", Y.bind(function (event) {
615
this._attachKeyHandler();
617
// Detach this "focusedChange" handler so that the
618
// key-related handlers only get attached once.
626
aHandlers.push(handle);
631
this._eventHandlers = aHandlers;
638
// Protected event handlers
641
* @method _onDocMouseDown
642
* @description "mousedown" event handler for the owner document of the
643
* Focus Manager's Node.
645
* @param event {Object} Object representing the DOM event.
647
_onDocMouseDown: function (event) {
649
var oHost = this.get(HOST),
650
oTarget = event.target,
651
bChildNode = oHost.contains(oTarget),
654
getFocusable = function (node) {
656
var returnVal = false;
658
if (!node.compareTo(oHost)) {
660
returnVal = this._isDescendant(node) ? node :
661
getFocusable.call(this, node.get("parentNode"));
672
// Check to make sure that the target isn't a child node of one
673
// of the focusable descendants.
675
node = getFocusable.call(this, oTarget);
680
else if (!node && this.get(FOCUSED)) {
682
// The target was a non-focusable descendant of the root
683
// node, so the "focused" attribute should be set to false.
685
this._set(FOCUSED, false);
686
this._onDocFocus(event);
693
if (bChildNode && this._isDescendant(oTarget)) {
695
// Fix general problem in Webkit: mousing down on a button or an
696
// anchor element doesn't focus it.
698
// For all browsers: makes sure that the descendant that
699
// was the target of the mousedown event is now considered the
700
// active descendant.
704
else if (UA.webkit && this.get(FOCUSED) &&
705
(!bChildNode || (bChildNode && !this._isDescendant(oTarget)))) {
709
// Document doesn't receive focus in Webkit when the user mouses
710
// down on it, so the "focused" attribute won't get set to the
713
// The goal is to force a blur if the user moused down on
714
// either: 1) A descendant node, but not one that managed by
715
// the FocusManager, or 2) an element outside of the
718
this._set(FOCUSED, false);
719
this._onDocFocus(event);
727
* @method _onDocFocus
728
* @description "focus" event handler for the owner document of the
729
* Focus Manager's Node.
731
* @param event {Object} Object representing the DOM event.
733
_onDocFocus: function (event) {
735
var oTarget = this._focusTarget || event.target,
736
bFocused = this.get(FOCUSED),
737
focusClass = this.get(FOCUS_CLASS),
738
oFocusedNode = this._focusedNode,
741
if (this._focusTarget) {
742
this._focusTarget = null;
746
if (this.get(HOST).contains(oTarget)) {
748
// The target is a descendant of the root Node.
750
bInCollection = this._isDescendant(oTarget);
752
if (!bFocused && bInCollection) {
754
// The user has focused a focusable descendant.
759
else if (bFocused && !bInCollection) {
761
// The user has focused a child of the root Node that is
762
// not one of the descendants managed by this Focus Manager
763
// so clear the currently focused descendant.
772
// The target is some other node in the document.
781
if (oFocusedNode && (!oFocusedNode.compareTo(oTarget) || !bFocused)) {
782
this._removeFocusClass();
785
if (bInCollection && bFocused) {
788
oTarget = focusClass.fn(oTarget);
789
oTarget.addClass(focusClass.className);
792
oTarget.addClass(focusClass);
795
this._focusedNode = oTarget;
802
this._set(FOCUSED, bFocused);
809
* @description Keydown event handler that moves focus to the next
810
* enabled descendant.
812
* @param event {Object} Object representing the DOM event.
813
* @param activeDescendant {Number} Number representing the index of the
814
* next descendant to be focused
816
_focusNext: function (event, activeDescendant) {
818
var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
822
if (this._isDescendant(event.target) &&
823
(nActiveDescendant <= this._lastNodeIndex)) {
825
nActiveDescendant = nActiveDescendant + 1;
827
if (nActiveDescendant === (this._lastNodeIndex + 1) &&
828
this.get(CIRCULAR)) {
830
nActiveDescendant = 0;
834
oNode = this._descendants.item(nActiveDescendant);
836
if (oNode.get(DISABLED)) {
837
this._focusNext(event, nActiveDescendant);
840
this.focus(nActiveDescendant);
845
this._preventScroll(event);
851
* @method _focusPrevious
852
* @description Keydown event handler that moves focus to the previous
853
* enabled descendant.
855
* @param event {Object} Object representing the DOM event.
856
* @param activeDescendant {Number} Number representing the index of the
857
* next descendant to be focused.
859
_focusPrevious: function (event, activeDescendant) {
861
var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
864
if (this._isDescendant(event.target) && nActiveDescendant >= 0) {
866
nActiveDescendant = nActiveDescendant - 1;
868
if (nActiveDescendant === -1 && this.get(CIRCULAR)) {
869
nActiveDescendant = this._lastNodeIndex;
872
oNode = this._descendants.item(nActiveDescendant);
874
if (oNode.get(DISABLED)) {
875
this._focusPrevious(event, nActiveDescendant);
878
this.focus(nActiveDescendant);
883
this._preventScroll(event);
889
* @method _afterActiveDescendantChange
890
* @description afterChange event handler for the
891
* "activeDescendant" attribute.
893
* @param event {Object} Object representing the change event.
895
_afterActiveDescendantChange: function (event) {
897
var oNode = this._descendants.item(event.prevVal);
900
oNode.set(TAB_INDEX, -1);
903
oNode = this._descendants.item(event.newVal);
906
oNode.set(TAB_INDEX, 0);
915
initializer: function (config) {
921
destructor: function () {
924
this.get(HOST).focusManager = null;
931
* @description Focuses the active descendant and sets the
932
* <code>focused</code> attribute to true.
933
* @param index {Number} Optional. Number representing the index of the
934
* descendant to be set as the active descendant.
935
* @param index {Node} Optional. Node instance representing the
936
* descendant to be set as the active descendant.
938
focus: function (index) {
940
if (Lang.isUndefined(index)) {
941
index = this.get(ACTIVE_DESCENDANT);
944
this.set(ACTIVE_DESCENDANT, index, { src: UI });
946
var oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
952
// In Opera focusing a <BUTTON> element programmatically
953
// will result in the document-level focus event handler
954
// "_onDocFocus" being called, resulting in the handler
955
// incorrectly setting the "focused" Attribute to false. To fix
956
// this, set a flag ("_focusTarget") that the "_onDocFocus" method
957
// can look for to properly handle this edge case.
959
if (UA.opera && oNode.get("nodeName").toLowerCase() === "button") {
960
this._focusTarget = oNode;
970
* @description Blurs the current active descendant and sets the
971
* <code>focused</code> attribute to false.
977
if (this.get(FOCUSED)) {
979
oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
985
// For Opera and Webkit: Blurring an element in either browser
986
// doesn't result in another element (such as the document)
987
// being focused. Therefore, the "_onDocFocus" method
988
// responsible for managing the application and removal of the
989
// focus indicator class name is never called.
991
this._removeFocusClass();
995
this._set(FOCUSED, false, { src: UI });
1003
* @description Enables the Focus Manager.
1005
start: function () {
1007
if (this._stopped) {
1009
this._initDescendants();
1010
this._attachEventHandlers();
1012
this._stopped = false;
1021
* @description Disables the Focus Manager by detaching all event handlers.
1025
if (!this._stopped) {
1027
this._detachEventHandlers();
1029
this._descendants = null;
1030
this._focusedNode = null;
1031
this._lastNodeIndex = 0;
1032
this._stopped = true;
1041
* @description Refreshes the Focus Manager's descendants by re-executing the
1042
* CSS selector query specified by the <code>descendants</code> attribute.
1044
refresh: function () {
1046
this._initDescendants();
1053
NodeFocusManager.NAME = "nodeFocusManager";
1054
NodeFocusManager.NS = "focusManager";
1056
Y.namespace("Plugin");
1057
Y.Plugin.NodeFocusManager = NodeFocusManager;
1060
}, '3.0.0' ,{requires:['attribute', 'node', 'plugin', 'node-event-simulate', 'event-key', 'event-focus']});