[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) <boi@odoo.com>
This commit is contained in:
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = () => {};
|
||||
|
||||
@@ -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Û!"]);
|
||||
|
||||
Reference in New Issue
Block a user