From efad2216b35498fb3dcb953fe3db593000223484 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Tue, 19 Jul 2022 08:53:37 +0000 Subject: [PATCH] [IMP] web: focus of a X2many When rewriting the Form view, we wanted the keyboard navigation using "tab" and "shift+tab" to use the native browser behaviour. The purpose of this commit is to remove the custom behaviour of X2many in a form view when a "tab" or "shift-tab" make it be focused. Before this commit, a new empty line would be created automatically. After this commit, the button "Add a line" takes focus but no new line is created. If one wants to add one, that can be done by pressing "Enter". Part-of: odoo/odoo#96101 --- .../static/src/views/list/list_renderer.js | 38 ------- .../static/src/views/list/list_renderer.xml | 3 +- .../views/fields/one2many_field_tests.js | 104 ++++++++++++++---- 3 files changed, 82 insertions(+), 63 deletions(-) diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index e6aabcfeed3..7d0e7d2ce7e 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -1377,44 +1377,6 @@ export class ListRenderer extends Component { return false; } - async onCreateAction(context) { - // TO DISCUSS: is it a use case for owl `batched()` ? - if (this.createProm) { - return; - } - this.props.onAdd({ context }); - this.createProm = Promise.resolve(); - this.createProm.then(() => { - this.lastCreatingAction = true; - }); - await this.createProm; - this.createProm = null; - } - - /** - * @param {FocusEvent & { - * target: HTMLElement, - * relatedTarget: HTMLElement | null - * }} ev - */ - onFocusIn(ev) { - const { relatedTarget, target } = ev; - const fromOutside = !this.rootRef.el.contains(relatedTarget); - if (!fromOutside) { - return; - } - - const isX2MRowAdder = - target.tagName === "A" && - target.parentElement.classList.contains("o_field_x2many_list_row_add"); - const withinSameUIActiveElement = - this.uiService.getActiveElementOf(relatedTarget) === this.activeElement; - if (withinSameUIActiveElement && isX2MRowAdder) { - const { context } = this.creates[0]; - this.onCreateAction(context); - } - } - setDirty(isDirty) { this.lastIsDirty = isDirty; } diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml index dc1c3883761..9b2ed6fdbf8 100644 --- a/addons/web/static/src/views/list/list_renderer.xml +++ b/addons/web/static/src/views/list/list_renderer.xml @@ -6,7 +6,6 @@ class="o_list_renderer" t-att-class="{'table-responsive': showTable}" t-attf-class="{{ props.archInfo.className or '' }}" - t-on-focusin="onFocusIn" tabindex="-1" t-ref="root" > @@ -48,7 +47,7 @@ role="button" t-att-class="create_index !== 0 ? 'ml16' : ''" t-att-tabindex="props.list.editedRecord ? '-1' : '0'" - t-on-click.stop.prevent="() => this.onCreateAction(create.context)" + t-on-click.stop.prevent="() => this.props.onAdd({context: create.context})" > diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index 0eb73b9c506..7afc91d63d3 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -11671,7 +11671,7 @@ QUnit.module("Fields", (hooks) => { ); QUnit.test( - "when Navigating to a one2many with tabs, it receives the focus and adds a new line", + "when Navigating to a one2many with tabs, the button add a line receives the focus", async function (assert) { await makeView({ type: "form", @@ -11698,21 +11698,19 @@ QUnit.module("Fields", (hooks) => { }); await clickEdit(target); - assert.strictEqual(target.querySelector("[name=qux] input"), document.activeElement); getNextTabableElement(target).focus(); // go inside one2many await nextTick(); - assert.strictEqual( - target.querySelector("[name=turtle_foo] input"), + target.querySelector(".o_field_x2many_list_row_add a"), document.activeElement ); } ); QUnit.test( - "when Navigating to a one2many with tabs, not filling any field and hitting tab, we should not add a first line but navigate to the next control", + "Navigate to a one2many with tab then tab again focus the next field", async function (assert) { serverData.models.partner.records[0].turtles = []; @@ -11752,16 +11750,85 @@ QUnit.module("Fields", (hooks) => { await nextTick(); assert.strictEqual( + target.querySelector(".o_field_x2many_list_row_add a"), + document.activeElement + ); + assert.containsNone(target, "[name=turtles] .o_selected_row"); + + const nextInput = target.querySelector("[name=foo] input"); + // trigger Tab event and check that the default behavior can happen. + const event = triggerEvent( document.activeElement, - target.querySelector("[name=turtle_foo] input") + null, + "keydown", + { key: "Tab" }, + { fast: true } + ); + assert.strictEqual(getNextTabableElement(target), nextInput); + assert.ok(!event.defaultPrevented); + nextInput.focus(); + await nextTick(); + assert.strictEqual(document.activeElement, nextInput); + } + ); + + QUnit.test( + "when Navigating to a one2many with tabs, not filling any field and hitting tab, no line is added and the next field is focused", + async function (assert) { + serverData.models.partner.records[0].turtles = []; + + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + + + + + + + + + + + + + + +
`, + resId: 1, + }); + + await clickEdit(target); + + assert.strictEqual(document.activeElement, target.querySelector("[name=qux] input")); + + getNextTabableElement(target).focus(); // go inside one2many + await nextTick(); + + assert.strictEqual( + target.querySelector(".o_field_x2many_list_row_add a"), + document.activeElement + ); + assert.containsNone(target, "[name=turtles] .o_selected_row"); + + await addRow(target); + assert.strictEqual( + target.querySelector("[name=turtle_foo] input"), + document.activeElement ); triggerHotkey("Tab"); // go to turtle_description field await nextTick(); - assert.strictEqual( - document.activeElement, - target.querySelector("[name=turtle_description] textarea") + target.querySelector("[name=turtle_description] textarea"), + document.activeElement ); const nextInput = target.querySelector("[name=foo] input"); @@ -11778,9 +11845,6 @@ QUnit.module("Fields", (hooks) => { nextInput.focus(); await nextTick(); assert.strictEqual(document.activeElement, nextInput); - // the default behavior happens but the list renderer makes the edited record - // pass in readonly mode. - assert.containsNone(target, "[name=turtles] .o_selected_row"); } ); @@ -11828,7 +11892,12 @@ QUnit.module("Fields", (hooks) => { getNextTabableElement(target).focus(); // go inside one2many await nextTick(); + assert.strictEqual( + target.querySelector(".o_field_x2many_list_row_add a"), + document.activeElement + ); + await addRow(target); assert.strictEqual( target.querySelector(".modal [name=turtle_foo] input"), document.activeElement @@ -11937,17 +12006,6 @@ QUnit.module("Fields", (hooks) => { firstCreateActionLink.focus(); // goes inside one2many await nextTick(); - assert.containsOnce(target, "[name=p] .o_selected_row"); - assert.strictEqual( - document.activeElement, - target.querySelector("[name=p] .o_selected_row input") - ); - - // press ESC to cancel 1st control click (create) - triggerHotkey("Escape"); - await nextTick(); - - assert.containsNone(target, "[name=p] .o_selected_row"); assert.strictEqual( document.activeElement, target.querySelector(".o_field_x2many_list_row_add a")