[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:
FrancoisGe
2023-05-12 20:52:13 +02:00
parent c3ccfbe98d
commit be520c4638
4 changed files with 133 additions and 19 deletions
@@ -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Û!"]);