From cc411a507651e15d237133afc4b871cf8345acc2 Mon Sep 17 00:00:00 2001 From: luvi Date: Fri, 25 Nov 2022 12:27:57 +0000 Subject: [PATCH] [IMP] web: only enable spellcheck on focused inputs This commit adds the ability to enable spellcheck on input/textarea only if they are in focus. This avoids the UI to be bloated with unwanted underlined text on unfocused elements. It is especially useful in a context of form view in always edit mode. A core hook has been introduced to allow other components to use the same behavior without reimplementing it. It is currently used by the TextField. It means it is also used in the HtmlField. Tests have been written, both for the field component, and the core hook. task #2861428 Part-of: odoo/odoo#106866 --- addons/web/static/src/core/utils/hooks.js | 41 ++++ .../src/views/fields/text/text_field.js | 31 +-- .../static/tests/core/utils/hooks_tests.js | 188 +++++++++++++++++- .../tests/views/fields/html_field_tests.js | 28 +++ 4 files changed, 272 insertions(+), 16 deletions(-) diff --git a/addons/web/static/src/core/utils/hooks.js b/addons/web/static/src/core/utils/hooks.js index bd883071f59..6764196b86a 100644 --- a/addons/web/static/src/core/utils/hooks.js +++ b/addons/web/static/src/core/utils/hooks.js @@ -214,6 +214,47 @@ export function useService(serviceName) { return service; } +// ----------------------------------------------------------------------------- +// useSpellCheck +// ----------------------------------------------------------------------------- + +/** + * To avoid elements to keep their spellcheck appearance when they are no + * longer in focus. We only add this attribute when needed. To disable this + * behavior, use the spellcheck attribute on the element. + */ +export function useSpellCheck({ refName } = {}) { + const elements = []; + const ref = useRef(refName || "spellcheck"); + function toggleSpellcheck(ev) { + ev.target.spellcheck = document.activeElement === ev.target; + } + useEffect( + (el) => { + if (el) { + const inputs = + ["INPUT", "TEXTAREA"].includes(el.nodeName) || el.contenteditable + ? [el] + : el.querySelectorAll("input, textarea, [contenteditable=true]"); + inputs.forEach((input) => { + if (input.spellcheck !== false) { + elements.push(input); + input.addEventListener("focus", toggleSpellcheck); + input.addEventListener("blur", toggleSpellcheck); + } + }); + } + return () => { + elements.forEach((input) => { + input.removeEventListener("focus", toggleSpellcheck); + input.removeEventListener("blur", toggleSpellcheck); + }); + }; + }, + () => [ref.el] + ); +} + /** * @typedef {Function} ForwardRef * @property {HTMLElement | undefined} el diff --git a/addons/web/static/src/views/fields/text/text_field.js b/addons/web/static/src/views/fields/text/text_field.js index 5870b86d070..588d5cba906 100644 --- a/addons/web/static/src/views/fields/text/text_field.js +++ b/addons/web/static/src/views/fields/text/text_field.js @@ -2,11 +2,12 @@ import { registry } from "@web/core/registry"; import { _lt } from "@web/core/l10n/translation"; +import { useSpellCheck } from "@web/core/utils/hooks"; import { useInputField } from "../input_field_hook"; import { standardFieldProps } from "../standard_field_props"; import { TranslationButton } from "../translation_button"; import { useDynamicPlaceholder } from "../dynamicplaceholder_hook"; -import { parseInteger } from '../parsers'; +import { parseInteger } from "../parsers"; import { Component, useEffect, onMounted, onWillUnmount, useRef } from "@odoo/owl"; @@ -17,6 +18,7 @@ export class TextField extends Component { } this.textareaRef = useRef("textarea"); useInputField({ getValue: () => this.props.value || "", refName: "textarea" }); + useSpellCheck({ refName: "textarea" }); useEffect(() => { if (!this.props.readonly) { @@ -30,34 +32,33 @@ export class TextField extends Component { if (ev.key === this.dynamicPlaceholder.TRIGGER_KEY && ev.target === this.textareaRef.el) { const baseModel = this.props.record.data.mailing_model_real; if (baseModel) { - await this.dynamicPlaceholder.open( - this.textareaRef.el, - baseModel, - { - validateCallback: this.onDynamicPlaceholderValidate.bind(this), - closeCallback: this.onDynamicPlaceholderClose.bind(this) - } - ); + await this.dynamicPlaceholder.open(this.textareaRef.el, baseModel, { + validateCallback: this.onDynamicPlaceholderValidate.bind(this), + closeCallback: this.onDynamicPlaceholderClose.bind(this), + }); } } } onMounted() { if (this.props.dynamicPlaceholder) { this.keydownListenerCallback = this.onKeydownListener.bind(this); - document.addEventListener('keydown', this.keydownListenerCallback); + document.addEventListener("keydown", this.keydownListenerCallback); } } onWillUnmount() { if (this.props.dynamicPlaceholder) { - document.removeEventListener('keydown', this.keydownListenerCallback); + document.removeEventListener("keydown", this.keydownListenerCallback); } } onDynamicPlaceholderValidate(chain, defaultValue) { if (chain) { const triggerKeyReplaceRegex = new RegExp(`${this.dynamicPlaceholder.TRIGGER_KEY}$`); - let dynamicPlaceholder = "{{object." + chain.join('.'); - dynamicPlaceholder += defaultValue && defaultValue !== '' ? ` or '''${defaultValue}'''}}` : '}}'; - this.props.update(this.textareaRef.el.value.replace(triggerKeyReplaceRegex, '') + dynamicPlaceholder); + let dynamicPlaceholder = "{{object." + chain.join("."); + dynamicPlaceholder += + defaultValue && defaultValue !== "" ? ` or '''${defaultValue}'''}}` : "}}"; + this.props.update( + this.textareaRef.el.value.replace(triggerKeyReplaceRegex, "") + dynamicPlaceholder + ); } } onDynamicPlaceholderClose() { @@ -114,7 +115,7 @@ TextField.props = { ...standardFieldProps, isTranslatable: { type: Boolean, optional: true }, placeholder: { type: String, optional: true }, - dynamicPlaceholder: { type: Boolean, optional: true}, + dynamicPlaceholder: { type: Boolean, optional: true }, rowCount: { type: Number, optional: true }, }; diff --git a/addons/web/static/tests/core/utils/hooks_tests.js b/addons/web/static/tests/core/utils/hooks_tests.js index 7bbaf633558..4515fcf14b6 100644 --- a/addons/web/static/tests/core/utils/hooks_tests.js +++ b/addons/web/static/tests/core/utils/hooks_tests.js @@ -1,7 +1,15 @@ /** @odoo-module **/ import { uiService } from "@web/core/ui/ui_service"; -import { useAutofocus, useBus, useChildRef, useForwardRefToParent, useListener, useService } from "@web/core/utils/hooks"; +import { + useAutofocus, + useBus, + useChildRef, + useForwardRefToParent, + useListener, + useService, + useSpellCheck, +} from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; import { makeTestEnv } from "@web/../tests/helpers/mock_env"; import { @@ -424,6 +432,184 @@ QUnit.module("utils", () => { assert.strictEqual(nbCalls, 8); }); + QUnit.module("useSpellCheck"); + + QUnit.test("useSpellCheck: ref is on the textarea", async function (assert) { + class MyComponent extends Component { + setup() { + useSpellCheck(); + } + } + MyComponent.template = xml`