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 don
t 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.