Source: deliteful/Combobox.js

/** @module deliteful/Combobox */
define([
	"dcl/dcl",
	"requirejs-dplugins/jquery!attributes/classes,event",	// addClass(), css(), on(), off()
	"dstore/Filter",
	"decor/sniff",
	"delite/register",
	"delite/FormValueWidget",
	"delite/HasDropDown",
	"./list/List",
	"./features!desktop-like-channel?:./Combobox/ComboPopup",
	"delite/handlebars!./Combobox/Combobox.html",
	"requirejs-dplugins/i18n!./Combobox/nls/Combobox",
	"delite/theme!./Combobox/themes/{{theme}}/Combobox.css"
], function (dcl, $, Filter, has, register, FormValueWidget, HasDropDown, List, ComboPopup, template, messages) {
	/**
	 * A form-aware and store-aware multichannel widget leveraging the `deliteful/list/List`
	 * widget for rendering the options.
	 * 
	 * The corresponding custom tag is `<d-combobox>`.
	 * 
	 * The property `list` allows to specify the List instance used by the widget.
	 * The customization of the mapping of data item attributes into render item
	 * attributes can be done on the `List` instance using the mapping API of `List`
	 * inherited from its superclass `delite/StoreMap`.
	 * 
	 * The property `selectionMode` allows to choose between single and multiple
	 * choice modes.
	 *  
	 * In single selection mode, if the property `autoFilter` is set to `true`
	 * (default is `false`) the widget allows to type one or more characters which
	 * are used for filtering the shown list items. By default, the filtering is
	 * case-insensitive, and an item is shown if its label contains the entered
	 * string. The default filtering policy can be customized thanks to the 
	 * `filterMode` and `ignoreCase` properties.
	 * 
	 * The widget provides multichannel rendering. Depending on the required channel, which
	 * is determined by the value of the channel flags of `deliteful/features`, the
	 * widget displays the popup containing the options in a different manner:
	 * * if `has("desktop-like-channel")` is `true`: in a popup below or above the root node.
	 * * otherwise (that is for `"phone-like-channel"` and `"tablet-like-channel"`): in an
	 * overlay centered on the screen, filled with an instance of `deliteful/Combobox/ComboPopup`.
	 * 
	 * The channel flags are set by `deliteful/features` using CSS media queries depending on
	 * the screen size. See the `deliteful/features` documentation for information about the
	 * channel flags and about how to configure them statically and how to customize the values
	 * of the screen size breakpoints used by the media queries.
	 * 
	 * The `value` property of the widget contains:
	 * - Single selection mode: the value of the selected list items. By default, the
	 * value of the first item is selected.
	 * - Multiple selection mode: an array containing the values of the selected items.
	 * Defaults to `[]`.
	 * 
	 * If the widget is used in an HTML form, the submitted value contains:
	 *  * - Single selection mode: the same as widget's `value` property.
	 * - Multiple selection mode: a string containing a comma-separated list of the values
	 * of the selected items. Defaults to `""`.
	 * 
	 * By default, the `label` field of the list render item is used as item value.
	 * A different field can be specified by using attribute mapping for `value` on the
	 * List instance.
	 * 
	 * Remark: the option items must be added, removed or updated exclusively using
	 * List's store API. Direct operations using the DOM API are not supported.
	 * 
	 * @example <caption>Markup</caption>
	 * JS:
	 * require(["deliteful/Combobox", "requirejs-domready/domReady!"],
	 *   function () {
	 *   });
	 * HTML:
	 * <d-combobox id="combobox1">
	 *   <d-list>
	 *   { "label": "France", "sales": 500, "profit": 50, "region": "EU" },
	 *   { "label": "Germany", "sales": 450, "profit": 48, "region": "EU" },
	 *   { "label": "UK", "sales": 700, "profit": 60, "region": "EU" },
	 *   { "label": "USA", "sales": 2000, "profit": 250, "region": "America" },
	 *   { "label": "Canada", "sales": 600, "profit": 30, "region": "America" },
	 *   { "label": "Brazil", "sales": 450, "profit": 30, "region": "America" },
	 *   { "label": "China", "sales": 500, "profit": 40, "region": "Asia" },
	 *   { "label": "Japan", "sales": 900, "profit": 100, "region": "Asia" }
	 *   </d-list>
	 * </d-combobox>
	 * 
	 * @example <caption>Programmatic</caption>
	 * JS:
	 * require(["deliteful/List",
	 *   "deliteful/Combobox", ..., "requirejs-domready/domReady!"],
	 *   function (List, Combobox, ...) {
	 *     var dataStore = ...; // Create data store
	 *     var list = new List({source: dataStore});
	 *     var combobox = new Combobox({list: list, selectionMode: "multiple"}).
	 *       placeAt(...);
	 *   });
	 * 
	 * @class module:deliteful/Combobox
	 * @augments module:delite/HasDropDown
	 * @augments module:delite/FormValueWidget
	 */
	return register("d-combobox", [HTMLElement, HasDropDown, FormValueWidget],
		/** @lends module:deliteful/Combobox# */ {
		
		// TODO: handle the situation the list has a null/undefined store.
		// Would be nice to have a global policy for all subclasses of
		// delite/Store (in terms of error feedback).
		// TODO: future mechanism at the level of delite/Store-delite/StoreMap
		// to allow delegation from host widget to a different widget - to get
		// a clean mechanism to support all possible use-cases. (Probably also
		// requires changes in List).
		// TODO: improve API doc.
		// TODO: add (optional) placeholder?
		
		// Note: the property `disabled` is inherited from delite/FormWidget.
		
		baseClass: "d-combobox",
		
		template: template,
		
		/**
		 * If `true`, the list of options can be filtered thanks to an editable
		 * input element. Only used if `selectionMode` is "single".
		 * @member {boolean} module:deliteful/Combobox#autoFilter
		 * @default false
		 */
		autoFilter: false,
		
		/**
		 * The chosen filter mode. Only used if `autoFilter` is `true` and
		 * `selectionMode` is `"single"`.
		 *
		 * Valid values are:
		 *
		 * 1. "startsWith": the item matches if its label starts with the filter text.
		 * 2. "contains": the item matches if its label contains the filter text.
		 * 3. "is": the item matches if its label is the filter text.
		 *
		 * @member {string}
		 * @default "startsWith"
		 */
		filterMode: "startsWith",
		
		/**
		 * If `true`, the filtering of list items ignores case when matching possible items.
		 * Only used if `autoFilter` is `true` and `selectionMode` is `"single"`.
		 * @member {boolean} module:deliteful/Combobox#autoFilter
		 * @default true
		 */
		ignoreCase: true,
		
		/**
		 * The chosen selection mode.
		 *
		 * Valid values are:
		 *
		 * 1. "single": Only one option can be selected at a time.
		 * 2. "multiple": Several options can be selected.
		 *
		 * The value of this property determines the value of the `selectionMode`
		 * property of the List instance used by this widget for displaying the options:
		 * * The value "single" is mapped to "radio".
		 * * The value "multiple" is mapped to "multiple".
		 * 
		 * Note that, regardless of the selection mode, it is always possible to set 
		 * several selected items using the `selectedItem` or `selectedItems` properties
		 * of the List instance.
		 * The mode will be enforced only when using `setSelected()` and/or
		 * `selectFromEvent()` APIs of the List.
		 *
		 * @member {string}
		 * @default "single"
		 */
		selectionMode: "single",
		
		/**
		 * The `deliteful/list/List` element which provides and renders the options
		 * shown by the popup of the Combobox.
		 * Note that this property is set by default to a newly created instance of
		 * `deliteful/list/List`.
		 * @member {module:deliteful/list/List} module:deliteful/Combobox#list
		 * @default instance of deliteful/list/List
		 */
		list: null,
		
		// Flag used for binding the readonly attribute of the input element in the template
		_inputReadOnly: true,
		
		/**
		 * The value of the placeholder attribute of the input element used
		 * for filtering the list of options. The default value is provided by the
		 * "search-placeholder" key of the message bundle.
		 * @member {string}
		 * @default "Search"
		 */
		searchPlaceHolder: messages["search-placeholder"],
		
		/**
		 * The text displayed in the input element when more than one option is
		 * selected. The default value is provided by the "search-placeholder" key of
		 * the message bundle.
		 * @member {string}
		 * @default "Search"
		 */
		multipleChoiceMsg: messages["multiple-choice"],
		
		/**
		 * The text displayed in the input element when no option is selected.
		 * The default value is provided by the "multiple-choice-no-selection" key of
		 * the message bundle.
		 * @member {string}
		 * @default "Select option(s)"
		 */
		multipleChoiceNoSelectionMsg: messages["multiple-choice-no-selection"],
		
		/**
		 * The text displayed in the OK button when the combobox popup contains such a button.
		 * The default value is provided by the "ok-button-label" key of
		 * the message bundle.
		 * @member {string}
		 * @default "OK"
		 */
		okMsg: messages["ok-button-label"],
		
		/**
		 * The text displayed in the Cancel button when the combobox popup contains such a button.
		 * The default value is provided by the "cancel-button-label" key of
		 * the message bundle.
		 * @member {string}
		 * @default "Cancel"
		 */
		cancelMsg: messages["cancel-button-label"],
		
		createdCallback: function () {
			// Declarative case (list specified declaratively inside the declarative Combobox)
			var list = this.querySelector("d-list");
			if (list) {
				if (!list.attached) {
					list.addEventListener("customelement-attached", this._attachedlistener = function () {
						list.removeEventListener("customelement-attached", this._attachedlistener);
						this.list = list;
						this.deliver();
					}.bind(this));
				} else {
					this.list = list;
				}
			}
			if (!this.list) {
				// default list, may be overridden later by user-defined value or when above event listener fires
				this.list = new List();
			}
		},

		refreshRendering: function (oldValues) {
			var updateReadOnly = false;
			if ("list" in oldValues) {
				this._initList();
			}
			if ("selectionMode" in oldValues) {
				updateReadOnly = true;
				if (this.list) {
					this.list.selectionMode = this.selectionMode === "single" ?
						"radio" : "multiple";
				}
			}
			if ("autoFilter" in oldValues ||
				"readOnly" in oldValues) {
				updateReadOnly = true;
			}
			if (updateReadOnly) {
				this._updateInputReadOnly();
				this._setSelectable(this.inputNode, !this.inputNode.readOnly);
			}
		},
		
		/**
		 * Updates the value of the private property on which the Combobox template
		 * binds the `readonly` attribute of the input element.
		 * @private 
		 */
		_updateInputReadOnly: function () {
			var oldValue = this._inputReadOnly;
			this._inputReadOnly = this.readOnly || !this.autoFilter ||
				this._useCenteredDropDown() || this.selectionMode === "multiple";
			if (this._inputReadOnly === oldValue) {
				// FormValueWidget.refreshRendering() mirrors the value of widget.readOnly
				// to focusNode.readOnly, thus competing with the binding of the readOnly
				// attribute of the input node (which is also the focusNode attach point)
				// in the template of Combobox. To ensure the refresh of the binding is done
				// including when the value of the flag _inputReadOnly doesn't change while
				// FormValueWidget has reset the attribute to a different value, force
				// the notification:
				this.notifyCurrentValue("_inputReadOnly");
			} // else no need to notify "by hand", rely on automatic notification
		},
		
		/**
		 * Configures inputNode such that the text is selectable or unselectable.
		 * @private
		 */
		_setSelectable: function (inputNode, selectable) {
			if (selectable) {
				inputNode.removeAttribute("unselectable");
				$(inputNode)
					.css("user-select", "") // maps to WebkitUserSelect, etc.
					.off("selectstart", false);
			} else {
				inputNode.setAttribute("unselectable", "on");
				$(inputNode)
					.css("user-select", "none") // maps to WebkitUserSelect, etc.
					.on("selectstart", false);
			}
		},

		afterFormResetCallback: function () {
			if (this.value !== this.valueNode.value ||
					// In multiple mode, with no option selected before reset,
					// valueNode.value is the same but still needs the reinit to get
					// the correct initial inputNode.value.
					this.selectionMode === "multiple") {
				this._initValue();
			}
		},

		_initList: function () {
			// TODO
			// This is a workaround waiting for a proper mechanism (at the level
			// of delite/Store - delite/StoreMap) to allow a store-based widget
			// to delegate the store-related functions to a parent widget (delite#323).
			if (!this.list.attached) {
				this.list.attachedCallback();
			}

			// Class added on the list such that Combobox' theme can have a specific
			// CSS selector for elements inside the List when used as dropdown in
			// the combo.
			$(this.list).addClass("d-combobox-list");

			// The drop-down is hidden initially
			$(this.list).addClass("d-combobox-list-hidden");

			// The role=listbox is required for the list part of a combobox by the
			// aria spec of role=combobox
			this.list.setAttribute("role", "listbox");

			// Avoid that List gives focus to list items when navigating, which would
			// blur the input field used for entering the filtering criteria.
			this.list.focusDescendants = false;

			this.list.selectionMode = this.selectionMode === "single" ?
				"radio" : "multiple";

			var dropDown = this._createDropDown();

			// Since the dropdown is not a child of the Combobox, it will not inherit
			// its dir attribute. Hence:
			var dir = this.getAttribute("dir");
			if (dir) {
				dropDown.setAttribute("dir", dir);
			}

			this.dropDown = dropDown; // delite/HasDropDown's property

			// Focus stays on the input element
			this.dropDown.focusOnOpen = false;

			// (temporary?) Workaround for delite #373
			this.dropDown.focus = null;

			this._initHandlers();
			this._initValue();
		},
		
		_initHandlers: function () {
			this.list.on("keynav-child-navigated", function (evt) {
				var input = this._popupInput || this.inputNode;
				var navigatedChild = evt.newValue; // never null
				var rend = this.list.getEnclosingRenderer(navigatedChild);
				var item = rend.item;
				input.setAttribute("aria-activedescendant", navigatedChild.id);
				if (this.selectionMode === "single" && !this.list.isSelected(item)) {
					this.list.selectFromEvent(evt, item, rend, true);
				} // else do not change the selection state of an item already selected
				if (evt.triggerEvent && // only for keyboard navigation
					(evt.triggerEvent.type === "keydown" || evt.triggerEvent.type === "keypress")) {
					this._updateScroll(item, true);
				}
			}.bind(this));
			
			this.list.on("click", function (evt) {
				if (this.selectionMode === "single") {
					var rend = this.list.getEnclosingRenderer(evt.target);
					if (rend && !this.list.isCategoryRenderer(rend)) {
						this.defer(function () {
							// deferred such that the user can see the selection feedback
							// before the dropdown closes.
							this.closeDropDown(true/*refocus*/);
						}.bind(this), 100); // worth exposing a property for the delay?
					}
				}
			}.bind(this));
			
			// React to interactive changes of selected items
			this.list.on("selection-change", function () {
				if (this.selectionMode === "single") {
					this._validateSingle();
				}
				this.handleOnInput(this.value); // emit "input" event
			}.bind(this));
			
			// React to programmatic changes of selected items
			this.list.observe(function (oldValues) {
				if ("selectedItems" in oldValues) {
					if (this.selectionMode === "single") {
						this._validateSingle();
						// do not emit "input" event for programmatic changes
					} else if (this.selectionMode === "multiple") {
						this._validateMultiple(this._popupInput || this.inputNode);
					}
				}
			}.bind(this));
			
			this._prepareInput(this.inputNode);
		},
		
		/**
		 * Sets the initial value of the widget. If the widget is inside a form,
		 * also called when reseting the form.
		 * @private 
		 */
		_initValue: function () {
			if (this.selectionMode === "single") {
				// Returns true if List content already available, false otherwise.
				var initValueSingleMode = function () {
					var selectedItem = this.list.selectedItem;
					var done = false;
					var value, label;
					if (selectedItem) {
						label = this._getItemLabel(selectedItem);
						value = this._getItemValue(selectedItem);
						done = true;
					} else {
						var firstItemRenderer = this.list.getItemRendererByIndex(0);
						if (firstItemRenderer) {
							label = this._getItemRendererLabel(firstItemRenderer);
							value = this._getItemRendererValue(firstItemRenderer);
							// Like the native select, the first item is selected.
							this.list.selectedItem = firstItemRenderer.item;
							done = true;
						}
					}
					if (done) {
						this.inputNode.value = label;
						// Initialize widget's value
						this._set("value", value);
						this.valueNode.value = value;
					}
					return done;
				}.bind(this);
				
				if (!initValueSingleMode()) {
					// List not ready, wait.
					// TODO: handle case when List is initialized but has no items yet, from "new List()".
					// Also, handle when the Combobox's selected item is deleted from the list.
					var waitListener = this.list.on("query-success", function () {
						initValueSingleMode();
						waitListener.remove(waitListener);
					});
				}
			} else { // selectionMode === "multiple"
				// Differently than in single selection mode, no need to wait for the data,
				// and do not select the first option, because it would be confusing; the user
				// may scroll and select some other option, without deselecting the first one.
				// The native select in multiple mode doesn't select any option by default either.
				this.inputNode.value = this.multipleChoiceNoSelectionMsg;
				// widget value: // array, more convenient for client-side usage
				this.value = [];
				// submitted form value: string (comma-separated values), more convenient for
				// server-side usage.
				this.valueNode.value = "";
			}
		},
		
		/**
		 * Returns the label of a List item renderer.
		 * @private 
		 */
		_getItemRendererLabel: function (itemRenderer) {
			return this._getItemLabel(itemRenderer.item);
		},
		
		/**
		 * Returns the value of a List item renderer. Defaults to its label
		 * if the underlying data item has no value. 
		 * @private 
		 */
		_getItemRendererValue: function (itemRenderer) {
			return this._getItemValue(itemRenderer.item);
		},
		
		/**
		 * Returns the label of a List render item.
		 * @private 
		 */
		_getItemLabel: function (item) {
			return item.label;
		},
		
		/**
		 * Returns the value of a List render item. Defaults to its label
		 * if the underlying data item has no value. 
		 * @private 
		 */
		_getItemValue: function (item) {
			return "value" in item ? item.value : item.label;
		},
		
		/**
		 * Returns `true` if the dropdown should be centered, and returns
		 * `false` if it should be displayed below/above the widget.
		 * Returns `true` when the module `deliteful/Combobox/ComboPopup` has
		 * been loaded. Note that the module is loaded conditionally, depending
		 * on the channel has() features set by `deliteful/features`.
		 * @private
		 */
		_useCenteredDropDown: function () {
			return !!ComboPopup;
		},
		
		_createDropDown: function () {
			this._updateInputReadOnly();
			
			var centeredDropDown = this._useCenteredDropDown();
			var dropDown = centeredDropDown ?
				this.createCenteredDropDown() :
				this.createAboveBelowDropDown();
			
			this.dropDownPosition = centeredDropDown ?
				["center"] :
				["below", "above"]; // this is the default
			
			return dropDown;
		},
		
		/**
		 * Factory method which creates the widget used inside above/below drop-down.
		 * The default implementation simply returns `this.list`.
		 * @protected
		 */
		createAboveBelowDropDown: function () {
			// Use the List itself as content of the popup. Embedding it in a
			// LinearLayout has seemed useful for solving layout issues on iOS
			// (deliteful issue #270), but appears to be harmful on IE11 (deliteful
			// issue #382). Hence the List is not wrapped anymore inside a LinearLayout.
			return this.list;
		},
		
		/**
		 * Factory method which creates the widget used inside centered drop-down.
		 * The default implementation returns a new instance of deliteful/Combobox/ComboPopup
		 * (the present widget is set for its `combobox` property).
		 * The method can be overridden in order to create a subclass of ComboPopup (for
		 * specifying a custom template, for instance).
		 * @protected
		 */
		createCenteredDropDown: function () {
			return new ComboPopup({combobox: this});
		},
		
		_prepareInput: function (inputElement) {
			this.on("input", function (evt) {
				// Would be nice to also have an "incrementalFilter" boolean property.
				// On desktop, this would allow to redo the filtering only for "change"
				// events, triggered when pressing ENTER. This would also fit for Chrome/Android,
				// where pressing the search key of the virtual keyboard also triggers a
				// change event. But there's no equivalent on Safari / iOS...
				this.filter(inputElement.value);
				this.openDropDown(); // reopen if closed
				// Stop the spurious "input" events emitted while the user types
				// such that only the "input" events emitted via FormValueWidget.handleOnInput()
				// bubble to widget's root node.
				evt.stopPropagation();
				evt.preventDefault();
			}.bind(this), inputElement);
			this.on("change", function (evt) {
				// Stop the spurious "change" events emitted while the user types
				// such that only the "change" events emitted via FormValueWidget.handleOnChange()
				// bubble to widget's root node.
				evt.stopPropagation();
				evt.preventDefault();
			}.bind(this), inputElement);
			this.on("keydown", function (evt) {
				/* jshint maxcomplexity: 15 */
				// deliteful issue #382: prevent the browser from navigating to
				// the previous page when typing backspace in a readonly input
				if (inputElement.readOnly && evt.key === "Backspace") {
					evt.stopPropagation();
					evt.preventDefault();
				} else if (evt.key === "Enter") {
					evt.stopPropagation();
					evt.preventDefault();
					if (this.opened) {
						this.closeDropDown(true/*refocus*/);
					}
				} else if (evt.key === "Spacebar") {
					// Simply forwarding the key event to List doesn't allow toggling
					// the selection, because List's mechanism is based on the event target
					// which here is the input element outside the List. TODO: see deliteful #500.
					if (this.selectionMode === "multiple") {
						var rend = this.list.getEnclosingRenderer(this.list.navigatedDescendant);
						this.list.selectFromEvent(evt, rend.item, rend, true);
					}
					if (this.selectionMode === "multiple" || !this.autoFilter) {
						evt.stopPropagation();
						evt.preventDefault();
					}
				} else if (evt.key === "ArrowDown" || evt.key === "ArrowUp" ||
					evt.key === "PageDown" || evt.key === "PageUp" ||
					evt.key === "Home" || evt.key === "End") {
					if (this._useCenteredDropDown()) {
						this.list.emit("keydown", evt);
					}
					evt.stopPropagation();
					evt.preventDefault();
				}
			}.bind(this), inputElement);
		},
		
		_validateSingle: function () {
			var selectedItem = this.list.selectedItem;
			// selectedItem non-null because List in radio selection mode, but
			// the List can be empty, so:
			this.inputNode.value = selectedItem ? this._getItemLabel(selectedItem) : "";
			this.value = selectedItem ? this._getItemValue(selectedItem) : "";
		},
		
		_validateMultiple: function (inputElement) {
			var selectedItems = this.list.selectedItems;
			var n = selectedItems ? selectedItems.length : 0;
			var value = [];
			if (n > 1) {
				inputElement.value = this.multipleChoiceMsg;
				for (var i = 0; i < n; i++) {
					value.push(selectedItems[i] ? this._getItemValue(selectedItems[i]) : "");
				}
			} else if (n === 1) {
				var selectedItem = this.list.selectedItem;
				inputElement.value = this._getItemLabel(selectedItem);
				value.push(this._getItemValue(selectedItem));
			} else { // no option selected
				inputElement.value = this.multipleChoiceNoSelectionMsg;
			}
			this._set("value", value);
			// FormWidget.refreshRendering() also updates valueNode.value, but we need to
			// make sure this is already done when FormValueWidget.handleOnInput() runs.
			this.valueNode.value = value;
			this.handleOnInput(this.value); // emit "input" event
		},
		
		/**
		 * Filters the embedded List to only show the items matching `filterTxt`.
		 * If `autoFilter` is `true` and `selectionMode` is `"single"`, the method
		 * is called automatically while the user types into the editable input
		 * element, with `filterTxt` being the currently entered text.
		 * The default implementation uses `dstore/Filter.match()`.
		 * The matching is performed against the `list.labelAttr` attribute of
		 * the data store items.
		 * The method can be overridden for implementing other filtering strategies.
		 * @protected
		 */
		filter: function (filterTxt) {
			if (this.filterMode === "startsWith") {
				filterTxt = "^" + filterTxt;
			} else if (this.filterMode === "is") {
				filterTxt = "^" + filterTxt + "$";
			} // nothing to add for "contains"
			
			// TODO: might be nice that, if no item matches the query thus the list is empty,
			// the popup shows some specific graphic feedback.
			var rexExp = new RegExp(filterTxt, this.ignoreCase ? "i" : "");
			this.list.query = (new Filter()).match(this.list.labelAttr, rexExp);
		},
		
		openDropDown: dcl.superCall(function (sup) {
			return function () {
				// Store the current selection, to be able to restore when pressing the
				// cancel button. Used by ComboPopup. (Could spare it in situations when
				// there is no cancel button, but not really worth.)
				this._selectedItems = this.list.selectedItems;
				
				if (!this.opened) {
					// Temporary workaround for issue with bad pairing in List of the
					// busy on/off state. The issue appears to go away if List.attachedCallback
					// wouldn't break the automatic chaining (hence the workaround wouldn't
					// be necessary if List gets this change), but this requires further
					// investigation (TODO).
					this.defer(function () {
						this.list._hideLoadingPanel();
						// Avoid loosing focus when clicking the arrow (instead of the input element):
						this.focusNode.focus();
					}.bind(this), 300);
				}

				var promise = sup.apply(this, arguments);
				
				return promise.then(function () {
					this._updateScroll(undefined, true);
				}.bind(this));
			};
		}),
		
		closeDropDown: dcl.superCall(function (sup) {
			return function () {
				this._selectedItems = null; // cleanup
				
				if (this.opened) {
					// Using the flag `opened` (managed by delite/HasDropDown), avoid
					// emitting a new change event if closeDropDown is closed more than once
					// for a closed dropdown.
					
					// Closing the dropdown represents a commit interaction
					this.handleOnChange(this.value); // emit "change" event
				
					// Reinit the query. Necessary such that after closing the dropdown
					// in autoFilter mode with a text in the input field not matching
					// any item, when the dropdown will be reopen it shows all items
					// instead of being empty 
					this.list.query = {};
				
					if (this.selectionMode === "single" && this.autoFilter) {
						// In autoFilter mode, reset the content of the inputNode when
						// closing the dropdown, such that next time the dropdown is opened
						// it doesn't show the text the user may have entered for filtering
						var selItem = this.list.selectedItem;
						if (selItem) {
							(this._popupInput || this.inputNode).value =
								this._getItemLabel(this.list.selectedItem);
						}
					}
				}

				sup.apply(this, arguments);
			};
		}),
		
		/**
		 * Scrolls the list inside the popup such that the specified item, or
		 * the first selected item if no item is specified, is visible.
		 * @private 
		 */
		_updateScroll: function (item, navigate) {
			// Since List is in focus-less mode, it does not give focus to
			// navigated items, thus the browser does not autoscroll.
			// TODO: see deliteful #498

			if (!item) {
				var selectedItems = this.list.selectedItems;
				item = selectedItems && selectedItems.length > 0 ?
					selectedItems[0] : null;
			}
			if (item) {
				// Make the first selected item (if any) visible.
				// Must be done after sup.apply, because List.getBottomDistance
				// relies on dimensions which are not available if the DOM nodes
				// are not (yet) visible, hence the popup needs to be shown before.
				var id = this.list.getIdentity(item);
				var renderer = this.list.getRendererByItemId(id);
				if (renderer) {
					this.list.scrollBy({y: this.list.getBottomDistance(renderer)});
					if (navigate) {
						this.list.navigatedDescendant = renderer.childNodes[0];
					}
				} // null if the list is empty because no item matches the auto-filtering
			}
		}
	});
});