This commit encapsulates the positioning logic of the Popover component
into a reusable hook. It also improves this logic by adding more
positioning directions and functionalities.
Before:
- only 4 directions (top, bottom, right, left : all middle aligned)
- the whole window viewport serves as a direction toggler
Now:
- 12 positions are available and are composed by 4 directions and 3
variants: {top,bottom,right,left}-{start,middle,end}
- the positioned element container is configurable. Default: window
Description:
When you use this hook, you make sure that your `popper` element (the
one you would like to position) is always placed at the given `position`
from the `reference` element. If positioning it so the `popper` element
is clipped off the `container` element, sensible fallback positions are
tried. If none is fully visible without being clipped off `container`,
the original position is used.
Usage:
usePosition(
reference, // [mandatory] HTMLElement or function giving HTMLElement
options // [optional] configure behaviour through options object
)
Options:
- popper (string): a t-ref reference. If none, component.el is used
- container (HTMLElement)
- margin (number)
- position (string): {top,bottom,right,left}-{start,middle,end}
Taskid-2578926
220 lines
7.4 KiB
JavaScript
220 lines
7.4 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { isMacOS } from "@web/core/browser/feature_detection";
|
|
import { patch, unpatch } from "@web/core/utils/patch";
|
|
import { registerCleanup } from "./cleanup";
|
|
import { download } from "@web/core/network/download";
|
|
|
|
const { Settings } = luxon;
|
|
|
|
/**
|
|
* Patch the native Date object
|
|
*
|
|
* Note that it will be automatically unpatched at the end of the test
|
|
*
|
|
* @param {number} [year]
|
|
* @param {number} [month]
|
|
* @param {number} [day]
|
|
* @param {number} [hours]
|
|
* @param {number} [minutes]
|
|
* @param {number} [seconds]
|
|
*/
|
|
export function patchDate(year, month, day, hours, minutes, seconds) {
|
|
const actualDate = new Date();
|
|
const fakeDate = new Date(year, month, day, hours, minutes, seconds);
|
|
const timeInterval = actualDate.getTime() - fakeDate.getTime();
|
|
const originalNow = Settings.now;
|
|
Settings.now = () => Date.now() - timeInterval;
|
|
registerCleanup(() => {
|
|
Settings.now = originalNow;
|
|
});
|
|
}
|
|
|
|
let nextId = 1;
|
|
|
|
/**
|
|
*
|
|
* @param {Object} obj object to patch
|
|
* @param {Object} patchValue the actual patch description
|
|
* @param {{pure?: boolean}} [options]
|
|
*/
|
|
export function patchWithCleanup(obj, patchValue, options) {
|
|
const patchName = `__test_patch_${nextId++}__`;
|
|
patch(obj, patchName, patchValue, options);
|
|
registerCleanup(() => {
|
|
unpatch(obj, patchName);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {HTMLElement}
|
|
*/
|
|
export function getFixture() {
|
|
if (QUnit.config.debug) {
|
|
return document.body;
|
|
} else {
|
|
return document.querySelector("#qunit-fixture");
|
|
}
|
|
}
|
|
|
|
export async function nextTick() {
|
|
await new Promise((resolve) => window.requestAnimationFrame(resolve));
|
|
await new Promise((resolve) => setTimeout(resolve));
|
|
}
|
|
|
|
export function makeDeferred() {
|
|
/** @type {(value:any)=>void} */
|
|
let resolve;
|
|
/** @type {(reason?:any)=>void} */
|
|
let reject;
|
|
const prom = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return Object.assign(prom, { resolve, reject });
|
|
}
|
|
|
|
function findElement(el, selector) {
|
|
let target = el;
|
|
if (selector) {
|
|
const els = el.querySelectorAll(selector);
|
|
if (els.length === 0) {
|
|
throw new Error(`No element found (selector: ${selector})`);
|
|
}
|
|
if (els.length > 1) {
|
|
throw new Error(`Found ${els.length} elements, instead of 1 (selector: ${selector})`);
|
|
}
|
|
target = els[0];
|
|
}
|
|
return target;
|
|
}
|
|
|
|
const keyboardEventBubble = (args) =>
|
|
Object.assign({}, args, { bubbles: true, keyCode: args.which });
|
|
|
|
const mouseEventMapping = (args) =>
|
|
Object.assign({}, args, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: args ? args.pageX : undefined,
|
|
clientY: args ? args.pageY : undefined,
|
|
view: window,
|
|
});
|
|
|
|
const mouseEventNoBubble = (args) =>
|
|
Object.assign({}, args, {
|
|
bubbles: false,
|
|
cancelable: false,
|
|
clientX: args ? args.pageX : undefined,
|
|
clientY: args ? args.pageY : undefined,
|
|
view: window,
|
|
});
|
|
|
|
const noBubble = (args) => Object.assign({}, args, { bubbles: false });
|
|
|
|
const onlyBubble = (args) => Object.assign({}, args, { bubbles: true });
|
|
|
|
// TriggerEvent constructor/args processor mapping
|
|
const EVENT_TYPES = {
|
|
auxclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
click: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
contextmenu: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
dblclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mousedown: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseup: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mousemove: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseenter: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
|
mouseleave: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
|
mouseover: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseout: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
focus: { constructor: FocusEvent, processParameters: noBubble },
|
|
focusin: { constructor: FocusEvent, processParameters: onlyBubble },
|
|
blur: { constructor: FocusEvent, processParameters: noBubble },
|
|
cut: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
copy: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
paste: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
keydown: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
keypress: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
keyup: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
drag: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragend: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragenter: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragstart: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragleave: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragover: { constructor: DragEvent, processParameters: onlyBubble },
|
|
drop: { constructor: DragEvent, processParameters: onlyBubble },
|
|
input: { constructor: InputEvent, processParameters: onlyBubble },
|
|
compositionstart: { constructor: CompositionEvent, processParameters: onlyBubble },
|
|
compositionend: { constructor: CompositionEvent, processParameters: onlyBubble },
|
|
};
|
|
|
|
export async function triggerEvent(el, selector, eventType, eventAttrs = {}) {
|
|
let event;
|
|
if (eventType in EVENT_TYPES) {
|
|
const { constructor, processParameters } = EVENT_TYPES[eventType];
|
|
event = new constructor(eventType, processParameters(eventAttrs));
|
|
} else {
|
|
event = new Event(eventType, Object.assign({}, eventAttrs, { bubbles: true }));
|
|
}
|
|
const target = findElement(el, selector);
|
|
target.dispatchEvent(event);
|
|
await nextTick();
|
|
}
|
|
|
|
export async function triggerEvents(el, querySelector, events) {
|
|
for (let e = 0; e < events.length; e++) {
|
|
await triggerEvent(el, querySelector, events[e]);
|
|
}
|
|
}
|
|
|
|
export function click(el, selector) {
|
|
return triggerEvent(el, selector, "click", { bubbles: true, cancelable: true });
|
|
}
|
|
|
|
/**
|
|
* Triggers an hotkey properly disregarding the operating system.
|
|
*
|
|
* @param {string} hotkey
|
|
* @param {boolean} addOverlayModParts
|
|
* @param {KeyboardEventInit} eventAttrs
|
|
*/
|
|
export function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
|
|
eventAttrs.key = hotkey.split("+").pop();
|
|
|
|
if (/shift/i.test(hotkey)) {
|
|
eventAttrs.shiftKey = true;
|
|
}
|
|
|
|
if (/control/i.test(hotkey)) {
|
|
if (isMacOS()) {
|
|
eventAttrs.metaKey = true;
|
|
} else {
|
|
eventAttrs.ctrlKey = true;
|
|
}
|
|
}
|
|
|
|
if (/alt/i.test(hotkey) || addOverlayModParts) {
|
|
if (isMacOS()) {
|
|
eventAttrs.ctrlKey = true;
|
|
} else {
|
|
eventAttrs.altKey = true;
|
|
}
|
|
}
|
|
|
|
window.dispatchEvent(new KeyboardEvent("keydown", eventAttrs));
|
|
window.dispatchEvent(new KeyboardEvent("keyup", eventAttrs));
|
|
}
|
|
|
|
export async function legacyExtraNextTick() {
|
|
return nextTick();
|
|
}
|
|
|
|
export function mockDownload(cb) {
|
|
patchWithCleanup(download, { _download: cb });
|
|
}
|
|
|
|
export const hushConsole = Object.create(null);
|
|
for (const propName of Object.keys(window.console)) {
|
|
hushConsole[propName] = () => {};
|
|
}
|