[IMP] web: add hotkey service options
This commit adds two options to the hotkey service to improve its usability in some cases: - `area` which is a function that returns an HTMLElement that restricts the operating area for an hotkey registration - `validate` which is a function that returns a boolean that tells if the hotkey registration's callback should be called or not Some typings adaptations were made in: - @web/core/commands/command_palette - @web/core/commands/command_service - @web/core/utils/ui Part-of: odoo/odoo#92475
This commit is contained in:
@@ -45,7 +45,7 @@ const FUZZY_NAMESPACES = ["default"];
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* configByNamespace?: {[namespace]: NamespaceConfig};
|
||||
* configByNamespace?: {[namespace: string]: NamespaceConfig};
|
||||
* FooterComponent?: Component;
|
||||
* providers: Provider[];
|
||||
* searchValue?: string;
|
||||
|
||||
@@ -18,11 +18,8 @@ const { Component, xml } = owl;
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* activeElement?: HTMLElement;
|
||||
* @typedef {import("../hotkeys/hotkey_service").HotkeyOptions & {
|
||||
* category?: string;
|
||||
* global?: boolean;
|
||||
* hotkey?: string;
|
||||
* }} CommandOptions
|
||||
*/
|
||||
|
||||
|
||||
@@ -9,15 +9,8 @@ const { useEffect } = owl;
|
||||
* when the caller component will mount/unmount.
|
||||
*
|
||||
* @param {string} hotkey
|
||||
* @param {()=>void} callback
|
||||
* @param {Object} options additional options
|
||||
* @param {boolean} [options.allowRepeat=false]
|
||||
* allow registration to perform multiple times when hotkey is held down
|
||||
* @param {boolean} [options.bypassEditableProtection=false]
|
||||
* if true the hotkey service will call this registration
|
||||
* even if an editable element is focused
|
||||
* @param {boolean} [options.global=false]
|
||||
* allow registration to perform no matter the UI active element
|
||||
* @param {import("./hotkey_service").HotkeyCallback} callback
|
||||
* @param {import("./hotkey_service").HotkeyOptions} [options] additional options
|
||||
*/
|
||||
export function useHotkey(hotkey, callback, options = {}) {
|
||||
const hotkeyService = useService("hotkey");
|
||||
|
||||
@@ -5,6 +5,29 @@ import { registry } from "../registry";
|
||||
import { browser } from "../browser/browser";
|
||||
import { getVisibleElements } from "../utils/ui";
|
||||
|
||||
/**
|
||||
* @typedef {(context: { area: HTMLElement, target: EventTarget }) => void} HotkeyCallback
|
||||
*
|
||||
* @typedef {Object} HotkeyOptions
|
||||
* @property {boolean} [allowRepeat]
|
||||
* allow registration to perform multiple times when hotkey is held down
|
||||
* @property {boolean} [bypassEditableProtection]
|
||||
* if true the hotkey service will call this registration
|
||||
* even if an editable element is focused
|
||||
* @property {boolean} [global]
|
||||
* 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]
|
||||
* adds a validation before calling the hotkey registration's callback
|
||||
*
|
||||
* @typedef {HotkeyOptions & {
|
||||
* hotkey: string,
|
||||
* callback: HotkeyCallback,
|
||||
* activeElement: HTMLElement,
|
||||
* }} HotkeyRegistration
|
||||
*/
|
||||
|
||||
const ALPHANUM_KEYS = "abcdefghijklmnopqrstuvwxyz0123456789".split("");
|
||||
const NAV_KEYS = [
|
||||
"arrowleft",
|
||||
@@ -19,16 +42,56 @@ const NAV_KEYS = [
|
||||
"enter",
|
||||
"tab",
|
||||
"delete",
|
||||
"space",
|
||||
];
|
||||
const MODIFIERS = ["alt", "control", "shift"];
|
||||
const AUTHORIZED_KEYS = [...ALPHANUM_KEYS, ...NAV_KEYS, "escape"];
|
||||
|
||||
/**
|
||||
* Get the actual hotkey being pressed.
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
* @returns {string} the active hotkey, in lowercase
|
||||
*/
|
||||
export function getActiveHotkey(ev) {
|
||||
const hotkey = [];
|
||||
|
||||
// ------- Modifiers -------
|
||||
// Modifiers are pushed in ascending order to the hotkey.
|
||||
if (isMacOS() ? ev.ctrlKey : ev.altKey) {
|
||||
hotkey.push("alt");
|
||||
}
|
||||
if (isMacOS() ? ev.metaKey : ev.ctrlKey) {
|
||||
hotkey.push("control");
|
||||
}
|
||||
if (ev.shiftKey) {
|
||||
hotkey.push("shift");
|
||||
}
|
||||
|
||||
// ------- Key -------
|
||||
let key = ev.key.toLowerCase();
|
||||
// Identify if the user has tapped on the number keys above the text keys.
|
||||
if (ev.code && ev.code.indexOf("Digit") === 0) {
|
||||
key = ev.code.slice(-1);
|
||||
}
|
||||
// Prefer physical keys for non-latin keyboard layout.
|
||||
if (!AUTHORIZED_KEYS.includes(key) && ev.code && ev.code.indexOf("Key") === 0) {
|
||||
key = ev.code.slice(-1).toLowerCase();
|
||||
}
|
||||
// Make sure we do not duplicate a modifier key
|
||||
if (!MODIFIERS.includes(key)) {
|
||||
hotkey.push(key);
|
||||
}
|
||||
return hotkey.join("+");
|
||||
}
|
||||
|
||||
export const hotkeyService = {
|
||||
dependencies: ["ui"],
|
||||
// Be aware that all odoo hotkeys are designed with this modifier in mind,
|
||||
// so changing the overlay modifier may conflict with some shortcuts.
|
||||
overlayModifier: "alt",
|
||||
start(env, { ui }) {
|
||||
/** @type {Map<number, HotkeyRegistration>} */
|
||||
const registrations = new Map();
|
||||
let nextToken = 0;
|
||||
let overlaysVisible = false;
|
||||
@@ -111,6 +174,7 @@ export const hotkeyService = {
|
||||
activeElement,
|
||||
hotkey,
|
||||
isRepeated: event.repeat,
|
||||
target: event.target,
|
||||
shouldProtectEditable,
|
||||
};
|
||||
const dispatched = dispatch(infos);
|
||||
@@ -140,33 +204,60 @@ export const hotkeyService = {
|
||||
* activeElement: HTMLElement,
|
||||
* hotkey: string,
|
||||
* isRepeated: boolean,
|
||||
* target: EventTarget,
|
||||
* shouldProtectEditable: boolean,
|
||||
* }} infos
|
||||
* @returns {boolean} true if has been dispatched
|
||||
*/
|
||||
function dispatch(infos) {
|
||||
const { activeElement, hotkey, isRepeated, shouldProtectEditable } = infos;
|
||||
const { activeElement, hotkey, isRepeated, target, shouldProtectEditable } = infos;
|
||||
|
||||
// Prepare registrations and the common filter
|
||||
const reversedRegistrations = Array.from(registrations.values()).reverse();
|
||||
const domRegistrations = getDomRegistrations(hotkey, activeElement);
|
||||
const allRegistrations = reversedRegistrations.concat(domRegistrations);
|
||||
|
||||
// Dispatch actual hotkey to first matching registration
|
||||
const match = allRegistrations.find(
|
||||
// Find all candidates
|
||||
const candidates = allRegistrations.filter(
|
||||
(reg) =>
|
||||
reg.hotkey === hotkey &&
|
||||
(reg.allowRepeat || !isRepeated) &&
|
||||
(reg.bypassEditableProtection || !shouldProtectEditable) &&
|
||||
(reg.global || reg.activeElement === activeElement)
|
||||
(reg.global || reg.activeElement === activeElement) &&
|
||||
(!reg.validate || reg.validate(target)) &&
|
||||
(!reg.area ||
|
||||
(target instanceof Node && reg.area() && reg.area().contains(target)))
|
||||
);
|
||||
if (match) {
|
||||
match.callback();
|
||||
|
||||
// First candidate
|
||||
let winner = candidates.shift();
|
||||
if (winner && winner.area) {
|
||||
// If there is an area, find the closest one
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.area() && winner.area().contains(candidate.area())) {
|
||||
winner = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch actual hotkey to the matching registration
|
||||
if (winner) {
|
||||
winner.callback({
|
||||
area: winner.area && winner.area(),
|
||||
target,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of registrations from the [data-hotkey] defined in the DOM
|
||||
*
|
||||
* @param {string} hotkey
|
||||
* @param {HTMLElement} activeElement
|
||||
* @returns {HotkeyRegistration[]}
|
||||
*/
|
||||
function getDomRegistrations(hotkey, activeElement) {
|
||||
const overlayModParts = hotkeyService.overlayModifier.split("+");
|
||||
if (!overlayModParts.every((el) => hotkey.includes(el))) {
|
||||
@@ -194,7 +285,7 @@ export const hotkeyService = {
|
||||
|
||||
/**
|
||||
* Add the hotkey overlays respecting the ui active element.
|
||||
* @param {Element} activeElement
|
||||
* @param {HTMLElement} activeElement
|
||||
*/
|
||||
function addHotkeyOverlays(activeElement) {
|
||||
for (const el of getVisibleElements(activeElement, "[data-hotkey]:not(:disabled)")) {
|
||||
@@ -247,57 +338,12 @@ export const hotkeyService = {
|
||||
overlaysVisible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual hotkey being pressed.
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
* @returns {string} the active hotkey, in lowercase
|
||||
*/
|
||||
function getActiveHotkey(ev) {
|
||||
const hotkey = [];
|
||||
|
||||
// ------- Modifiers -------
|
||||
// Modifiers are pushed in ascending order to the hotkey.
|
||||
if (isMacOS() ? ev.ctrlKey : ev.altKey) {
|
||||
hotkey.push("alt");
|
||||
}
|
||||
if (isMacOS() ? ev.metaKey : ev.ctrlKey) {
|
||||
hotkey.push("control");
|
||||
}
|
||||
if (ev.shiftKey) {
|
||||
hotkey.push("shift");
|
||||
}
|
||||
|
||||
// ------- Key -------
|
||||
let key = ev.key.toLowerCase();
|
||||
// Identify if the user has tapped on the number keys above the text keys.
|
||||
if (ev.code && ev.code.indexOf("Digit") === 0) {
|
||||
key = ev.code.slice(-1);
|
||||
}
|
||||
// Prefer physical keys for non-latin keyboard layout.
|
||||
if (!AUTHORIZED_KEYS.includes(key) && ev.code && ev.code.indexOf("Key") === 0) {
|
||||
key = ev.code.slice(-1).toLowerCase();
|
||||
}
|
||||
// Make sure we do not duplicate a modifier key
|
||||
if (!MODIFIERS.includes(key)) {
|
||||
hotkey.push(key);
|
||||
}
|
||||
return hotkey.join("+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new hotkey.
|
||||
*
|
||||
* @param {string} hotkey
|
||||
* @param {()=>void} callback
|
||||
* @param {Object} options additional options
|
||||
* @param {boolean} [options.allowRepeat=false]
|
||||
* allow registration to perform multiple times when hotkey is held down
|
||||
* @param {boolean} [options.bypassEditableProtection=false]
|
||||
* if true the hotkey service will call this registration
|
||||
* even if an editable element is focused
|
||||
* @param {boolean} [options.global=false]
|
||||
* allow registration to perform no matter the UI active element
|
||||
* @param {HotkeyCallback} callback
|
||||
* @param {HotkeyOptions} [options]
|
||||
* @returns {number} registration token
|
||||
*/
|
||||
function registerHotkey(hotkey, callback, options = {}) {
|
||||
@@ -336,6 +382,7 @@ export const hotkeyService = {
|
||||
|
||||
// Add registration
|
||||
const token = nextToken++;
|
||||
/** @type {HotkeyRegistration} */
|
||||
const registration = {
|
||||
hotkey: hotkey.toLowerCase(),
|
||||
callback,
|
||||
@@ -343,8 +390,9 @@ export const hotkeyService = {
|
||||
allowRepeat: options && options.allowRepeat,
|
||||
bypassEditableProtection: options && options.bypassEditableProtection,
|
||||
global: options && options.global,
|
||||
area: options && options.area,
|
||||
validate: options && options.validate,
|
||||
};
|
||||
registrations.set(token, registration);
|
||||
|
||||
// Due to the way elements are mounted in the DOM by Owl (bottom-to-top),
|
||||
// we need to wait the next micro task tick to set the context owner of the registration.
|
||||
@@ -352,6 +400,7 @@ export const hotkeyService = {
|
||||
registration.activeElement = ui.activeElement;
|
||||
});
|
||||
|
||||
registrations.set(token, registration);
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -367,11 +416,8 @@ export const hotkeyService = {
|
||||
return {
|
||||
/**
|
||||
* @param {string} hotkey
|
||||
* @param {() => void} callback
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.allowRepeat=false]
|
||||
* @param {boolean} [options.bypassEditableProtection=false]
|
||||
* @param {boolean} [options.global=false]
|
||||
* @param {HotkeyCallback} callback
|
||||
* @param {HotkeyOptions} [options]
|
||||
* @returns {() => void}
|
||||
*/
|
||||
add(hotkey, callback, options = {}) {
|
||||
@@ -391,3 +437,4 @@ export const hotkeyService = {
|
||||
};
|
||||
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
/** @typedef {ReturnType<hotkeyService["start"]>} HotkeyService */
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* @param {Document} activeElement
|
||||
* @param {DOMString} selector
|
||||
* @param {Element} activeElement
|
||||
* @param {String} selector
|
||||
* @returns all selected and visible elements present in the activeElement
|
||||
*/
|
||||
export function getVisibleElements(activeElement, selector) {
|
||||
const visibleElements = [];
|
||||
for (const el of activeElement.querySelectorAll(selector)) {
|
||||
/** @type {NodeListOf<HTMLElement>} */
|
||||
const elements = activeElement.querySelectorAll(selector);
|
||||
for (const el of elements) {
|
||||
const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
|
||||
if (isVisible) {
|
||||
visibleElements.push(el);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
triggerHotkey,
|
||||
} from "../../helpers/utils";
|
||||
|
||||
const { Component, useState, xml } = owl;
|
||||
const { Component, useRef, useState, xml } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
@@ -242,18 +242,18 @@ QUnit.test("non-MacOS usability", async (assert) => {
|
||||
|
||||
const hotkey = env.services.hotkey;
|
||||
const key = "q";
|
||||
const commentEventAttrs = { key, bubbles: true };
|
||||
|
||||
// On non-MacOS, ALT is NOT replaced by CONTROL key
|
||||
let removeHotkey = hotkey.add(`alt+${key}`, () => assert.step(`alt+${key}`));
|
||||
await nextTick();
|
||||
|
||||
let keydown = new KeyboardEvent("keydown", { key, altKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
let keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, altKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([`alt+${key}`]);
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, ctrlKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, ctrlKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
@@ -263,13 +263,13 @@ QUnit.test("non-MacOS usability", async (assert) => {
|
||||
removeHotkey = hotkey.add(`control+${key}`, () => assert.step(`control+${key}`));
|
||||
await nextTick();
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, ctrlKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, ctrlKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([`control+${key}`]);
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, metaKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, metaKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
@@ -407,18 +407,19 @@ QUnit.test("MacOS usability", async (assert) => {
|
||||
|
||||
const hotkey = env.services.hotkey;
|
||||
const key = "q";
|
||||
const commentEventAttrs = { key, bubbles: true };
|
||||
|
||||
// On MacOS, ALT is replaced by CONTROL key
|
||||
let removeHotkey = hotkey.add(`alt+${key}`, () => assert.step(`alt+${key}`));
|
||||
await nextTick();
|
||||
|
||||
let keydown = new KeyboardEvent("keydown", { key, altKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
let keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, altKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, ctrlKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, ctrlKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([`alt+${key}`]);
|
||||
|
||||
@@ -428,13 +429,13 @@ QUnit.test("MacOS usability", async (assert) => {
|
||||
removeHotkey = hotkey.add(`control+${key}`, () => assert.step(`control+${key}`));
|
||||
await nextTick();
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, ctrlKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, ctrlKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key, metaKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commentEventAttrs, metaKey: true });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([`control+${key}`]);
|
||||
|
||||
@@ -761,11 +762,12 @@ QUnit.test("protects editable elements", async (assert) => {
|
||||
const input = target.querySelector(".foo");
|
||||
|
||||
assert.verifySteps([]);
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(["called"]);
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
input.focus();
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
[],
|
||||
@@ -785,11 +787,12 @@ QUnit.test("protects editable elements: can bypassEditableProtection", async (as
|
||||
const input = target.querySelector(".foo");
|
||||
|
||||
assert.verifySteps([]);
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(["called"]);
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
input.focus();
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["called"],
|
||||
@@ -809,15 +812,17 @@ QUnit.test("protects editable elements: an editable can allow hotkeys", async (a
|
||||
const barInput = target.querySelector(".bar");
|
||||
|
||||
assert.verifySteps([]);
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(["called"]);
|
||||
|
||||
fooInput.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
fooInput.focus();
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(["called"], "the callback gets called as the foo editable allows it");
|
||||
|
||||
barInput.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
barInput.focus();
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
[],
|
||||
@@ -829,17 +834,21 @@ QUnit.test("ignore numpad keys", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const key = "1";
|
||||
const commonEventAttrs = {
|
||||
altKey: true,
|
||||
bubbles: true,
|
||||
};
|
||||
|
||||
env.services.hotkey.add(`alt+${key}`, () => assert.step(key));
|
||||
await nextTick();
|
||||
|
||||
let keydown = new KeyboardEvent("keydown", { key, code: "Numpad1", altKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
let keydown = new KeyboardEvent("keydown", { ...commonEventAttrs, key, code: "Numpad1" });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
keydown = new KeyboardEvent("keydown", { key: "&", code: "Digit1", altKey: true });
|
||||
window.dispatchEvent(keydown);
|
||||
keydown = new KeyboardEvent("keydown", { ...commonEventAttrs, key: "&", code: "Digit1" });
|
||||
document.activeElement.dispatchEvent(keydown);
|
||||
await nextTick();
|
||||
assert.verifySteps(["1"]);
|
||||
});
|
||||
@@ -874,3 +883,215 @@ QUnit.test("within iframes", async (assert) => {
|
||||
await nextTick();
|
||||
assert.verifySteps(["called"]);
|
||||
});
|
||||
|
||||
QUnit.test("callback: received context", async (assert) => {
|
||||
class A extends Component {
|
||||
setup() {
|
||||
useHotkey("a", (context) => {
|
||||
assert.deepEqual(context, {
|
||||
area: undefined,
|
||||
target: document.activeElement,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
A.template = xml``;
|
||||
|
||||
class B extends Component {
|
||||
setup() {
|
||||
useHotkey(
|
||||
"b",
|
||||
(context) => {
|
||||
assert.deepEqual(context, {
|
||||
area: target,
|
||||
target: target,
|
||||
});
|
||||
},
|
||||
{ area: () => target }
|
||||
);
|
||||
}
|
||||
}
|
||||
B.template = xml``;
|
||||
|
||||
await mount(A, target, { env });
|
||||
await mount(B, target, { env });
|
||||
triggerHotkey("A");
|
||||
triggerHotkey("B");
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("operating area can be restricted", async (assert) => {
|
||||
class A extends Component {
|
||||
setup() {
|
||||
const areaRef = useRef("area");
|
||||
useHotkey(
|
||||
"space",
|
||||
({ area }) => {
|
||||
assert.step("RGNTDJÛ!");
|
||||
assert.strictEqual(area, target.querySelector(".two"));
|
||||
},
|
||||
{
|
||||
area: () => areaRef.el,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
A.template = xml`
|
||||
<div class="one" tabindex="0"/>
|
||||
<div class="two" tabindex="0" t-ref="area"/>
|
||||
`;
|
||||
await mount(A, target, { env });
|
||||
|
||||
target.querySelector(".one").focus();
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
target.querySelector(".two").focus();
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ!"]);
|
||||
});
|
||||
|
||||
QUnit.test("operating area and UI active element", async (assert) => {
|
||||
class UIOwnershipTakerComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("bouh");
|
||||
}
|
||||
}
|
||||
UIOwnershipTakerComponent.template = xml`<p class="owner" t-ref="bouh">bouh</p>`;
|
||||
class C extends Component {
|
||||
setup() {
|
||||
this.state = useState({ foo: false });
|
||||
const areaRef = useRef("area");
|
||||
useHotkey(
|
||||
"space",
|
||||
({ area }) => {
|
||||
assert.step("RGNTDJÛ!");
|
||||
assert.strictEqual(area, target.querySelector(".two"));
|
||||
},
|
||||
{
|
||||
area: () => areaRef.el,
|
||||
}
|
||||
);
|
||||
useHotkey(
|
||||
"backspace",
|
||||
({ area }) => {
|
||||
assert.step("RGNTDJÛ! (global)");
|
||||
assert.strictEqual(area, target.querySelector(".two"));
|
||||
},
|
||||
{
|
||||
area: () => areaRef.el,
|
||||
global: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
C.components = { UIOwnershipTakerComponent };
|
||||
C.template = xml`
|
||||
<main>
|
||||
<UIOwnershipTakerComponent t-if="state.foo" />
|
||||
<div class="one" tabindex="0"/>
|
||||
<div class="two" tabindex="0" t-ref="area"/>
|
||||
</main>
|
||||
`;
|
||||
const comp = await mount(C, target, { env });
|
||||
assert.strictEqual(env.services.ui.activeElement, document);
|
||||
|
||||
// Show the UIOwnershipTaker
|
||||
comp.state.foo = true;
|
||||
await nextTick();
|
||||
assert.hasClass(env.services.ui.activeElement, "owner");
|
||||
|
||||
// Trigger hotkeys from the 'one'
|
||||
target.querySelector(".one").focus();
|
||||
triggerHotkey("Space");
|
||||
triggerHotkey("BackSpace");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Trigger hotkeys from the 'two'
|
||||
target.querySelector(".two").focus();
|
||||
triggerHotkey("Space");
|
||||
triggerHotkey("BackSpace");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ! (global)"]);
|
||||
});
|
||||
|
||||
QUnit.test("validating option", async (assert) => {
|
||||
let isValid = false;
|
||||
class A extends Component {
|
||||
setup() {
|
||||
useHotkey(
|
||||
"space",
|
||||
() => {
|
||||
assert.step("RGNTDJÛ!");
|
||||
},
|
||||
{
|
||||
validate: (eventTarget) => {
|
||||
assert.strictEqual(eventTarget, document.activeElement);
|
||||
return isValid;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
A.template = xml``;
|
||||
await mount(A, target, { env });
|
||||
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ!"]);
|
||||
});
|
||||
|
||||
QUnit.test("operation area with validating option", async (assert) => {
|
||||
let isValid;
|
||||
class A extends Component {
|
||||
setup() {
|
||||
const areaRef = useRef("area");
|
||||
useHotkey(
|
||||
"space",
|
||||
() => {
|
||||
assert.step("RGNTDJÛ!");
|
||||
},
|
||||
{ area: () => areaRef.el, validate: () => isValid }
|
||||
);
|
||||
}
|
||||
}
|
||||
A.template = xml`
|
||||
<div class="one" tabindex="0"/>
|
||||
<div class="two" tabindex="0" t-ref="area"/>
|
||||
`;
|
||||
await mount(A, target, { env });
|
||||
|
||||
// Trigger hotkeys from the 'one'
|
||||
target.querySelector(".one").focus();
|
||||
|
||||
isValid = false;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Trigger hotkeys from the 'two'
|
||||
target.querySelector(".two").focus();
|
||||
|
||||
isValid = false;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ!"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user