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Û!"]);