diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js index c57e85fb726..d52fd91af4f 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js @@ -73,6 +73,9 @@ export class PartnerAutoCompleteCharField extends CharField { } }); this.props.record.update(data.company); + if (this.props.setDirty) { + this.props.setDirty(false); + } } } diff --git a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js index c80c30b8584..727b8b404df 100644 --- a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js +++ b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js @@ -4,11 +4,11 @@ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { click, + editInput, editSelect, getFixture, patchWithCleanup, triggerEvent, - editInput, } from "@web/../tests/helpers/utils"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { loadJS } from "@web/core/assets"; @@ -348,18 +348,20 @@ QUnit.module('partner_autocomplete', { } }); - QUnit.test("Show confirmation dialog on input blur", async function (assert) { - assert.expect(1); + QUnit.test("Click out after edition", async function (assert) { + assert.expect(2); await makeView(makeViewParams); const input = target.querySelector("[name=parent_id] input.o-autocomplete--input.o_input"); await triggerEvent(input, null, "focus"); await click(input); await editInput(input, null, "go"); + assert.strictEqual(input.value, "go"); + await triggerEvent(target, null, "pointerdown"); await triggerEvent(input, null, "blur"); - assert.containsOnce(target, ".o_dialog"); + assert.strictEqual(input.value, ""); }); - QUnit.test("Hide auto complate suggestion for no create", async function (assert) { + QUnit.test("Hide auto complete suggestion for no create", async function (assert) { const partnerMakeViewParams = { ...makeViewParams, arch: diff --git a/addons/sale/static/src/js/sale_product_field.js b/addons/sale/static/src/js/sale_product_field.js index cd0856ce332..eec807b165d 100644 --- a/addons/sale/static/src/js/sale_product_field.js +++ b/addons/sale/static/src/js/sale_product_field.js @@ -20,7 +20,7 @@ export class SaleOrderLineProductField extends Many2OneField { isInternalUpdate = true; return updateRecord.call(this, value); }; - useEffect(value => { + useEffect(value => { if (!isMounted) { isMounted = true; } else if (value && isInternalUpdate) { diff --git a/addons/sale/static/tests/sale_product_field_tests.js b/addons/sale/static/tests/sale_product_field_tests.js index f899c70468f..6bc027df492 100644 --- a/addons/sale/static/tests/sale_product_field_tests.js +++ b/addons/sale/static/tests/sale_product_field_tests.js @@ -1,7 +1,13 @@ /** @odoo-module **/ import { - getFixture, patchWithCleanup, addRow, editInput, triggerEvent, click } from "@web/../tests/helpers/utils"; + getFixture, + patchWithCleanup, + addRow, + editInput, + triggerHotkey, + nextTick +} from "@web/../tests/helpers/utils"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { browser } from "@web/core/browser/browser"; @@ -88,7 +94,7 @@ QUnit.module("Fields", (hooks) => { QUnit.module("Sale product field"); - QUnit.test("blurring input with incomplete text will propose to create product", async function (assert) { + QUnit.test("pressing tab with incomplete text will create a product", async function (assert) { await makeView({ type: "form", @@ -112,23 +118,16 @@ QUnit.module("Fields", (hooks) => { // add a line and enter new product name await addRow(target, ".o_field_x2many_list"); await editInput(target, "[name='product_template_id'] input", "new product"); - - // blur input => should ask for confirmation if we want to create product - await triggerEvent(target, "[name='product_template_id'] input", "blur"); - assert.containsOnce(target, ".modal:contains(Create new product as a new Product)") + await triggerHotkey("tab"); + await nextTick(); assert.verifySteps([ "get_views", "onchange", "onchange", "name_search", - ]); - - await click(target, ".modal button.btn-primary"); - assert.verifySteps([ "name_create", "get_single_product_variant", ]); - }); }); diff --git a/addons/web/static/src/core/autocomplete/autocomplete.js b/addons/web/static/src/core/autocomplete/autocomplete.js index b5dcb0604d1..9e23db6d0fb 100644 --- a/addons/web/static/src/core/autocomplete/autocomplete.js +++ b/addons/web/static/src/core/autocomplete/autocomplete.js @@ -104,6 +104,16 @@ export class AutoComplete extends Component { this.state.activeSourceOption = null; } + cancel() { + if (this.inputRef.el.value.length) { + if (this.props.autoSelect) { + this.inputRef.el.value = this.props.value; + this.props.onCancel(); + } + } + this.close(); + } + async loadSources(useInput) { this.sources = []; this.state.activeSourceOption = null; @@ -237,21 +247,10 @@ export class AutoComplete extends Component { this.ignoreBlur = false; return; } - const value = this.inputRef.el.value; - if ( - this.props.autoSelect && - this.state.activeSourceOption && - value.length > 0 && - value !== this.props.value - ) { - this.selectOption(this.state.activeSourceOption, { triggeredOnBlur: true }); - } else { - this.props.onBlur({ - inputValue: value, - }); - this.inEdition = false; - this.close(); - } + this.props.onBlur({ + inputValue: this.inputRef.el.value, + }); + this.inEdition = false; } onInputClick() { if (!this.isOpened) { @@ -325,7 +324,7 @@ export class AutoComplete extends Component { if (!this.isOpened) { return; } - this.close(); + this.cancel(); break; case "tab": if (!this.isOpened) { @@ -373,7 +372,7 @@ export class AutoComplete extends Component { externalClose(ev) { if (this.isOpened && !this.root.el.contains(ev.target)) { - this.close(); + this.cancel(); } } } @@ -397,6 +396,7 @@ Object.assign(AutoComplete, { placeholder: { type: String, optional: true }, autoSelect: { type: Boolean, optional: true }, resetOnSelect: { type: Boolean, optional: true }, + onCancel: { type: Function, optional: true }, onInput: { type: Function, optional: true }, onChange: { type: Function, optional: true }, onBlur: { type: Function, optional: true }, @@ -411,6 +411,7 @@ Object.assign(AutoComplete, { placeholder: "", autoSelect: false, dropdown: true, + onCancel: () => {}, onInput: () => {}, onChange: () => {}, onBlur: () => {}, diff --git a/addons/web/static/src/views/fields/many2one/many2one_field.js b/addons/web/static/src/views/fields/many2one/many2one_field.js index 97f4609d252..296245967d0 100644 --- a/addons/web/static/src/views/fields/many2one/many2one_field.js +++ b/addons/web/static/src/views/fields/many2one/many2one_field.js @@ -134,10 +134,7 @@ export class Many2OneField extends Component { }; if (this.props.canQuickCreate) { - this.quickCreate = (name, params = {}) => { - if (params.triggeredOnBlur) { - return this.openConfirmationDialog(name); - } + this.quickCreate = (name) => { this.state.isFloating = false; return this.updateRecord([false, name]); }; @@ -368,7 +365,7 @@ export const many2OneField = { extractProps({ attrs, context, decorations, options, string }, dynamicInfo) { const hasCreatePermission = attrs.can_create ? evaluateBooleanExpr(attrs.can_create) : true; const hasWritePermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true; - const canCreate = options.no_create ? false : hasCreatePermission + const canCreate = options.no_create ? false : hasCreatePermission; return { placeholder: attrs.placeholder, canOpen: !options.no_open, diff --git a/addons/web/static/src/views/fields/properties/property_value.js b/addons/web/static/src/views/fields/properties/property_value.js index 87e7e41007b..67bbb6cfaf4 100644 --- a/addons/web/static/src/views/fields/properties/property_value.js +++ b/addons/web/static/src/views/fields/properties/property_value.js @@ -293,10 +293,6 @@ export class PropertyValue extends Component { * @param {object} params */ async onQuickCreate(name, params = {}) { - if (params.triggeredOnBlur) { - this.onValueChange(false); - return; - } const result = await this.orm.call(this.props.comodel, "name_create", [name], { context: this.props.context, }); diff --git a/addons/web/static/src/views/fields/relational_utils.js b/addons/web/static/src/views/fields/relational_utils.js index 09f20f90acc..d0994f3f68d 100644 --- a/addons/web/static/src/views/fields/relational_utils.js +++ b/addons/web/static/src/views/fields/relational_utils.js @@ -253,6 +253,9 @@ export class Many2XAutocomplete extends Component { this.props.setInputFloats(true); } } + onCancel() { + this.props.setInputFloats(false); + } onSelect(option, params = {}) { if (option.action) { diff --git a/addons/web/static/src/views/fields/relational_utils.xml b/addons/web/static/src/views/fields/relational_utils.xml index 8a20593f518..224868015dc 100644 --- a/addons/web/static/src/views/fields/relational_utils.xml +++ b/addons/web/static/src/views/fields/relational_utils.xml @@ -59,6 +59,7 @@ dropdown="props.dropdown" autofocus="props.autofocus" resetOnSelect="props.value === ''" + onCancel.bind="onCancel" /> diff --git a/addons/web/static/tests/core/autocomplete_tests.js b/addons/web/static/tests/core/autocomplete_tests.js index 896130bc0c0..6802c900341 100644 --- a/addons/web/static/tests/core/autocomplete_tests.js +++ b/addons/web/static/tests/core/autocomplete_tests.js @@ -164,7 +164,7 @@ QUnit.module("Components", (hooks) => { assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); }); - QUnit.test("close dropdown on escape keydown", async (assert) => { + QUnit.test("cancel result on escape keydown", async (assert) => { class Parent extends Component {} Parent.components = { AutoComplete }; Parent.template = xml` @@ -172,17 +172,21 @@ QUnit.module("Components", (hooks) => { value="'Hello'" sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]" onSelect="() => {}" + autoSelect="true" /> `; await mount(Parent, target, { env }); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]); assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); + await editInput(target, ".o-autocomplete--input", "H"); await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "Escape" }); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); }); QUnit.test("select input text on first focus", async (assert) => { @@ -198,7 +202,7 @@ QUnit.module("Components", (hooks) => { assert.strictEqual(el.value.substring(el.selectionStart, el.selectionEnd), "Bar"); }); - QUnit.test("scroll outside should close dropdown", async (assert) => { + QUnit.test("scroll outside should cancel result", async (assert) => { class Parent extends Component {} Parent.components = { AutoComplete }; Parent.template = xml` @@ -206,17 +210,21 @@ QUnit.module("Components", (hooks) => { value="'Hello'" sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]" onSelect="() => {}" + autoSelect="true" /> `; await mount(Parent, target, { env }); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); await click(target, ".o-autocomplete--input"); assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); + await editInput(target, ".o-autocomplete--input", "H"); await triggerEvent(target, null, "scroll"); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); }); QUnit.test("scroll inside should keep dropdown open", async (assert) => { @@ -240,7 +248,33 @@ QUnit.module("Components", (hooks) => { assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); }); - QUnit.test("losing focus should close dropdown", async (assert) => { + QUnit.test("losing focus should cancel result", async (assert) => { + class Parent extends Component {} + Parent.components = { AutoComplete }; + Parent.template = xml` + + `; + + await mount(Parent, target, { env }); + assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); + + await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]); + assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); + await editInput(target, ".o-autocomplete--input", "H"); + + await triggerEvent(target, "", "pointerdown"); + await triggerEvent(target, ".o-autocomplete--input", "blur"); + assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); + }); + + QUnit.test("click out after clearing input", async (assert) => { class Parent extends Component {} Parent.components = { AutoComplete }; Parent.template = xml` @@ -253,12 +287,16 @@ QUnit.module("Components", (hooks) => { await mount(Parent, target, { env }); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello"); await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]); assert.containsOnce(target, ".o-autocomplete--dropdown-menu"); + await editInput(target, ".o-autocomplete--input", ""); + await triggerEvent(target, "", "pointerdown"); await triggerEvent(target, ".o-autocomplete--input", "blur"); assert.containsNone(target, ".o-autocomplete--dropdown-menu"); + assert.strictEqual(target.querySelector(".o-autocomplete--input").value, ""); }); QUnit.test("open twice should not display previous results", async (assert) => { @@ -495,10 +533,9 @@ QUnit.module("Components", (hooks) => { onChange() { assert.step("change"); } - onSelect(option, params) { + onSelect(option) { target.querySelector(".o-autocomplete--input").value = option.label; assert.step("select " + option.label); - assert.notOk(params.triggeredOnBlur); } onBlur() { assert.step("blur"); diff --git a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js index b98618aedeb..cfd0d900f23 100644 --- a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js @@ -1014,7 +1014,8 @@ QUnit.module("Fields", (hooks) => { await triggerEvent(input, null, "focus"); await click(input); await editInput(input, null, "go"); - await triggerEvent(input, null, "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsNone(document.body, ".modal"); assert.containsOnce(target, ".o_field_many2many_tags .badge"); @@ -1028,7 +1029,8 @@ QUnit.module("Fields", (hooks) => { await click(input); await editInput(input, null, "r"); await triggerEvent(input, null, "keydown", { key: "ArrowDown" }); - await triggerEvent(input, null, "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.strictEqual( target.querySelectorAll(".o_field_many2many_tags .badge")[1].textContent, "red", diff --git a/addons/web/static/tests/views/fields/many2one_field_tests.js b/addons/web/static/tests/views/fields/many2one_field_tests.js index 74e3c98cfef..2271a69564c 100644 --- a/addons/web/static/tests/views/fields/many2one_field_tests.js +++ b/addons/web/static/tests/views/fields/many2one_field_tests.js @@ -2950,7 +2950,7 @@ QUnit.module("Fields", (hooks) => { ); QUnit.test("quick create on a many2one", async function (assert) { - assert.expect(2); + assert.expect(1); await makeView({ type: "form", @@ -2971,13 +2971,7 @@ QUnit.module("Fields", (hooks) => { await triggerEvent(target, ".o_field_many2one input", "focus"); await editInput(target, ".o_field_many2one input", "new partner"); - await triggerEvent(target, ".o_field_many2one input", "blur"); - - assert.strictEqual( - target.querySelector(".modal .modal-body").textContent.trim(), - "Create new partner as a new Product?" - ); - await click(target, ".modal .modal-footer .btn-primary"); + await triggerHotkey("tab"); }); QUnit.test( @@ -3117,7 +3111,8 @@ QUnit.module("Fields", (hooks) => { // cancel the many2one creation with Discard button await editInput(target, ".o_field_many2one input", "new product"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsOnce(target, ".modal", "there should be one opened modal"); @@ -3131,7 +3126,8 @@ QUnit.module("Fields", (hooks) => { // cancel the many2one creation with Close button await editInput(target, ".o_field_many2one input", "new product"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsOnce(target, ".modal", "there should be one opened modal"); await click(target, ".modal .modal-header button"); @@ -3153,7 +3149,8 @@ QUnit.module("Fields", (hooks) => { ); await editInput(target, ".o_field_many2one input", "new product"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsOnce(target, ".modal", "there should be one opened modal"); await click(target, ".modal .modal-footer .btn:not(.btn-primary)"); @@ -3165,7 +3162,8 @@ QUnit.module("Fields", (hooks) => { // confirm the many2one creation await editInput(target, ".o_field_many2one input", "new product"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsOnce( target, @@ -3185,7 +3183,8 @@ QUnit.module("Fields", (hooks) => { }); await editInput(target, ".o_field_many2one input", "xph"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsNone(target, ".modal"); assert.strictEqual(target.querySelector(".o_field_many2one input").value, "xphone"); @@ -3206,7 +3205,8 @@ QUnit.module("Fields", (hooks) => { }); await editInput(target, ".o_field_many2one input", "new partner"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsNone(target, ".modal", "should not display the create modal"); assert.strictEqual( @@ -3230,7 +3230,8 @@ QUnit.module("Fields", (hooks) => { `, }); await editInput(target, ".o_field_many2one input", "new partner"); - await triggerEvent(target, ".o_field_many2one input", "blur"); + await triggerHotkey("tab"); + await nextTick(); assert.containsNone(target, ".modal", "should not display the create modal"); assert.strictEqual( @@ -3240,30 +3241,33 @@ QUnit.module("Fields", (hooks) => { ); }); - QUnit.test("no_quick_create option on a many2one when can_create is absent", async function (assert) { - serverData.models.partner.fields.product_id.readonly = true; - await makeView({ - type: "form", - resModel: "partner", - serverData, - arch: ` + QUnit.test( + "no_quick_create option on a many2one when can_create is absent", + async function (assert) { + serverData.models.partner.fields.product_id.readonly = true; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: `
`, - }); - await editInput(target, ".o_field_many2one input", "new partner"); - assert.containsOnce( - target, - ".ui-autocomplete .o_m2o_dropdown_option", - "Dropdown should be opened and have only one item" - ); - assert.hasClass( - target.querySelector(".ui-autocomplete .o_m2o_dropdown_option"), - "o_m2o_dropdown_option_create_edit" - ); - }); + }); + await editInput(target, ".o_field_many2one input", "new partner"); + assert.containsOnce( + target, + ".ui-autocomplete .o_m2o_dropdown_option", + "Dropdown should be opened and have only one item" + ); + assert.hasClass( + target.querySelector(".ui-autocomplete .o_m2o_dropdown_option"), + "o_m2o_dropdown_option_create_edit" + ); + } + ); QUnit.test("can_create and can_write option on a many2one", async function (assert) { serverData.models.product.options = { @@ -3451,12 +3455,8 @@ QUnit.module("Fields", (hooks) => { "there should be option for 'No records'" ); - await triggerEvent(target, ".o_field_many2one[name=product_id] input", "blur"); - assert.containsNone( - target, - ".o_field_many2one[name=product_id] .o_m2o_no_result", - "there should be option for 'No records'" - ); + await triggerEvent(target, "", "pointerdown"); + assert.containsNone(target, ".o_field_many2one[name=product_id] .o_m2o_no_result"); } ); diff --git a/addons/web/static/tests/views/kanban/kanban_view_tests.js b/addons/web/static/tests/views/kanban/kanban_view_tests.js index 61c903b0a58..e7fcce6e4f8 100644 --- a/addons/web/static/tests/views/kanban/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban/kanban_view_tests.js @@ -3571,6 +3571,7 @@ QUnit.module("Views", (hooks) => { QUnit.test("quick create record: cancel when modal is opened", async (assert) => { serverData.views["partner,some_view_ref,form"] = '
'; + serverData.views["product,false,form"] = '
'; // patch setTimeout s.t. the autocomplete dropdown opens directly patchWithCleanup(browser, { @@ -3596,7 +3597,7 @@ QUnit.module("Views", (hooks) => { await editInput(target, ".o_kanban_quick_create input", "test"); await triggerEvent(target, ".o_kanban_quick_create input", "input"); - await triggerEvent(target, ".o_kanban_quick_create input", "blur"); + await click(target, ".o_m2o_dropdown_option_create_edit"); // When focusing out of the many2one, a modal to add a 'product' will appear. // The following assertions ensures that a click on the body element that has 'modal-open' diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 9aa5e190811..314215cfdd8 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -13115,10 +13115,9 @@ QUnit.module("Views", (hooks) => { await editInput(target, ".o_field_many2one input", "abcdef"); await nextTick(); - // simulate focus out - await triggerEvent(target, ".o_field_many2one input", "blur"); + await click(target, ".o_m2o_dropdown_option_create_edit"); - assert.containsOnce(target, ".modal", "should ask confirmation to create a record"); + assert.containsOnce(target, ".modal", "should show dialog to create the record"); assert.containsOnce(target, ".o_data_row", "the row should still be there"); } ); @@ -16809,13 +16808,9 @@ QUnit.module("Views", (hooks) => { const input = target.querySelector(".o_data_row .o_data_cell input"); await editInput(input, null, "aaa"); - await triggerEvents(input, null, ["keyup", "blur"]); - document.body.click(); + await triggerEvents(input, null, ["keyup"]); + await triggerHotkey("tab"); await nextTick(); - assert.containsOnce(target, ".modal", "the quick_create modal should appear"); - - await click(target.querySelector(".modal .btn-primary")); - await click(target.querySelector(".o_list_view")); assert.strictEqual( target.getElementsByClassName("o_data_cell")[0].innerHTML, "aaa",