Tab Key Managed Widget

The TabKeyManagedWidget was originally a widget I wrote in YUI 2 for the popups on Mint.com. I have changed it a lot sense then, especially in its conversion to YUI 3. The problem we solved, is that sometimes, especially when using in-page popups, one must prevent users from being able to tab through the entire page. So we bound the end-user to a tabbing context, such as a popup or form, preventing the browser from executing its default behavior.

Once this widget is instantiated, it will listen for end-user click and keydown:tab events on the document. When a click event fires, it evaluates if the event target is a descendant of any of the instantiated TabKeyManagedWidget nodes. If so, then keydown:tab will be bound to fields found inside of that element. Otherwise, keydown:tab will behave normally, as defined by the browser/OS.

In YUI 3 TabKeyManagedWidget extends the "Y.Base" and therefore leverages the set/get methods and ATTR object.

Example 1: TabKeyManagedWidget Constants

// the name of the widget, used by Y.Node
_F.NAME = "tabKeyManagedWidget";

// the default attributes of widget, used by Y.Node
_F.ATTRS = {
	// When active this widget uses document clicking to set the form for tabbing.
	autoFocus: {
		value: true
	},

	// A collection of current fields for tabbing.
	fieldsForTabbing: {
		value: []
	},

	// The HTML className to apply to elements when focused.
	focusClass: {
		value: focus
	},

	// The node to bind this widget.
	node: {
		setter: function(node) {
			var n = Node.get(node); 
			if (!n) {
				Y.fail(TabKeyManagedWidget: Invalid Node Given:  + node);
			}
			return n;
		}
	},

	// The selector string to find elements for tabbing.
	tabElements: {
		value: a, textarea, input, select
//		value: input
	},

	// The current index in the collection of tabbing fields.
	tabIndex: {
		value: 0
	},

	// The waits to focus on a tab event until the previous BLUR event fires.
	waitForBlur: {
		value: true
	}
};

// the event that fires before tabbing, if the callback function returns false, tabbing does not happen
_F.CE_BEFORE_TAB = beforeTab;

// the event that fires after tabbing
_F.CE_AFTER_TAB = afterTab;

There are a lot of properties for this widget: autoFocus, fieldsForTabbing, focusClass, node, tabElements, tabIndex, waitForBlur. These properties can be overridden by the configuration object passed into the constructor, or by using set method of an instantiated widget. The autoFocus property is used when tabbing should be bound to the fields inside of node anytime a click event occurs somewhere inside of that node (this is on by default). Otherwise, the developer will need to manage binding by calling the public toggleBinding method on a TabKeyManagedWidget instance. The fieldsForTabbing and tabIndex are internal properties used by the widget to know what fields should be tabbed through and the current field index. The focusClass is a CSS class to be applied automatically to a element when focused on. The node is the root node of all the fields that should be managed; this should always be provided to the constructor. The tabElements is a comma separated list of all HTML tags that should be included for tabbing; by default this is a, textarea, input, and select elements. The waitForBlur property will wait for the blur event of the previously focused element to fire before focusing on the next element; this is useful if you have other listeners attached to an element that are not managed by this widget. There are two custom events that will fire before and after tabbing. If the callback function of CE_BEFORE_TAB returns false, then the tabbing is canceled.

The "Y.Node" object that TabKeyManagedWidget inherits from, provides some syntactic sugar by calling the initializer method during instantiation and destructor methods when deleting. The widget leverages this to fully initialize the tabElements (the tags need to be bound to the form), and to register the widget with the shared widget manager.

Example 2: Initializer and Destructor

destructor: function() {
		_sharedObject.destroy(this);
    },
    initializer: function(conf) {
		var tabElements = this.get(tabElements).split(,),
			idprefix = # + this.get(node).get(id) + ;
		_sharedObject.init(this);
		this.set(tabElements, idprefix + tabElements.join(, + idprefix));
    },

The _sharedObject is used shared by all instances of TabKeyManagedWidget, handling the events attached to the document and maintaining a list of all TabKeyManagedWidget instances. This should probably be another object that inherits from "Y.Node", which would allow custom event bubbling from TabKeyManagedWidget instances, but I ran out of time trying to get TabKeyManagedWidget converted to YUI 3.

