2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('node-focusmanager', function (Y, NAME) {
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
39 DEPRECATED: The FocusManager Node Plugin has been deprecated as of YUI 3.9.0. This module will be removed from the library in a future version. If you require functionality similar to the one provided by this module, consider taking a look at the various modules in the YUI Gallery <http://yuilibrary.com/gallery/>.
41 * @module node-focusmanager
45 // Frequently used strings
47 var ACTIVE_DESCENDANT = "activeDescendant",
49 DISABLED = "disabled",
50 TAB_INDEX = "tabIndex",
52 FOCUS_CLASS = "focusClass",
53 CIRCULAR = "circular",
56 ACTIVE_DESCENDANT_CHANGE = ACTIVE_DESCENDANT + "Change",
59 // Collection of keys that, when pressed, cause the browser viewport
81 * The NodeFocusManager class is a plugin for a Node instance. The class is used
82 * via the <a href="Node.html#method_plug"><code>plug</code></a> method of Node
83 * and should not be instantiated directly.
85 * @class NodeFocusManager
87 NodeFocusManager = function () {
89 NodeFocusManager.superclass.constructor.apply(this, arguments);
94 NodeFocusManager.ATTRS = {
97 * Boolean indicating that one of the descendants is focused.
113 * String representing the CSS selector used to define the descendant Nodes
114 * whose focus should be managed.
116 * @attribute descendants
121 getter: function (value) {
123 return this.get(HOST).all(value);
131 * <p>Node, or index of the Node, representing the descendant that is either
132 * focused or is focusable (<code>tabIndex</code> attribute is set to 0).
133 * The value cannot represent a disabled descendant Node. Use a value of -1
134 * to remove all descendant Nodes from the default tab flow.
135 * If no value is specified, the active descendant will be inferred using
136 * the following criteria:</p>
138 * <li>Examining the <code>tabIndex</code> attribute of each descendant and
139 * using the first descendant whose <code>tabIndex</code> attribute is set
141 * <li>If no default can be inferred then the value is set to either 0 or
142 * the index of the first enabled descendant.</li>
145 * @attribute activeDescendant
150 setter: function (value) {
152 var isNumber = Lang.isNumber,
153 INVALID_VALUE = Y.Attribute.INVALID_VALUE,
154 descendantsMap = this._descendantsMap,
155 descendants = this._descendants,
161 if (isNumber(value)) {
163 returnValue = nodeIndex;
165 else if ((value instanceof Y.Node) && descendantsMap) {
167 nodeIndex = descendantsMap[value.get(ID)];
169 if (isNumber(nodeIndex)) {
170 returnValue = nodeIndex;
174 // The user passed a reference to a Node that wasn't one
175 // of the descendants.
176 returnValue = INVALID_VALUE;
182 returnValue = INVALID_VALUE;
188 oNode = descendants.item(nodeIndex);
190 if (oNode && oNode.get("disabled")) {
192 // Setting the "activeDescendant" attribute to the index
193 // of a disabled descendant is invalid.
194 returnValue = INVALID_VALUE;
209 * Object literal representing the keys to be used to navigate between the
210 * next/previous descendant. The format for the attribute's value is
211 * <code>{ next: "down:40", previous: "down:38" }</code>. The value for the
212 * "next" and "previous" properties are used to attach
213 * <a href="event/#keylistener"><code>key</code></a> event listeners. See
214 * the <a href="event/#keylistener">Using the key Event</a> section of
215 * the Event documentation for more information on "key" event listeners.
234 * String representing the name of class applied to the focused active
235 * descendant Node. Can also be an object literal used to define both the
236 * class name, and the Node to which the class should be applied. If using
237 * an object literal, the format is:
238 * <code>{ className: "focus", fn: myFunction }</code>. The function
239 * referenced by the <code>fn</code> property in the object literal will be
240 * passed a reference to the currently focused active descendant Node.
242 * @attribute focusClass
243 * @type String|Object
249 * Boolean indicating if focus should be set to the first/last descendant
250 * when the end or beginning of the descendants has been reached.
252 * @attribute circular
262 Y.extend(NodeFocusManager, Y.Plugin.Base, {
264 // Protected properties
266 // Boolean indicating if the NodeFocusManager is active.
269 // NodeList representing the descendants selected via the
270 // "descendants" attribute.
273 // Object literal mapping the IDs of each descendant to its index in the
274 // "_descendants" NodeList.
275 _descendantsMap: null,
277 // Reference to the Node instance to which the focused class (defined
278 // by the "focusClass" attribute) is currently applied.
281 // Number representing the index of the last descendant Node.
284 // Array of handles for event handlers used for a NodeFocusManager instance.
285 _eventHandlers: null,
292 * @method _initDescendants
293 * @description Sets the <code>tabIndex</code> attribute of all of the
294 * descendants to -1, except the active descendant, whose
295 * <code>tabIndex</code> attribute is set to 0.
298 _initDescendants: function () {
300 var descendants = this.get("descendants"),
304 nActiveDescendant = this.get(ACTIVE_DESCENDANT),
311 if (Lang.isUndefined(nActiveDescendant)) {
312 nActiveDescendant = -1;
318 nDescendants = descendants.size();
321 for (i = 0; i < nDescendants; i++) {
323 oNode = descendants.item(i);
325 if (nFirstEnabled === -1 && !oNode.get(DISABLED)) {
330 // If the user didn't specify a value for the
331 // "activeDescendant" attribute try to infer it from
334 // Need to pass "2" when using "getAttribute" for IE to get
335 // the attribute value as it is set in the markup.
336 // Need to use "parseInt" because IE always returns the
337 // value as a number, whereas all other browsers return
338 // the attribute as a string when accessed
339 // via "getAttribute".
341 if (nActiveDescendant < 0 &&
342 parseInt(oNode.getAttribute(TAB_INDEX, 2), 10) === 0) {
344 nActiveDescendant = i;
349 oNode.set(TAB_INDEX, -1);
359 descendantsMap[sID] = i;
364 // If the user didn't specify a value for the
365 // "activeDescendant" attribute and no default value could be
366 // determined from the markup, then default to 0.
368 if (nActiveDescendant < 0) {
369 nActiveDescendant = 0;
373 oNode = descendants.item(nActiveDescendant);
375 // Check to make sure the active descendant isn't disabled,
376 // and fall back to the first enabled descendant if it is.
378 if (!oNode || oNode.get(DISABLED)) {
379 oNode = descendants.item(nFirstEnabled);
380 nActiveDescendant = nFirstEnabled;
383 this._lastNodeIndex = nDescendants - 1;
384 this._descendants = descendants;
385 this._descendantsMap = descendantsMap;
387 this.set(ACTIVE_DESCENDANT, nActiveDescendant);
389 // Need to set the "tabIndex" attribute here, since the
390 // "activeDescendantChange" event handler used to manage
391 // the setting of the "tabIndex" attribute isn't wired up yet.
394 oNode.set(TAB_INDEX, 0);
403 * @method _isDescendant
404 * @description Determines if the specified Node instance is a descendant
405 * managed by the Focus Manager.
406 * @param node {Node} Node instance to be checked.
407 * @return {Boolean} Boolean indicating if the specified Node instance is a
408 * descendant managed by the Focus Manager.
411 _isDescendant: function (node) {
413 return (node.get(ID) in this._descendantsMap);
419 * @method _removeFocusClass
420 * @description Removes the class name representing focus (as specified by
421 * the "focusClass" attribute) from the Node instance to which it is
425 _removeFocusClass: function () {
427 var oFocusedNode = this._focusedNode,
428 focusClass = this.get(FOCUS_CLASS),
432 sClassName = Lang.isString(focusClass) ?
433 focusClass : focusClass.className;
436 if (oFocusedNode && sClassName) {
437 oFocusedNode.removeClass(sClassName);
444 * @method _detachKeyHandler
445 * @description Detaches the "key" event handlers used to support the "keys"
449 _detachKeyHandler: function () {
451 var prevKeyHandler = this._prevKeyHandler,
452 nextKeyHandler = this._nextKeyHandler;
454 if (prevKeyHandler) {
455 prevKeyHandler.detach();
458 if (nextKeyHandler) {
459 nextKeyHandler.detach();
466 * @method _preventScroll
467 * @description Prevents the viewport from scolling when the user presses
468 * the up, down, left, or right key.
471 _preventScroll: function (event) {
473 if (scrollKeys[event.keyCode] && this._isDescendant(event.target)) {
474 event.preventDefault();
482 * @description Fires the click event if the enter key is pressed while
483 * focused on an HTML element that is not natively clickable.
486 _fireClick: function (event) {
488 var oTarget = event.target,
489 sNodeName = oTarget.get("nodeName").toLowerCase();
491 if (event.keyCode === 13 && (!clickableElements[sNodeName] ||
492 (sNodeName === "a" && !oTarget.getAttribute("href")))) {
495 oTarget.simulate("click");
503 * @method _attachKeyHandler
504 * @description Attaches the "key" event handlers used to support the "keys"
508 _attachKeyHandler: function () {
510 this._detachKeyHandler();
512 var sNextKey = this.get("keys.next"),
513 sPrevKey = this.get("keys.previous"),
514 oNode = this.get(HOST),
515 aHandlers = this._eventHandlers;
518 this._prevKeyHandler =
519 Y.on(KEY, Y.bind(this._focusPrevious, this), oNode, sPrevKey);
523 this._nextKeyHandler =
524 Y.on(KEY, Y.bind(this._focusNext, this), oNode, sNextKey);
528 // In Opera it is necessary to call the "preventDefault" method in
529 // response to the user pressing the arrow keys in order to prevent
530 // the viewport from scrolling when the user is moving focus among
531 // the focusable descendants.
534 aHandlers.push(oNode.on("keypress", this._preventScroll, this));
538 // For all browsers except Opera: HTML elements that are not natively
539 // focusable but made focusable via the tabIndex attribute don't
540 // fire a click event when the user presses the enter key. It is
541 // possible to work around this problem by simplying dispatching a
542 // click event in response to the user pressing the enter key.
545 aHandlers.push(oNode.on("keypress", this._fireClick, this));
552 * @method _detachEventHandlers
553 * @description Detaches all event handlers used by the Focus Manager.
556 _detachEventHandlers: function () {
558 this._detachKeyHandler();
560 var aHandlers = this._eventHandlers;
564 Y.Array.each(aHandlers, function (handle) {
568 this._eventHandlers = null;
576 * @method _detachEventHandlers
577 * @description Attaches all event handlers used by the Focus Manager.
580 _attachEventHandlers: function () {
582 var descendants = this._descendants,
587 if (descendants && descendants.size()) {
589 aHandlers = this._eventHandlers || [];
590 oDocument = this.get(HOST).get("ownerDocument");
593 if (aHandlers.length === 0) {
596 aHandlers.push(oDocument.on("focus", this._onDocFocus, this));
598 aHandlers.push(oDocument.on("mousedown",
599 this._onDocMouseDown, this));
602 this.after("keysChange", this._attachKeyHandler));
605 this.after("descendantsChange", this._initDescendants));
608 this.after(ACTIVE_DESCENDANT_CHANGE,
609 this._afterActiveDescendantChange));
612 // For performance: defer attaching all key-related event
613 // handlers until the first time one of the specified
614 // descendants receives focus.
616 handle = this.after("focusedChange", Y.bind(function (event) {
621 this._attachKeyHandler();
623 // Detach this "focusedChange" handler so that the
624 // key-related handlers only get attached once.
632 aHandlers.push(handle);
637 this._eventHandlers = aHandlers;
644 // Protected event handlers
647 * @method _onDocMouseDown
648 * @description "mousedown" event handler for the owner document of the
649 * Focus Manager's Node.
651 * @param event {Object} Object representing the DOM event.
653 _onDocMouseDown: function (event) {
655 var oHost = this.get(HOST),
656 oTarget = event.target,
657 bChildNode = oHost.contains(oTarget),
660 getFocusable = function (node) {
662 var returnVal = false;
664 if (!node.compareTo(oHost)) {
666 returnVal = this._isDescendant(node) ? node :
667 getFocusable.call(this, node.get("parentNode"));
678 // Check to make sure that the target isn't a child node of one
679 // of the focusable descendants.
681 node = getFocusable.call(this, oTarget);
686 else if (!node && this.get(FOCUSED)) {
688 // The target was a non-focusable descendant of the root
689 // node, so the "focused" attribute should be set to false.
691 this._set(FOCUSED, false);
692 this._onDocFocus(event);
699 if (bChildNode && this._isDescendant(oTarget)) {
701 // Fix general problem in Webkit: mousing down on a button or an
702 // anchor element doesn't focus it.
704 // For all browsers: makes sure that the descendant that
705 // was the target of the mousedown event is now considered the
706 // active descendant.
710 else if (UA.webkit && this.get(FOCUSED) &&
711 (!bChildNode || (bChildNode && !this._isDescendant(oTarget)))) {
715 // Document doesn't receive focus in Webkit when the user mouses
716 // down on it, so the "focused" attribute won't get set to the
719 // The goal is to force a blur if the user moused down on
720 // either: 1) A descendant node, but not one that managed by
721 // the FocusManager, or 2) an element outside of the
724 this._set(FOCUSED, false);
725 this._onDocFocus(event);
733 * @method _onDocFocus
734 * @description "focus" event handler for the owner document of the
735 * Focus Manager's Node.
737 * @param event {Object} Object representing the DOM event.
739 _onDocFocus: function (event) {
741 var oTarget = this._focusTarget || event.target,
742 bFocused = this.get(FOCUSED),
743 focusClass = this.get(FOCUS_CLASS),
744 oFocusedNode = this._focusedNode,
747 if (this._focusTarget) {
748 this._focusTarget = null;
752 if (this.get(HOST).contains(oTarget)) {
754 // The target is a descendant of the root Node.
756 bInCollection = this._isDescendant(oTarget);
758 if (!bFocused && bInCollection) {
760 // The user has focused a focusable descendant.
765 else if (bFocused && !bInCollection) {
767 // The user has focused a child of the root Node that is
768 // not one of the descendants managed by this Focus Manager
769 // so clear the currently focused descendant.
778 // The target is some other node in the document.
787 if (oFocusedNode && (!oFocusedNode.compareTo(oTarget) || !bFocused)) {
788 this._removeFocusClass();
791 if (bInCollection && bFocused) {
794 oTarget = focusClass.fn(oTarget);
795 oTarget.addClass(focusClass.className);
798 oTarget.addClass(focusClass);
801 this._focusedNode = oTarget;
808 this._set(FOCUSED, bFocused);
815 * @description Keydown event handler that moves focus to the next
816 * enabled descendant.
818 * @param event {Object} Object representing the DOM event.
819 * @param activeDescendant {Number} Number representing the index of the
820 * next descendant to be focused
822 _focusNext: function (event, activeDescendant) {
824 var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
828 if (this._isDescendant(event.target) &&
829 (nActiveDescendant <= this._lastNodeIndex)) {
831 nActiveDescendant = nActiveDescendant + 1;
833 if (nActiveDescendant === (this._lastNodeIndex + 1) &&
834 this.get(CIRCULAR)) {
836 nActiveDescendant = 0;
840 oNode = this._descendants.item(nActiveDescendant);
844 if (oNode.get("disabled")) {
845 this._focusNext(event, nActiveDescendant);
848 this.focus(nActiveDescendant);
855 this._preventScroll(event);
861 * @method _focusPrevious
862 * @description Keydown event handler that moves focus to the previous
863 * enabled descendant.
865 * @param event {Object} Object representing the DOM event.
866 * @param activeDescendant {Number} Number representing the index of the
867 * next descendant to be focused.
869 _focusPrevious: function (event, activeDescendant) {
871 var nActiveDescendant = activeDescendant || this.get(ACTIVE_DESCENDANT),
874 if (this._isDescendant(event.target) && nActiveDescendant >= 0) {
876 nActiveDescendant = nActiveDescendant - 1;
878 if (nActiveDescendant === -1 && this.get(CIRCULAR)) {
879 nActiveDescendant = this._lastNodeIndex;
882 oNode = this._descendants.item(nActiveDescendant);
886 if (oNode.get("disabled")) {
887 this._focusPrevious(event, nActiveDescendant);
890 this.focus(nActiveDescendant);
897 this._preventScroll(event);
903 * @method _afterActiveDescendantChange
904 * @description afterChange event handler for the
905 * "activeDescendant" attribute.
907 * @param event {Object} Object representing the change event.
909 _afterActiveDescendantChange: function (event) {
911 var oNode = this._descendants.item(event.prevVal);
914 oNode.set(TAB_INDEX, -1);
917 oNode = this._descendants.item(event.newVal);
920 oNode.set(TAB_INDEX, 0);
929 initializer: function (config) {
934 destructor: function () {
937 this.get(HOST).focusManager = null;
944 * @description Focuses the active descendant and sets the
945 * <code>focused</code> attribute to true.
946 * @param index {Number} Optional. Number representing the index of the
947 * descendant to be set as the active descendant.
948 * @param index {Node} Optional. Node instance representing the
949 * descendant to be set as the active descendant.
951 focus: function (index) {
953 if (Lang.isUndefined(index)) {
954 index = this.get(ACTIVE_DESCENDANT);
957 this.set(ACTIVE_DESCENDANT, index, { src: UI });
959 var oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
965 // In Opera focusing a <BUTTON> element programmatically
966 // will result in the document-level focus event handler
967 // "_onDocFocus" being called, resulting in the handler
968 // incorrectly setting the "focused" Attribute to false. To fix
969 // this, set a flag ("_focusTarget") that the "_onDocFocus" method
970 // can look for to properly handle this edge case.
972 if (UA.opera && oNode.get("nodeName").toLowerCase() === "button") {
973 this._focusTarget = oNode;
983 * @description Blurs the current active descendant and sets the
984 * <code>focused</code> attribute to false.
990 if (this.get(FOCUSED)) {
992 oNode = this._descendants.item(this.get(ACTIVE_DESCENDANT));
998 // For Opera and Webkit: Blurring an element in either browser
999 // doesn't result in another element (such as the document)
1000 // being focused. Therefore, the "_onDocFocus" method
1001 // responsible for managing the application and removal of the
1002 // focus indicator class name is never called.
1004 this._removeFocusClass();
1008 this._set(FOCUSED, false, { src: UI });
1016 * @description Enables the Focus Manager.
1018 start: function () {
1020 if (this._stopped) {
1022 this._initDescendants();
1023 this._attachEventHandlers();
1025 this._stopped = false;
1034 * @description Disables the Focus Manager by detaching all event handlers.
1038 if (!this._stopped) {
1040 this._detachEventHandlers();
1042 this._descendants = null;
1043 this._focusedNode = null;
1044 this._lastNodeIndex = 0;
1045 this._stopped = true;
1054 * @description Refreshes the Focus Manager's descendants by re-executing the
1055 * CSS selector query specified by the <code>descendants</code> attribute.
1057 refresh: function () {
1059 this._initDescendants();
1061 if (!this._eventHandlers) {
1062 this._attachEventHandlers();
1070 NodeFocusManager.NAME = "nodeFocusManager";
1071 NodeFocusManager.NS = "focusManager";
1073 Y.namespace("Plugin");
1074 Y.Plugin.NodeFocusManager = NodeFocusManager;
1077 }, '3.13.0', {"requires": ["attribute", "node", "plugin", "node-event-simulate", "event-key", "event-focus"]});