-
Notifications
You must be signed in to change notification settings - Fork 36
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
Changes from 7 commits
b4e53a5
b0199b3
ff26def
23c5824
80e6270
3f36ca1
50a0edf
95a0fb2
4d46111
49fabb6
1ff8cb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"); | ||
|
@@ -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) { | ||
|
@@ -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; | ||
} | ||
} | ||
if ("displayedValue" in oldValues) { | ||
this.inputNode.value = this.displayedValue; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about just saying There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
}, | ||
|
||
/** | ||
|
@@ -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; | ||
} | ||
}, | ||
|
||
|
@@ -517,6 +539,8 @@ define([ | |
} | ||
}.bind(this), 100); // worth exposing a property for the delay? | ||
} | ||
} else { | ||
this.focusNode.focus(); // put the focus back to the inputNode. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change should be tested (that only passes after your change to Combobox.js). Probably just updating one of the existing tests to check focus after clicking a list item. Actually, I loaded cwithout your changes, and tried both the first and the second Combobox on that page, and it seems like clicking a list's item already refocuses the Combobox if autoFilter===false, or the Combobox's Also, I'm confused about what happens when:
|
||
} | ||
}.bind(this)), | ||
|
||
|
@@ -647,8 +671,11 @@ define([ | |
// inputNode does not contain text | ||
if (!this.hasDownArrow) { | ||
// in auto complete mode | ||
this.closeDropDown(); | ||
this._toggleComboPopupList(); | ||
if (this._isMobile) { | ||
this._toggleComboPopupList(); | ||
} else { | ||
this.closeDropDown(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should have a test and a ticket for the bug you are fixing. I also don't understand the commit comment. It says:
That makes it sound like ComboPopup would close immediately after opening, since when the ComboPopup opens the |
||
return false; | ||
} | ||
} | ||
|
@@ -659,8 +686,9 @@ define([ | |
* Toggles the list's visibility when ComboPopup is used (so in mobile) | ||
*/ | ||
_toggleComboPopupList: function () { | ||
if (this._useCenteredDropDown()) { | ||
this.list.setAttribute("d-shown", "" + this.inputNode.value.length >= this.minFilterChars); | ||
if (this._isMobile) { | ||
this.list.setAttribute("d-shown", | ||
"" + this.dropDown.inputNode.value.length >= this.minFilterChars); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see anywhere that you're setting Could you create an initial commit with just that refactor (changing _useCenteredDropDown() to _isMobile) and then I'll push that first and then move on to your bug fix commits? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PS: Just noticed the rest of the _isMobile refactor is in your later commit titled "Fix broken readOnly (at creation time). Add tests." Anyway, all the _isMobile changes should be consolidated into a single commit without any other changes. |
||
this.list.emit("delite-size-change"); | ||
} | ||
}, | ||
|
@@ -684,7 +712,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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
||
|
@@ -724,7 +752,7 @@ define([ | |
if (this.opened) { | ||
this.closeDropDown(true/*refocus*/); | ||
} | ||
} else if (evt.key === "Spacebar") { | ||
} else if (evt.key === "Spacebar" && this.opened) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I pushed this as 7d08bcc. |
||
// 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. | ||
|
@@ -750,28 +778,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; | ||
|
@@ -795,7 +825,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) { | ||
|
@@ -809,11 +839,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; | ||
} | ||
} | ||
}, | ||
|
@@ -848,6 +879,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; | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
@@ -929,6 +975,12 @@ define([ | |
// mobile version | ||
if (!this.hasDownArrow) { | ||
this._toggleComboPopupList(); | ||
} else { | ||
// INFO: display into the popup inputNode any pre-selected item, | ||
// only if the inputNode is visible, though. | ||
if (!this._inputReadOnly) { | ||
this.dropDown.inputNode.value = this.displayedValue; | ||
} | ||
} | ||
this.dropDown.focus(); | ||
} | ||
|
@@ -960,8 +1012,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(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} else if (this.inputNode.value.length < this.minFilterChars) { | ||
return; | ||
} | ||
|
@@ -979,6 +1033,9 @@ define([ | |
// 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) { | ||
item = this.list.navigatedDescendant; | ||
} | ||
|
||
if (!item) { | ||
var selectedItems = this.list.selectedItems; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(); | ||
}, | ||
|
||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this code call _validateMultiple() even for single-selection Comboboxes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, but I don't understand what you are referring to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh OK I didn't realize that. |
||
}, | ||
|
||
/** | ||
|
There was a problem hiding this comment.
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:
all the time, without the
if()
statements?