Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combobox: multiple fixes. #674

Closed
wants to merge 11 commits into from
Closed
122 changes: 82 additions & 40 deletions Combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,18 @@ define([
minFilterChars: 1,

/**
* Displayed value, when Combobox.value is set at the creation time.
* InputNode's value. Can be used to display something at the creation time. After that,
* each user interaction will override this with the selected item label.
* @type {string}
*/
displayedValue: "",

// Flag used for skipping consecutive validations, if one already run.
_justValidated: false,

// Flag used for post initializing the widget value, if the list has not been created yet.
_processValueAfterListInit: false,

createdCallback: function () {
// Declarative case (list specified declaratively inside the declarative Combobox)
var list = this.querySelector("d-list");
Expand Down Expand Up @@ -375,7 +382,7 @@ define([
/* jshint maxcomplexity: 17 */
refreshRendering: function (oldValues) {
var updateReadOnly = false;
if ("list" in oldValues) {
if ("list" in oldValues && this.list) {
this._initList();
}
if ("selectionMode" in oldValues) {
Expand All @@ -395,11 +402,23 @@ define([
}
if ("value" in oldValues) {
if (!this._justValidated) {
this._validateInput(false, true);
} else {
delete this._justValidated;
if (this.list) {
// INFO: if list is already created and attached, then we can validate the `value` value
this._validateInput(false);
} else {
// INFO: otherwise we have to delay its evaluation.
this._processValueAfterListInit = true;
}
}
}
if ("_justValidated" in oldValues) {
if (this._justValidated) {
this._justValidated = false;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing to me, why don't you just do:

this._justValidated = false;

all the time, without the if() statements?

if ("displayedValue" in oldValues) {
this.inputNode.value = this.displayedValue;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just saying value={{displayedValue}} in the template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have already tried this solution before, but it wasn't working and I couldnt explain the reason. Do you have any hint maybe?

},

/**
Expand Down Expand Up @@ -452,31 +471,34 @@ define([
},

_initList: function () {
if (this.list) {
// 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();
}
// 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");

// 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-hidden");

// The drop-down is hidden initially
$(this.list).addClass("d-hidden");
// The role=listbox is required for the list part of a combobox by the
// aria spec of role=combobox
this.list.type = "listbox";

// The role=listbox is required for the list part of a combobox by the
// aria spec of role=combobox
this.list.type = "listbox";
this.list.selectionMode = this.selectionMode === "single" ?
"radio" : "multiple";

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

this._initHandlers();
if (this._processValueAfterListInit) {
this.notifyCurrentValue("value");
delete this._processValueAfterListInit;
}
},

Expand Down Expand Up @@ -684,7 +706,7 @@ define([
// save what user typed at each keystroke.
this.value = inputElement.value;
if (this._useCenteredDropDown()) {
this.inputNode.value = inputElement.value;
this.displayedValue = inputElement.value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I filed #688 for the remaining task to keep displayedValue in sync w/the actual displayed value.

}
this.handleOnInput(this.value); // emit "input" event.

Expand Down Expand Up @@ -750,28 +772,30 @@ define([
}.bind(this), inputElement);
},

_validateInput: function (userInteraction, init) {
_validateInput: function (userInteraction) {
if (this.selectionMode === "single") {
this._validateSingle(userInteraction, init);
this._validateSingle(userInteraction);
} else {
this._validateMultiple(userInteraction, init);
this._validateMultiple(userInteraction);
}
this._justValidated = true;
this.notifyCurrentValue("_justValidated");
},

_validateSingle: function (userInteraction, init) {
_validateSingle: function (userInteraction) {
if (userInteraction) {
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.displayedValue = selectedItem ? this._getItemLabel(selectedItem) : "";
this.value = selectedItem ? this._getItemValue(selectedItem) : "";
} else if (init) {
this.inputNode.value = this.displayedValue !== "" ? this.displayedValue : this.value;
} else {
var item = this._retrieveItemFromSource(this.value);
this.displayedValue = item ? item[this.list.labelAttr || this.list.labelFunc] : this.value;
}
},

_validateMultiple: function (userInteraction, init) {
_validateMultiple: function (userInteraction) {
var n;
if (userInteraction) {
var selectedItems = this.list.selectedItems;
Expand All @@ -795,7 +819,7 @@ define([
// make sure this is already done when FormValueWidget.handleOnInput() runs.
this.valueNode.value = value;
this.handleOnInput(this.value); // emit "input" event
} else if (init) {
} else {
var items = [];
if (typeof this.value === "string") {
if (this.value.length > 0) {
Expand All @@ -809,11 +833,12 @@ define([
} // else empty array. No pre-set values.
n = items.length;
if (n > 1) {
this.inputNode.value = string.substitute(this.multipleChoiceMsg, {items: n});
this.displayedValue = string.substitute(this.multipleChoiceMsg, {items: n});
} else if (n === 1) {
this.inputNode.value = this.displayedValue !== "" ? this.displayedValue : items[0];
var item = this._retrieveItemFromSource(items[0]);
this.displayedValue = item ? item[this.list.labelAttr || this.list.labelFunc] : this.value;
} else {
this.inputNode.value = this.multipleChoiceNoSelectionMsg;
this.displayedValue = this.multipleChoiceNoSelectionMsg;
}
}
},
Expand Down Expand Up @@ -848,6 +873,21 @@ define([
this.openDropDown();
},

_retrieveItemFromSource: function (key) {
var item = null,
_source = this.source || (this.list && this.list.source);
if (_source) {
if (Array.isArray(_source)) {
item = _source.filter(function (i) {
return i[this.list.valueAttr || this.list.labelAttr] === key;
}.bind(this))[0];
} else {
item = _source.getSync && _source.getSync(key);
}
}
return item;
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange to have code that only works with a sync store, especially considering that you are working on auto-complete which is asynchronous.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can make this part specific for only when in not auto-complete mode (hasDownArrow=false). Do you agree?


/**
* Sets the new list's query.
* This method can be overridden when using other store types.
Expand Down Expand Up @@ -960,8 +1000,10 @@ define([
_dropDownKeyUpHandler: dcl.superCall(function (sup) {
return function () {
if (this.hasDownArrow) {
if (this.inputNode.value.length === 0 && this.minFilterChars === 0) {
this.list.query = this._getDefaultQuery();
if (this.inputNode.value.length === 0) {
if (this.minFilterChars === 0) {
this.list.query = this._getDefaultQuery();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is the code change to "Restore down-arrow key original behaviour, even if the inputNode is empty."?

It doesn't seem quite right. For example:

  • What if the list hasn't been created yet? Didn't we agree to always set the list's query at the same time as we create the list?
  • What if the user typed a different key than the down arrow, such as backspace?

Copy link
Contributor Author

@brunano21 brunano21 Dec 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What if the list hasn't been created yet? Didn't we agree to always set the list's query at the same time as we create the list?
    --- Sure, I should check if the list is already created. However this.list.query and this.list.source are set at the same time, because the _dropDownKeyUpHandler function calls its sup function which calls openDropDown (where this.list.source gets assigned. So, what's wrong?

  • What if the user typed a different key than the down arrow, such as backspace?
    --- What's your point? In this case the above method (_dropDownKeyUpHandler) runs only when the user press the Down Arrow Key (see: https://github.com/ibm-js/delite/blob/master/HasDropDown.js#L400-L412). So, what's the problem with backspace?

} else if (this.inputNode.value.length < this.minFilterChars) {
return;
}
Expand Down
6 changes: 5 additions & 1 deletion Combobox/ComboPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ define([
* @protected
*/
okHandler: function () {
this.combobox._validateMultiple(this.combobox.inputNode);
// NOTE: no need to validate since it's handled by the `selection-change` listener
this.combobox.closeDropDown();
},

Expand All @@ -76,7 +76,11 @@ define([
* @protected
*/
cancelHandler: function () {
// INFO: resetting any selected items.
this.combobox.list.selectedItems = [];
this.combobox.closeDropDown();
// cont: then ask to validate, so widget's value and inputNode get updated as well.
this.combobox._validateMultiple(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this code call _validateMultiple() even for single-selection Comboboxes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, but I don't understand what you are referring to. cancelHandler function runs only when user press "Cancel" button, which is visible only if selectionMode=multiple.
In selectionMode=single we don't have any button shown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh OK I didn't realize that.

},

/**
Expand Down
5 changes: 3 additions & 2 deletions Combobox/Combobox.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
autocomplete="off" autocorrect="off" autocapitalize="none"
aria-autocomplete="list"
type="text"
readonly="{{this._inputReadOnly ? 'readonly' : ''}}">
readonly="{{this._inputReadOnly ? 'readonly' : ''}}"
placeholder="{{searchPlaceHolder}}">
<input class="d-hidden"
attach-point="valueNode"
readonly name="{{name}}" value="{{value}}">
readonly name="{{name}}">
<span class="d-combobox-arrow" d-shown="{{hasDownArrow}}"
attach-point="buttonNode" aria-hidden="true"></span>
</template>
4 changes: 2 additions & 2 deletions samples/Combobox.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@

<p>
<label for="comboTeams3-input">Your favorite team (single choice):</label><br/>
<span>[options: hasDownArrow=false, filterDelay=750ms, minFilterChars=0]</span>
<span>[options: hasDownArrow=true, filterDelay=750ms, minFilterChars=0]</span>
</p>
<d-combobox selectionMode="single" autoFilter="true" id="comboTeams3" hasDownArrow="true" filterDelay="750" minFilterChars="0">
<d-list righttextAttr="world-cup-victories" categoryAttr="region" showNoItems="true">
Expand Down Expand Up @@ -219,7 +219,7 @@
</d-combobox>

<p>
<label for="comboPlayersWithValuesDecl-input">Your favorite players (multiple choice - declaratively ):</label>
<label for="comboPlayersWithValuesDecl-input">Your favorite players (multiple choice - declaratively):</label>
<br/>Pre-set values = Hagi, Buffon
</p>
<d-combobox selectionMode="multiple" id="comboPlayersWithValuesDecl" value="Hagi,Buffon">
Expand Down
Loading