[FIX] web: prevent annoying focus on touch devices
This commit fixes an annoying issue when using Odoo on a tablet or a PC when using the touch screen primarly. The virtual keyboard popped way too much when navigating in between views and screens, since the focus is often set on inputs (mostly the search bar with useAutofocus). Steps to reproduce: - on a Windows laptop or tablet with a touch screen, open any view - the virtual keyboard appears - you must touch out of the keyboard to use Odoo To fix the useAutofocus hook, hasTouch is now being used instead of relying on the size of the screen. Autofocus test with isSmall has been adapted to specify that touch isn't modified, and that the autofocus is still present. And another test has been added asserting the behavior on a touch device. In the form renderer, the autofocus set manually on the first element is now prevented as well, reducing the popping effect of the keyboard when opening a view. A test has been added as well. task-3627697 closes odoo/odoo#148925 X-original-commit: 1f2ab6d5dda61a6aad6e0e9bd282195cdd990f15 Related: odoo/enterprise#54074 Signed-off-by: Romain Estievenart (res) <res@odoo.com> Signed-off-by: Luca Vitali (luvi) <luvi@odoo.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { SERVICES_METADATA } from "@web/env";
|
||||
import { isMobileOS } from "@web/core/browser/feature_detection";
|
||||
import { hasTouch, isMobileOS } from "@web/core/browser/feature_detection";
|
||||
|
||||
import { status, useComponent, useEffect, useRef, onWillUnmount } from "@odoo/owl";
|
||||
|
||||
@@ -35,16 +35,15 @@ import { status, useComponent, useEffect, useRef, onWillUnmount } from "@odoo/ow
|
||||
* @param {Object} [params]
|
||||
* @param {string} [params.refName] override the ref name "autofocus"
|
||||
* @param {boolean} [params.selectAll] if true, will select the entire text value.
|
||||
* @param {boolean} [params.mobile] if true, will autofocus on mobile devices.
|
||||
* @param {boolean} [params.mobile] if true, will force autofocus on touch devices.
|
||||
* @returns {Ref} the element reference
|
||||
*/
|
||||
export function useAutofocus({ refName, selectAll, mobile } = {}) {
|
||||
const comp = useComponent();
|
||||
const ref = useRef(refName || "autofocus");
|
||||
const uiService = useService("ui");
|
||||
|
||||
// Prevent autofocus in mobile
|
||||
if (!mobile && comp.env.isSmall) {
|
||||
// Prevent autofocus on touch devices to avoid the virtual keyboard from popping up unexpectedly
|
||||
if (!mobile && hasTouch()) {
|
||||
return ref;
|
||||
}
|
||||
// LEGACY
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Notebook } from "@web/core/notebook/notebook";
|
||||
import { Setting } from "./setting/setting";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { hasTouch } from "@web/core/browser/feature_detection";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
import { ButtonBox } from "@web/views/form/button_box/button_box";
|
||||
@@ -77,7 +78,7 @@ export class FormRenderer extends Component {
|
||||
}
|
||||
|
||||
get shouldAutoFocus() {
|
||||
return !this.props.archInfo.disableAutofocus;
|
||||
return !hasTouch() && !this.props.archInfo.disableAutofocus;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import {
|
||||
useAutofocus,
|
||||
@@ -11,7 +12,14 @@ import {
|
||||
} from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { destroy, getFixture, makeDeferred, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
import { Component, onMounted, useState, xml } from "@odoo/owl";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
@@ -110,45 +118,44 @@ QUnit.module("utils", () => {
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("useAutofocus returns also a ref when isSmall is true", async function (assert) {
|
||||
assert.expect(2);
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
assert.ok(this.env.isSmall);
|
||||
onMounted(() => {
|
||||
assert.ok(this.inputRef.el);
|
||||
});
|
||||
QUnit.test(
|
||||
"useAutofocus returns also a ref when screen has touch",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
onMounted(() => {
|
||||
assert.ok(this.inputRef.el);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = {};
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
return ui;
|
||||
},
|
||||
};
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
});
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useAutofocus works when isSmall and you provide mobile param",
|
||||
"useAutofocus works when screen has touch and you provide mobile param",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
@@ -161,20 +168,17 @@ QUnit.module("utils", () => {
|
||||
</span>
|
||||
`;
|
||||
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = {};
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
return ui;
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
@@ -182,41 +186,36 @@ QUnit.module("utils", () => {
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
QUnit.test(
|
||||
"useAutofocus does not focus when isSmall and you don't provide mobile param",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
|
||||
QUnit.test("useAutofocus does not focus when screen has touch", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = {};
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
return ui;
|
||||
},
|
||||
};
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.notEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.notEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("supports different ref names", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
@@ -276,16 +275,18 @@ QUnit.module("utils", () => {
|
||||
assert.strictEqual(comp.inputRef.el.selectionEnd, 10);
|
||||
});
|
||||
|
||||
QUnit.test("useAutofocus: autofocus outside of active element doesn't work (CommandPalette)", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
QUnit.test(
|
||||
"useAutofocus: autofocus outside of active element doesn't work (CommandPalette)",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
get OverlayContainer() {
|
||||
return registry.category("main_components").get("OverlayContainer");
|
||||
}
|
||||
}
|
||||
get OverlayContainer() {
|
||||
return registry.category("main_components").get("OverlayContainer");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
<div class="o_dialog_container"/>
|
||||
@@ -293,27 +294,28 @@ QUnit.module("utils", () => {
|
||||
</div>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
|
||||
const config = { providers: [] };
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target , { env });
|
||||
await nextTick();
|
||||
const config = { providers: [] };
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
env.services.dialog.add(CommandPalette, { config });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
env.services.dialog.add(CommandPalette, { config });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.module("useBus");
|
||||
|
||||
|
||||
@@ -8208,6 +8208,30 @@ QUnit.module("Views", (hooks) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("on a touch screen, fields are not focused", async function (assert) {
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="foo"/><field name="bar"/></form>',
|
||||
});
|
||||
|
||||
assert.notEqual(
|
||||
document.activeElement,
|
||||
target.querySelector('.o_field_widget[name="foo"] input')
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"no autofocus with disable_autofocus option [REQUIRE FOCUS]",
|
||||
async function (assert) {
|
||||
|
||||
Reference in New Issue
Block a user