Source: delite/FormWidget.js

/** @module delite/FormWidget */
define([
	"dcl/dcl",
	"./Widget"
], function (dcl, Widget) {

	/**
	 * Base class for widgets that extend `HTMLElement`, but conceptually correspond
	 * to native HTML elements such as `<checkbox>` or `<button>`,
	 * which can be children of a `<form>` node.
	 *
	 * Note that FormWidget requires that `this.focusNode` be a sub-node of the widget, rather than the
	 * root node.  This is because of its processing of `tabIndex`.
	 *
	 * @mixin module:delite/FormWidget
	 * @augments module:delite/Widget
	 */
	return dcl(Widget, /** @lends module:delite/FormWidget# */ {
		/**
		 * Name used when submitting form; same as "name" attribute or plain HTML elements.
		 * @member {string}
		 */
		name: "",

		/**
		 * Corresponds to the native HTML `<input>` element's attribute.
		 * @member {string}
		 */
		alt: "",

		/**
		 * Corresponds to the native HTML `<input>` element's attribute.
		 * @member {string}
		 */
		value: "",

		/**
		 * The order in which fields are traversed when user hits the tab key.
		 * @member {number}
		 * @default 0
		 */
		tabIndex: 0,

		/**
		 * Comma separated list of tabbable nodes, i.e. comma separated list of widget properties that reference
		 * the widget DOM nodes that receive focus during tab operations.
		 * @member {string}
		 * @default "focusNode"
		 */
		tabStops: "focusNode",

		/**
		 * If set to true, the widget will not respond to user input and will not be included in form submission.
		 * @member {boolean}
		 * @default false
		 */
		disabled: false,

		refreshRendering: function (oldValues) {
			// Handle disabled and tabIndex, across the tabStops and root node.
			// No special processing is needed for tabStops other than just to refresh disable and tabIndex.
			var self = this;
			var tabStops = this.tabStops.split(/[ ,]/);
			if ("tabStops" in oldValues || "disabled" in oldValues) {
				var isDisabled = this.disabled;
				if (this.valueNode && this.valueNode !== this) {
					this.valueNode.disabled = isDisabled; // prevent submit
				}
				tabStops.forEach(
					function (nodeName) {
						var node = self[nodeName];
						if (node !== self) {
							node.disabled = isDisabled;
						}
						// let JAWS know
						node.setAttribute("aria-disabled", "" + isDisabled);
					},
					this
				);
				if (!isDisabled) {
					this.removeAttribute("disabled");
				}
			}
			if ("tabStops" in oldValues || "tabIndex" in oldValues || "disabled" in oldValues) {
				tabStops.forEach(
					function (nodeName) {
						var node = self[nodeName];
						if (node !== self) {
							if (self.disabled) {
								node.removeAttribute("tabindex");
							} else {
								node.tabIndex = self._get("tabIndex");
							}
						}
					},
					this
				);
			}
			return oldValues; // for after advice
		},

		/**
		 * Put focus on this widget.
		 */
		focus: function () {
			if (!this.disabled && this.focusNode.focus) {
				try {
					this.focusNode.focus();
				} catch (e) {
					// squelch errors from hidden nodes
				}
			}
		},

		////////////////////////////////////////////////////////////////////////////////////////////
		// Override setAttribute() etc. to put aria-label etc. onto the focus node rather than the root
		// node, so that screen readers work properly.
		setAttribute: dcl.superCall(function (sup) {
			return function (name, value) {
				if (/^aria-/.test(name)) {
					this.focusNode.setAttribute(name, value);
				} else {
					sup.call(this, name, value);
				}
			};
		}),

		getAttribute: dcl.superCall(function (sup) {
			return function (name) {
				if (/^aria-/.test(name)) {
					return this.focusNode.getAttribute(name);
				} else {
					return sup.call(this, name);
				}
			};
		}),

		hasAttribute: dcl.superCall(function (sup) {
			return function (name) {
				if (/^aria-/.test(name)) {
					return this.focusNode.hasAttribute(name);
				} else {
					return sup.call(this, name);
				}
			};
		}),

		removeAttribute: dcl.superCall(function (sup) {
			return function (name) {
				if (/^aria-/.test(name)) {
					this.focusNode.removeAttribute(name);
				} else {
					sup.call(this, name);
				}
			};
		}),

		createdCallback: function () {
			// Move all initially specified aria- attributes to focus node.
			var attr, idx = 0;
			while ((attr = this.attributes[idx++])) {
				if (/^aria-/.test(attr.name)) {
					this.setAttribute(attr.name, attr.value);

					// force remove from root node not this.focusNode
					HTMLElement.prototype.removeAttribute.call(this, attr.name);
				}
			}
		}
	});
});