[FIX] web: useNumpadDecimal: synchronize the inputs listeners

This makes sure that the handler is properly assigned/removed
to/from the added/removed input elements.

closes odoo/odoo#104114

X-original-commit: d88b5e96a4d4a80dd14a271538a4669a6efd2d48
Signed-off-by: Bruno Boi (boi) <boi@odoo.com>
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
Joseph Caburnay
2022-10-26 09:07:46 +02:00
parent 5bbd8a05dd
commit 5a460d1cfa
2 changed files with 75 additions and 19 deletions
@@ -17,7 +17,6 @@ const { useRef, useEffect } = owl;
*/
export function useNumpadDecimal() {
const decimalPoint = localization.decimalPoint;
const listeners = [];
const ref = useRef("numpadDecimal");
const handler = (ev) => {
if (
@@ -35,19 +34,15 @@ export function useNumpadDecimal() {
"end"
);
};
useEffect(
(el) => {
if (el) {
const inputs = el.nodeName === "INPUT" ? [el] : el.querySelectorAll("input");
inputs.forEach((input) => {
listeners.push(input);
input.addEventListener("keydown", handler);
});
}
return () => {
listeners.forEach((input) => input.removeEventListener("keydown", handler));
};
},
() => [ref.el]
);
useEffect(() => {
let inputs = [];
const el = ref.el;
if (el) {
inputs = el.nodeName === "INPUT" ? [el] : el.querySelectorAll("input");
inputs.forEach((input) => input.addEventListener("keydown", handler));
}
return () => {
inputs.forEach((input) => input.removeEventListener("keydown", handler));
};
});
}
@@ -5,6 +5,10 @@ import { registry } from "@web/core/registry";
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { localization } from "@web/core/l10n/localization";
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
import { makeTestEnv } from "../../helpers/mock_env";
const { Component, mount, useState, xml } = owl;
let serverData;
let target;
@@ -78,6 +82,7 @@ QUnit.module("Fields", (hooks) => {
};
setupViewRegistries();
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
});
QUnit.module("Numeric fields");
@@ -193,8 +198,6 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"Numeric fields: NumpadDecimal key is different from the decimalPoint",
async function (assert) {
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
await makeView({
serverData,
type: "form",
@@ -223,7 +226,7 @@ QUnit.module("Fields", (hooks) => {
* Common assertion steps are extracted in this procedure.
*
* @param {object} params
* @param {InputElement} params.el
* @param {HTMLInputElement} params.el
* @param {[number, number]} params.selectionRange
* @param {string} params.expectedValue
* @param {string} params.msg
@@ -313,4 +316,62 @@ QUnit.module("Fields", (hooks) => {
});
}
);
QUnit.test(
"useNumpadDecimal should synchronize handlers on input elements",
async function (assert) {
/**
* Takes an array of input elements and asserts that each has the correct event listener.
* @param {HTMLInputElement[]} inputEls
*/
async function testInputElements(inputEls) {
for (const inputEl of inputEls) {
inputEl.focus();
const numpadDecimalEvent = new KeyboardEvent("keydown", {
code: "NumpadDecimal",
key: ".",
});
numpadDecimalEvent.preventDefault = () => assert.step("preventDefault");
inputEl.dispatchEvent(numpadDecimalEvent);
await nextTick();
// dispatch an extra keydown event and assert that it's not default prevented
const extraEvent = new KeyboardEvent("keydown", { code: "Digit1", key: "1" });
extraEvent.preventDefault = () => {
throw new Error("should not be default prevented");
};
inputEl.dispatchEvent(extraEvent);
await nextTick();
assert.verifySteps(["preventDefault"]);
}
}
class MyComponent extends Component {
setup() {
useNumpadDecimal();
this.state = useState({ showOtherInput: false });
}
}
MyComponent.template = xml`
<main t-ref="numpadDecimal">
<input type="text" placeholder="input 1" />
<input t-if="state.showOtherInput" type="text" placeholder="input 2" />
</main>
`;
const comp = await mount(MyComponent, target, { env: await makeTestEnv() });
// Initially, only one input should be rendered.
assert.containsOnce(target, "main > input");
await testInputElements(target.querySelectorAll("main > input"));
// We show the second input by manually updating the state.
comp.state.showOtherInput = true;
await nextTick();
// The second input should also be able to handle numpad decimal.
assert.containsN(target, "main > input", 2);
await testInputElements(target.querySelectorAll("main > input"));
}
);
});