[IMP] web: tabbing out of an empty m2o should not set a value
Before this commit, when the use focused a many2one field and then pressed tab, the first item of the autocomplete dropdown was selected. This is not what we want if the user didn't write anything in the input, and if he didn't use the up/down keys to highlight a specific item of the dropdown. This commit ensures that we only select the highlighted item when the user presses tab if he previously wrote something in the input (which hasn't been emptied meanwhile) or if he used the up/down keys to select a specific item. task-2455781 closes odoo/odoo#66443 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -138,6 +138,11 @@ var FieldMany2One = AbstractField.extend({
|
||||
// list of last autocomplete suggestions
|
||||
this.suggestions = [];
|
||||
|
||||
// flag used to prevent from selecting the highlighted item in the autocomplete
|
||||
// dropdown when the user leaves the many2one by pressing Tab (unless he
|
||||
// manually selected the item using UP/DOWN keys)
|
||||
this.ignoreTabSelect = false;
|
||||
|
||||
// use a DropPrevious to properly handle related record quick creations,
|
||||
// and store a createDef to be able to notify the environment that there
|
||||
// is pending quick create operation
|
||||
@@ -274,10 +279,19 @@ var FieldMany2One = AbstractField.extend({
|
||||
});
|
||||
},
|
||||
select: function (event, ui) {
|
||||
// we do not want the select event to trigger any additional
|
||||
// effect, such as navigating to another field.
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
// do not select anything if the input is empty and the user
|
||||
// presses Tab (except if he manually highlighted an item with
|
||||
// up/down keys)
|
||||
if (!self.floating && event.key === "Tab" && self.ignoreTabSelect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
// on Enter we do not want any additional effect, such as
|
||||
// navigating to another field
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var item = ui.item;
|
||||
self.floating = false;
|
||||
@@ -290,6 +304,11 @@ var FieldMany2One = AbstractField.extend({
|
||||
},
|
||||
focus: function (event) {
|
||||
event.preventDefault(); // don't automatically select values on focus
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
// the user manually selected an item by pressing up/down keys,
|
||||
// so select this item if he presses tab later on
|
||||
self.ignoreTabSelect = false;
|
||||
}
|
||||
},
|
||||
open: function (event) {
|
||||
self._onScroll = function (ev) {
|
||||
@@ -300,6 +319,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
window.addEventListener('scroll', self._onScroll, true);
|
||||
},
|
||||
close: function (event) {
|
||||
self.ignoreTabSelect = false;
|
||||
// it is necessary to prevent ESC key from propagating to field
|
||||
// root, to prevent unwanted discard operations.
|
||||
if (event.which === $.ui.keyCode.ESCAPE) {
|
||||
@@ -788,6 +808,9 @@ var FieldMany2One = AbstractField.extend({
|
||||
* @private
|
||||
*/
|
||||
_onInputClick: function () {
|
||||
if (this.autocomplete_bound && !this.$input.autocomplete("widget").is(":visible")) {
|
||||
this.ignoreTabSelect = true;
|
||||
}
|
||||
this._toggleAutoComplete();
|
||||
},
|
||||
/**
|
||||
@@ -810,6 +833,14 @@ var FieldMany2One = AbstractField.extend({
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onInputKeyup: function (ev) {
|
||||
const $autocomplete = this.$input.autocomplete("widget");
|
||||
// close autocomplete if no autocomplete item is selected and user presses TAB
|
||||
// s.t. we properly move to the next field in this case
|
||||
if (ev.which === $.ui.keyCode.TAB &&
|
||||
$autocomplete.is(":visible") &&
|
||||
!$autocomplete.find('.ui-menu-item .ui-state-active').length) {
|
||||
this.$input.autocomplete("close");
|
||||
}
|
||||
if (ev.which === $.ui.keyCode.ENTER || ev.which === $.ui.keyCode.TAB) {
|
||||
// If we pressed enter or tab, we want to prevent _onInputFocusout from
|
||||
// executing since it would open a M2O dialog to request
|
||||
@@ -819,6 +850,9 @@ var FieldMany2One = AbstractField.extend({
|
||||
}
|
||||
this.isDirty = true;
|
||||
if (this.$input.val() === "") {
|
||||
if (ev.key === "Backspace" || ev.key === "Delete") { // Backspace or Delete
|
||||
this.ignoreTabSelect = true;
|
||||
}
|
||||
this.reinitialize(false);
|
||||
} else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) {
|
||||
this.floating = true;
|
||||
@@ -833,22 +867,6 @@ var FieldMany2One = AbstractField.extend({
|
||||
this.floating = false;
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Stops the left/right navigation move event if the cursor is not at the
|
||||
* start/end of the input element. Stops any navigation move event if the
|
||||
* user is selecting text.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onNavigationMove: function (ev) {
|
||||
// TODO Maybe this should be done in a mixin or, better, the m2o field
|
||||
// should be an InputField (but this requires some refactoring).
|
||||
basicFields.InputField.prototype._onNavigationMove.apply(this, arguments);
|
||||
if (this.mode === 'edit' && $(this.$input.autocomplete('widget')).is(':visible')) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} event
|
||||
|
||||
@@ -2574,7 +2574,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
|
||||
QUnit.test('pressing enter in a m2o in an editable list', async function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(8);
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
@@ -2616,11 +2616,6 @@ QUnit.module('fields', {}, function () {
|
||||
var $dropdown = $input.autocomplete('widget');
|
||||
assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible");
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input[0], document.activeElement,
|
||||
"input should still be focused");
|
||||
|
||||
// we now trigger again ENTER to make sure we can move to next line
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
|
||||
assert.notOk(document.contains($input[0]),
|
||||
"input should no longer be in dom");
|
||||
@@ -2698,7 +2693,7 @@ QUnit.module('fields', {}, function () {
|
||||
});
|
||||
|
||||
var $input = form.$('.o_field_many2one input');
|
||||
await testUtils.fields.editInput($input, "first");
|
||||
await testUtils.fields.editAndTrigger($input, "first", ["keydown", "keyup"]);
|
||||
await testUtils.fields.triggerKey('down', $input, 'tab');
|
||||
await testUtils.fields.triggerKey('press', $input, 'tab');
|
||||
await testUtils.fields.triggerKey('up', $input, 'tab');
|
||||
@@ -2722,6 +2717,80 @@ QUnit.module('fields', {}, function () {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('leaving a many2one by pressing tab', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `<form>
|
||||
<field name="trululu"/>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const $input = form.$('.o_field_many2one input');
|
||||
await testUtils.dom.click($input);
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input.val(), '', "no record should have been selected");
|
||||
|
||||
// open autocomplete dropdown and manually select item by UP/DOWN key and press TAB
|
||||
await testUtils.dom.click($input);
|
||||
await testUtils.fields.triggerKeydown($input, 'down');
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input.val(), 'second record', "second record should have been selected");
|
||||
|
||||
// clear many2one and then open autocomplete, write something and press TAB
|
||||
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'), '', ['keyup', 'blur']);
|
||||
await testUtils.dom.triggerEvent($input, 'focus');
|
||||
await testUtils.fields.editInput($input, 'se');
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input.val(), 'second record', "first record should have been selected");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('leaving an empty many2one by pressing tab (after backspace or delete)', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `<form>
|
||||
<field name="trululu"/>
|
||||
<field name="display_name"/>
|
||||
</form>`,
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
const $input = form.$('.o_field_many2one input');
|
||||
assert.ok($input.val(), "many2one should have value");
|
||||
|
||||
// simulate backspace to remove values and press TAB
|
||||
await testUtils.fields.editInput($input, "");
|
||||
await testUtils.fields.triggerKeyup($input, 'backspace');
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input.val(), '', "no record should have been selected");
|
||||
|
||||
// reset a value
|
||||
await testUtils.fields.many2one.clickOpenDropdown('trululu');
|
||||
await testUtils.fields.many2one.clickItem('trululu', 'first record');
|
||||
assert.ok($input.val(), "many2one should have value");
|
||||
|
||||
// simulate delete to remove values and press TAB
|
||||
await testUtils.fields.editInput($input, "");
|
||||
await testUtils.fields.triggerKeyup($input, 'delete');
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
assert.strictEqual($input.val(), '', "no record should have been selected");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', async function (assert) {
|
||||
assert.expect(6);
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
|
||||
Reference in New Issue
Block a user