From be520c4638bc8bdf8bcb022e06ee5c0f56998cc0 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Fri, 12 May 2023 11:31:01 +0000 Subject: [PATCH] [IMP] web: useCommand supports hotkeyOptions With this commit, all hotkeyOptions are accepted when defining a command with hotkey. In the hotkey service, to unify the API with the command service, we will rename validate to isAvailable and remove the target param because it is never used. closes odoo/odoo#121257 Signed-off-by: Bruno Boi (boi) --- .../src/core/commands/command_service.js | 21 +++- .../static/src/core/hotkeys/hotkey_service.js | 6 +- .../core/commands/command_service_tests.js | 104 ++++++++++++++++++ .../core/hotkeys/hotkey_service_tests.js | 21 ++-- 4 files changed, 133 insertions(+), 19 deletions(-) diff --git a/addons/web/static/src/core/commands/command_service.js b/addons/web/static/src/core/commands/command_service.js index 78a78d3f4cf..0329e6c763f 100644 --- a/addons/web/static/src/core/commands/command_service.js +++ b/addons/web/static/src/core/commands/command_service.js @@ -7,6 +7,7 @@ import { Component, xml, EventBus } from "@odoo/owl"; /** * @typedef {import("./command_palette").CommandPaletteConfig} CommandPaletteConfig + * @typedef {import("../hotkeys/hotkey_service").HotkeyOptions} HotkeyOptions */ /** @@ -19,9 +20,12 @@ import { Component, xml, EventBus } from "@odoo/owl"; */ /** - * @typedef {import("../hotkeys/hotkey_service").HotkeyOptions & { + * @typedef {{ * category?: string; - * isAvailable: ()=>(boolean); + * isAvailable?: ()=>(boolean); + * global?: boolean; + * hotkey?: string; + * hotkeyOptions?: HotkeyOptions * }} CommandOptions */ @@ -174,9 +178,18 @@ export const commandService = { } }; registration.removeHotkey = hotkeyService.add(registration.hotkey, action, { - activeElement: registration.activeElement, + ...options.hotkeyOptions, global: registration.global, - validate: registration.isAvailable, + isAvailable: (...args) => { + let available = true; + if (registration.isAvailable) { + available = registration.isAvailable(...args); + } + if (available && options.hotkeyOptions?.isAvailable) { + available = options.hotkeyOptions?.isAvailable(...args); + } + return available; + }, }); } diff --git a/addons/web/static/src/core/hotkeys/hotkey_service.js b/addons/web/static/src/core/hotkeys/hotkey_service.js index cb605f04f6a..6ddc7408341 100644 --- a/addons/web/static/src/core/hotkeys/hotkey_service.js +++ b/addons/web/static/src/core/hotkeys/hotkey_service.js @@ -18,7 +18,7 @@ import { getVisibleElements } from "../utils/ui"; * allow registration to perform no matter the UI active element * @property {() => HTMLElement} [area] * adds a restricted operating area for this hotkey - * @property {(target: EventTarget) => boolean} [validate] + * @property {() => boolean} [isAvailable] * adds a validation before calling the hotkey registration's callback * * @typedef {HotkeyOptions & { @@ -233,7 +233,7 @@ export const hotkeyService = { (reg.allowRepeat || !isRepeated) && (reg.bypassEditableProtection || !shouldProtectEditable) && (reg.global || reg.activeElement === activeElement) && - (!reg.validate || reg.validate(target)) && + (!reg.isAvailable || reg.isAvailable()) && (!reg.area || (target instanceof Node && reg.area() && reg.area().contains(target))) ); @@ -400,7 +400,7 @@ export const hotkeyService = { bypassEditableProtection: options && options.bypassEditableProtection, global: options && options.global, area: options && options.area, - validate: options && options.validate, + isAvailable: options && options.isAvailable, }; // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), diff --git a/addons/web/static/tests/core/commands/command_service_tests.js b/addons/web/static/tests/core/commands/command_service_tests.js index 4fd1b10ef2e..45b74709667 100644 --- a/addons/web/static/tests/core/commands/command_service_tests.js +++ b/addons/web/static/tests/core/commands/command_service_tests.js @@ -266,6 +266,110 @@ QUnit.test("command with hotkey and isAvailable", async (assert) => { assert.verifySteps([hotkey]); }); +QUnit.test("useCommand hook with hotkey and hotkeyOptions", async (assert) => { + const allowRepeatKey = "a"; + const disallowRepeatKey = "b"; + const defaultBehaviourKey = "c"; + class MyComponent extends TestComponent { + setup() { + useCommand("Allow repeat key", () => assert.step(allowRepeatKey), { + hotkey: allowRepeatKey, + hotkeyOptions: { + allowRepeat: true, + }, + }); + useCommand("Disallow repeat key", () => assert.step(disallowRepeatKey), { + hotkey: disallowRepeatKey, + hotkeyOptions: { + allowRepeat: false, + }, + }); + useCommand("Default repeat key", () => assert.step(defaultBehaviourKey), { + hotkey: defaultBehaviourKey, + }); + } + } + await mount(MyComponent, target, { env }); + + // Dispatch the three keys without repeat: + triggerHotkey(allowRepeatKey); + triggerHotkey(disallowRepeatKey); + triggerHotkey(defaultBehaviourKey); + await nextTick(); + + assert.verifySteps([allowRepeatKey, disallowRepeatKey, defaultBehaviourKey]); + + // Dispatch the three keys with repeat: + triggerHotkey(allowRepeatKey, false, { repeat: true }); + triggerHotkey(disallowRepeatKey, false, { repeat: true }); + triggerHotkey(defaultBehaviourKey, false, { repeat: true }); + await nextTick(); + + assert.verifySteps([allowRepeatKey]); +}); + +QUnit.test("useCommand hook with hotkey and isAvailable", async (assert) => { + const hotkeys = ["a", "b", "c", "d", "e"]; + class MyComponent extends TestComponent { + setup() { + useCommand("Command 1", () => assert.step(hotkeys[0]), { + hotkey: hotkeys[0], + isAvailable: () => true, + hotkeyOptions: { + allowRepeat: true, + isAvailable: () => true, + }, + }); + useCommand("Command 2", () => assert.step(hotkeys[1]), { + hotkey: hotkeys[1], + isAvailable: () => true, + hotkeyOptions: { + allowRepeat: true, + isAvailable: () => false, + }, + }); + useCommand("Command 3", () => assert.step(hotkeys[2]), { + hotkey: hotkeys[2], + isAvailable: () => false, + hotkeyOptions: { + allowRepeat: true, + isAvailable: () => true, + }, + }); + useCommand("Command 4", () => assert.step(hotkeys[3]), { + hotkey: hotkeys[3], + isAvailable: () => true, + hotkeyOptions: { + allowRepeat: true, + }, + }); + useCommand("Command 5", () => assert.step(hotkeys[4]), { + hotkey: hotkeys[4], + isAvailable: () => false, + hotkeyOptions: { + allowRepeat: true, + }, + }); + } + } + await mount(MyComponent, target, { env }); + + for (const hotkey of hotkeys) { + triggerHotkey(hotkey); + } + await nextTick(); + assert.verifySteps(["a", "d"]); + + triggerHotkey("control+k"); + await nextTick(); + assert.containsOnce(target, ".o_command_palette"); + assert.containsN(target, ".o_command", 3); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Command 1A", "Command 2B", "Command 4D"] + ); +}); + QUnit.test("open command palette with command config", async (assert) => { const hotkey = "alt+a"; const action = () => {}; diff --git a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js index 529a33faf9f..8c423bfbad1 100644 --- a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js +++ b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js @@ -1075,7 +1075,7 @@ QUnit.test("operating area and UI active element", async (assert) => { }); QUnit.test("validating option", async (assert) => { - let isValid = false; + let isAvailable = false; class A extends Component { setup() { useHotkey( @@ -1084,10 +1084,7 @@ QUnit.test("validating option", async (assert) => { assert.step("RGNTDJÛ!"); }, { - validate: (eventTarget) => { - assert.strictEqual(eventTarget, document.activeElement); - return isValid; - }, + isAvailable: () => isAvailable, } ); } @@ -1099,14 +1096,14 @@ QUnit.test("validating option", async (assert) => { await nextTick(); assert.verifySteps([]); - isValid = true; + isAvailable = true; triggerHotkey("Space"); await nextTick(); assert.verifySteps(["RGNTDJÛ!"]); }); QUnit.test("operation area with validating option", async (assert) => { - let isValid; + let isAvailable; class A extends Component { setup() { const areaRef = useRef("area"); @@ -1115,7 +1112,7 @@ QUnit.test("operation area with validating option", async (assert) => { () => { assert.step("RGNTDJÛ!"); }, - { area: () => areaRef.el, validate: () => isValid } + { area: () => areaRef.el, isAvailable: () => isAvailable } ); } } @@ -1128,12 +1125,12 @@ QUnit.test("operation area with validating option", async (assert) => { // Trigger hotkeys from the 'one' target.querySelector(".one").focus(); - isValid = false; + isAvailable = false; triggerHotkey("Space"); await nextTick(); assert.verifySteps([]); - isValid = true; + isAvailable = true; triggerHotkey("Space"); await nextTick(); assert.verifySteps([]); @@ -1141,12 +1138,12 @@ QUnit.test("operation area with validating option", async (assert) => { // Trigger hotkeys from the 'two' target.querySelector(".two").focus(); - isValid = false; + isAvailable = false; triggerHotkey("Space"); await nextTick(); assert.verifySteps([]); - isValid = true; + isAvailable = true; triggerHotkey("Space"); await nextTick(); assert.verifySteps(["RGNTDJÛ!"]);