Example 3: _sharedObject

_sharedObject = {
	// The current tab widget.
	_currentWidget: null,

	// Handle the document click, attempt to find a valid tab widget.
	_handleDocumentClick: function(e) {
		var n = e.target,
			i,
			j = _sharedObject.widgets.length;

		// search for ancestor, was this click on element inside of a node we have tab binding on
		for (i = 0; i < j; i += 1) {
			if (_sharedObject.widgets[i].get(node).contains(n)) {
				if (_sharedObject._currentWidget === _sharedObject.widgets[i]) {
					_sharedObject._currentWidget._updateTabIndex(n);
				}
				else {
					_sharedObject._currentWidget = _sharedObject.widgets[i];
					_sharedObject._currentWidget._setupFieldsForTabbing(n);
				}
				
				return;
			}
		}

		_sharedObject._currentWidget = null;
	},

	// Handle the use of the tab key.
	_handleTabKeyDown: function(e) {
		if (_sharedObject._currentWidget) {
			if (! Y.UA.opera) {e.halt();}
			
			if (_sharedObject._currentWidget.fire(_F.CE_BEFORE_TAB)) {
				_sharedObject._currentWidget[e.shiftKey ? previous : next]();
				_sharedObject._currentWidget.fire(_F.CE_AFTER_TAB);
			}
		}
	},

	// A simple hash to evaluate if a tab widget object has been added.
	hasWidget: {},

	// Initialize the shared object and/or adds the widget thereto.
	init: function(o) {
		if (! _sharedObject.initialized) {
			var doc = o.get(node).get(ownerDocument);
			if (o.get(autoFocus)) {
				doc.on(click, _sharedObject._handleDocumentClick);
			}
			doc.on(key, _sharedObject._handleTabKeyDown, down:9);
			_sharedObject.initialized = true;
		}

		if (! _sharedObject.hasWidget[o]) {
			_sharedObject.hasWidget[o] = true;
			_sharedObject.widgets.push(o);
		}
	},

	// Removes the widget from the shard object.
	destroy: function(o) {
		if (_sharedObject.hasWidget[o]) {
			var widgets = [], i = 0, j = _sharedObject.widgets.length;
			delete _sharedObject.hasWidget[o];

			for (; i < j; i += 1) {
				if (o !== _sharedObject.widgets) {
					widgets.push(o);
				}
			}

			_sharedObject.widgets = widgets;
		}
	},

	// Inidicates if the initlization code has run.
	initialized: false,

	// A collection of registered widgets.
	widgets: []
};

An instantiated TabKeyManagedWidget has the following protected methods that are used internally to handle the tabbing logic: _applyFocusClass, _setupFieldsForTabbing, _moveTabIndex, _updateTabIndex. Im not going to show them all here, because it is simply too much code, but will describe their function. The _applyFocusClass method, applies the focusClass property, and attaches a blur event that will remove the focusClass. The _setupFieldsForTabbing method iterates through the fields that are found via the tabElements property and caches them (fieldsForTabbing property). The _moveTabIndex method is called by the public next and previous methods, moving the focus forward or backwards a field. Lastly, the _updateTabIndex is called when clicking into the currently active instance of TabKeyManagedWidget, it only updates the tabIndex property, without searching the DOM.

If you look at the source code, you will notice that for most browsers, we prevent the default keydown:tab event, then emulate a blur event on the previous field before focusing on the next field. This normalizes the behavior of tab across all browsers (except Opera) and operating systems, forcing the correct timing of blur and focus events, and ensuring the focus never leaves the page or browser. Opera behaves oddly, if the same technique is used, so I simply to not use it. All browsers originally were coded like the Opera variant, but then I realized that there was timing issues in IE and it simply did not work on OSX, because the operating system handled the tab event before the browser (if you dont prevent the default behavior). So we are left with Opera using the original code, which delegates to the browser, before correcting the focused element, and all other browsers explicitly handling the events.

This ended up being a much bigger project that I originally anticipated. Hopefully, it will help serve as an example of how to use YUI 3 to create an widget. I have put it all together into a demo page and the source code is available.