[IMP] web: introduce position hook

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
This commit is contained in:
Bruno Boi
2021-08-03 08:51:36 +00:00
parent 9eed0d4b34
commit 67b1fdcd39
8 changed files with 1089 additions and 240 deletions
+5 -133
View File
@@ -1,80 +1,15 @@
/** @odoo-module **/
import { debounce, throttle } from "../utils/timing";
import { usePosition } from "../position/position_hook";
const { Component } = owl;
const { useExternalListener, useRef } = owl.hooks;
export class Popover extends Component {
setup() {
this.popoverRef = useRef("popover");
useExternalListener(document, "scroll", throttle(this.compute, 50), { capture: true });
useExternalListener(window, "resize", debounce(this.compute, 250));
}
mounted() {
this.compute();
}
patched() {
this.compute();
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Computes the popover according to its props. This method will try to
* position the popover as requested (according to the `position` props).
* If the requested position does not fit the viewport, other positions will
* be tried in a clockwise order starting a the requested position
* (e.g. starting from left: top, right, bottom). If no position is found
* that fits the viewport, "bottom" is used.
*
* @private
*/
compute() {
const positioningData = this.constructor.computePositioningData(
this.popoverRef.el,
this.props.target
);
const ORDERED_POSITIONS = ["top", "bottom", "left", "right"];
// copy the default ordered position to avoid updating them in place
const positionIndex = ORDERED_POSITIONS.indexOf(this.props.position);
// check if the requested position fits the viewport; if not,
// try all other positions and find one that does
const position = ORDERED_POSITIONS.slice(positionIndex)
.concat(ORDERED_POSITIONS.slice(0, positionIndex))
.map((pos) => positioningData[pos])
.find((pos) => {
this.popoverRef.el.style.top = `${pos.top}px`;
this.popoverRef.el.style.left = `${pos.left}px`;
const rect = this.popoverRef.el.getBoundingClientRect();
const html = document.documentElement;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || html.clientHeight) &&
rect.right <= (window.innerWidth || html.clientWidth)
);
});
// remove all existing positioning classes
for (const pos of ORDERED_POSITIONS) {
this.popoverRef.el.classList.remove(`o_popover_${pos}`);
}
if (position) {
// apply the preferred found position that fits the viewport
this.popoverRef.el.classList.add(`o_popover_${position.name}`);
} else {
// use the given `position` props because no position fits
this.popoverRef.el.style.top = `${positioningData[this.props.position].top}px`;
this.popoverRef.el.style.left = `${positioningData[this.props.position].left}px`;
this.popoverRef.el.classList.add(`o_popover_${this.props.position}`);
}
usePosition(this.props.target, {
margin: 16,
position: this.props.position,
});
}
}
@@ -93,66 +28,3 @@ Popover.props = {
},
target: HTMLElement,
};
/**
* Compute the expected positioning coordinates for each possible
* positioning based on the target and popover sizes.
* In particular the popover must not overflow the viewport in any
* direction, it should actually stay at `margin` distance from the
* border to look good.
*
* @param {HTMLElement} popoverElement The popover element
* @param {HTMLElement} targetElement The target element, to which
* the popover will be visually "bound"
* @param {number} [margin=16] Minimal accepted margin from the border
* of the viewport.
* @returns {Object}
*/
Popover.computePositioningData = function (popoverElement, targetElement, margin = 16) {
// set target position, possible position
const boundingRectangle = targetElement.getBoundingClientRect();
const targetTop = boundingRectangle.top;
const targetLeft = boundingRectangle.left;
const targetHeight = targetElement.offsetHeight;
const targetWidth = targetElement.offsetWidth;
const popoverHeight = popoverElement.offsetHeight;
const popoverWidth = popoverElement.offsetWidth;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const leftOffsetForVertical = Math.max(
margin,
Math.min(
Math.round(targetLeft - (popoverWidth - targetWidth) / 2),
windowWidth - popoverWidth - margin
)
);
const topOffsetForHorizontal = Math.max(
margin,
Math.min(
Math.round(targetTop - (popoverHeight - targetHeight) / 2),
windowHeight - popoverHeight - margin
)
);
return {
top: {
name: "top",
top: Math.round(targetTop - popoverHeight),
left: leftOffsetForVertical,
},
right: {
name: "right",
top: topOffsetForHorizontal,
left: Math.round(targetLeft + targetWidth),
},
bottom: {
name: "bottom",
top: Math.round(targetTop + targetHeight),
left: leftOffsetForVertical,
},
left: {
name: "left",
top: topOffsetForHorizontal,
left: Math.round(targetLeft - popoverWidth),
},
};
};
+78 -79
View File
@@ -54,99 +54,98 @@
$animation-time: 0.2s;
max-width: 100vw;
position: fixed;
top: 0; // will be updated in js
left: 0; // but let's set it in the viewport in case of test/debug
z-index: 1060;
border: 1px solid $border-color;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
&_top {
animation: $animation-time slide-top;
margin-top: -$arrow-size;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -$arrow-size;
border: $arrow-size solid transparent;
border-top-color: $bg-color;
}
&::before {
margin-top: 1px;
border-top-color: $border-color;
}
}
&_right {
animation: $animation-time slide-right;
margin-left: $arrow-size;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 50%;
right: 100%;
&.o-popper-position {
&--tm {
animation: $animation-time slide-top;
margin-top: -$arrow-size;
border: $arrow-size solid transparent;
border-right-color: $bg-color;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -$arrow-size;
border: $arrow-size solid transparent;
border-top-color: $bg-color;
}
&::before {
margin-top: 1px;
border-top-color: $border-color;
}
}
&::before {
margin-right: 1px;
border-right-color: $border-color;
&--rm {
animation: $animation-time slide-right;
margin-left: $arrow-size;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 50%;
right: 100%;
margin-top: -$arrow-size;
border: $arrow-size solid transparent;
border-right-color: $bg-color;
}
&::before {
margin-right: 1px;
border-right-color: $border-color;
}
}
}
&_bottom {
animation: $animation-time slide-bottom;
margin-top: $arrow-size;
&--bm {
animation: $animation-time slide-bottom;
margin-top: $arrow-size;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
bottom: 100%;
left: 50%;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -$arrow-size;
border: $arrow-size solid transparent;
border-bottom-color: $bg-color;
}
&::before {
margin-bottom: 1px;
border-bottom-color: $border-color;
}
}
&--lm {
animation: $animation-time slide-left;
margin-left: -$arrow-size;
border: $arrow-size solid transparent;
border-bottom-color: $bg-color;
}
&::before {
margin-bottom: 1px;
border-bottom-color: $border-color;
}
}
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 50%;
left: 100%;
margin-top: -$arrow-size;
border: $arrow-size solid transparent;
border-left-color: $bg-color;
}
&_left {
animation: $animation-time slide-left;
margin-left: -$arrow-size;
&::before,
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 50%;
left: 100%;
margin-top: -$arrow-size;
border: $arrow-size solid transparent;
border-left-color: $bg-color;
}
&::before {
margin-left: 1px;
border-left-color: $border-color;
&::before {
margin-left: 1px;
border-left-color: $border-color;
}
}
}
}
@@ -5,7 +5,6 @@
<div role="tooltip" class="o_popover"
t-att-class="props.popoverClass"
t-on-popover-compute="compute"
t-ref="popover"
>
<t t-slot="default" />
</div>
@@ -0,0 +1,5 @@
.o-popper-position {
position: fixed;
top: 0;
left: 0;
}
@@ -0,0 +1,229 @@
/** @odoo-module */
import { useEffect } from "@web/core/utils/hooks";
import { debounce, throttle } from "../utils/timing";
/**
* @typedef {{
* popper?: string;
* container?: HTMLElement;
* margin?: number;
* position?: Position;
* }} Options
*
* @typedef {{
* directions: (DirectionsDataKey)[];
* variants: (VariantsDataKey)[];
* get: (d?: DirectionsDataKey, v?: VariantsDataKey, containerRestricted?: boolean) => PositioningSolution | null;
* }} Positioning
*
* @typedef {keyof DirectionsData} DirectionsDataKey
* @typedef {{
* t: number;
* b: number;
* l: number;
* r: number;
* }} DirectionsData
*
* @typedef {keyof VariantsData} VariantsDataKey
* @typedef {{
* vs: number;
* vm: number;
* ve: number;
* hs: number;
* hm: number;
* he: number;
* }} VariantsData
*
* @typedef {"top" | "left" | "bottom" | "right"} Direction
* @typedef {Direction
* | "top-start" | "top-middle" | "top-end"
* | "left-start" | "left-middle" | "left-end"
* | "right-start" | "right-middle" | "right-end"
* | "bottom-start" | "bottom-middle" | "bottom-end"} Position
*
* @typedef {{ className: string, top: number, left: number }} PositioningSolution
*/
const { hooks } = owl;
const { useComponent, useExternalListener, useRef } = hooks;
const POPPER_CLASS = "o-popper-position";
const DIRECTION_FLIP_ORDER = { top: "tbrl", right: "rltb", bottom: "btrl", left: "lrbt" };
const VARIANT_FLIP_ORDER = { start: "sme", middle: "mse", end: "ems" };
/** @type {Options} */
export const DEFAULTS = {
margin: 0,
position: "bottom",
};
/**
* Computes positioning data used to determine each possible position
* based on the reference, popper, and container sizes.
* Particularly, a popper must not overflow the container in any direction,
* it should actually stay at `margin` distance from the border to look good.
*
* @param {HTMLElement} reference
* @param {HTMLElement} popper
* @param {Options} options
* @returns {Positioning} a positioning object containing:
* - ascendingly sorted directions and variants
* - a method returning style to apply to the popper for a given direction and variant
*/
export function computePositioning(reference, popper, options) {
const { container, margin, position } = options;
// Retrieve directions and variants
const [directionKey, variantKey = "middle"] = position.split("-");
const directions = DIRECTION_FLIP_ORDER[directionKey];
const variants = VARIANT_FLIP_ORDER[variantKey];
// Boxes
const popBox = popper.getBoundingClientRect();
const refBox = reference.getBoundingClientRect();
const contBox = container.getBoundingClientRect();
// Compute positioning data
/** @type {DirectionsData} */
const directionsData = {
t: refBox.top - popBox.height - margin,
b: refBox.bottom + margin,
r: refBox.right + margin,
l: refBox.left - popBox.width - margin,
};
/** @type {VariantsData} */
const variantsData = {
vs: refBox.left,
vm: refBox.left + refBox.width / 2 + -popBox.width / 2,
ve: refBox.right - popBox.width,
hs: refBox.top,
hm: refBox.top + refBox.height / 2 + -popBox.height / 2,
he: refBox.bottom - popBox.height,
};
function get(d = directions[0], v = variants[0], containerRestricted = false) {
const vertical = ["t", "b"].includes(d);
const variantPrefix = vertical ? "v" : "h";
const directionValue = directionsData[d];
const variantValue = variantsData[variantPrefix + v];
if (containerRestricted) {
const [directionSize, variantSize] = vertical
? [popBox.height + margin, popBox.width]
: [popBox.width, popBox.height + margin];
const [directionMin, directionMax] = vertical
? [contBox.top, contBox.bottom]
: [contBox.left, contBox.right];
const [variantMin, variantMax] = vertical
? [contBox.left, contBox.right]
: [contBox.top, contBox.bottom];
// Abort if outside container boundaries
const directionOverflow =
directionValue < directionMin || directionValue + directionSize > directionMax;
const variantOverflow =
variantValue < variantMin || variantValue + variantSize > variantMax;
if (directionOverflow || variantOverflow) {
return null;
}
}
const positioning = vertical
? {
top: directionValue,
left: variantValue,
}
: {
top: variantValue,
left: directionValue,
};
return { ...positioning, className: `${POPPER_CLASS}--${d}${v}` };
}
return {
directions,
variants,
get,
};
}
/**
* This method will try to position the popper as requested (according to options).
* If the requested position does not fit the container, other positions will be
* tried in different direction and variant flip orders (depending on the requested position).
* If no position is found that fits the container, the requested position stays used.
*
* When the final position is applied, a corresponding CSS class is also added to the popper.
* This could be used to further styling.
*
* @param {HTMLElement} reference
* @param {HTMLElement} popper
* @param {Options} options
*/
function reposition(reference, popper, options) {
options = {
container: document.documentElement,
...DEFAULTS,
...options,
};
// Reset all existing popper classes and only leave it as standalone
for (const popperClass of popper.classList) {
if (popperClass.startsWith(POPPER_CLASS)) {
popper.classList.remove(popperClass);
}
}
popper.classList.add(POPPER_CLASS);
// Compute positioning and find first match
const positioning = computePositioning(reference, popper, options);
for (const d of positioning.directions) {
for (const v of positioning.variants) {
const posData = positioning.get(d, v, true);
if (!posData) {
continue;
}
// From now, a position match have been found.
// Apply styles
const { className, top, left } = posData;
popper.classList.add(className);
popper.style.top = `${top}px`;
popper.style.left = `${left}px`;
return;
}
}
// use the given `position` because no position fits
const { className, top, left } = positioning.get();
popper.classList.add(className);
popper.style.top = `${top}px`;
popper.style.left = `${left}px`;
}
/**
* Makes sure that the `popper` element is always
* placed at `position` from the `reference` element.
* If doing so the `popper` element is clipped off `container`,
* sensible fallback positions are tried.
* If all of fallback positions are also clipped off `container`,
* the original position is used.
*
* @param {HTMLElement | (()=>HTMLElement)} reference
* @param {Options} options
*/
export function usePosition(reference, options) {
options = { ...DEFAULTS, ...options };
const { popper } = options;
const popperRef = popper ? useRef(popper) : useComponent();
const getReference = reference instanceof HTMLElement ? () => reference : reference;
const update = () => {
if (popperRef.el) {
reposition(getReference(), popperRef.el, options);
}
};
useEffect(update);
useExternalListener(document, "scroll", throttle(update, 50), { capture: true });
useExternalListener(window, "resize", debounce(update, 250));
}
@@ -2,31 +2,23 @@
import { Popover } from "@web/core/popover/popover";
import { registerCleanup } from "../../helpers/cleanup";
import { getFixture, patchWithCleanup } from "../../helpers/utils";
import { getFixture } from "../../helpers/utils";
const { mount } = owl;
let fixture;
let popoverTarget;
function computePositioningDataTest(popover, target) {
const rect = target.getBoundingClientRect();
const top = Math.floor(rect.top);
const left = Math.floor(rect.left);
return {
top: { name: "top", top, left },
bottom: { name: "bottom", top: top + 10, left },
left: { name: "left", top: top + 10, left },
right: { name: "right", top: top + 10, left: left + 10 },
};
}
const positionClassMap = {
top: "o-popper-position--tm",
right: "o-popper-position--rm",
bottom: "o-popper-position--bm",
left: "o-popper-position--lm",
};
function pointsTo(popover, element, position) {
const hasCorrectClass = popover.classList.contains(`o_popover_${position}`);
const expectedPosition = Popover.computePositioningData(popover, element)[position];
const correctLeft = parseFloat(popover.style.left) === expectedPosition.left;
const correctTop = parseFloat(popover.style.top) === expectedPosition.top;
return hasCorrectClass && correctLeft && correctTop;
function pointsTo(popover, position) {
const hasCorrectClass = popover.classList.contains(positionClassMap[position]);
return hasCorrectClass;
}
QUnit.module("Popover", {
@@ -40,10 +32,6 @@ QUnit.module("Popover", {
registerCleanup(() => {
fixture.removeChild(popoverTarget);
});
patchWithCleanup(Popover, {
computePositioningData: computePositioningDataTest,
});
},
});
@@ -64,7 +52,7 @@ QUnit.test("popover is rendered nearby target (default)", async (assert) => {
});
const popoverEl = fixture.querySelector(".o_popover");
assert.ok(pointsTo(popoverEl, popoverTarget, "bottom"));
assert.ok(pointsTo(popoverEl, "bottom"));
popover.destroy();
});
@@ -75,7 +63,7 @@ QUnit.test("popover is rendered nearby target (bottom)", async (assert) => {
});
const popoverEl = fixture.querySelector(".o_popover");
assert.ok(pointsTo(popoverEl, popoverTarget, "bottom"));
assert.ok(pointsTo(popoverEl, "bottom"));
popover.destroy();
});
@@ -86,7 +74,7 @@ QUnit.test("popover is rendered nearby target (top)", async (assert) => {
});
const popoverEl = fixture.querySelector(".o_popover");
assert.ok(pointsTo(popoverEl, popoverTarget, "top"));
assert.ok(pointsTo(popoverEl, "top"));
popover.destroy();
});
@@ -97,7 +85,7 @@ QUnit.test("popover is rendered nearby target (left)", async (assert) => {
});
const popoverEl = fixture.querySelector(".o_popover");
assert.ok(pointsTo(popoverEl, popoverTarget, "left"));
assert.ok(pointsTo(popoverEl, "left"));
popover.destroy();
});
@@ -108,6 +96,6 @@ QUnit.test("popover is rendered nearby target (right)", async (assert) => {
});
const popoverEl = fixture.querySelector(".o_popover");
assert.ok(pointsTo(popoverEl, popoverTarget, "right"));
assert.ok(pointsTo(popoverEl, "right"));
popover.destroy();
});
@@ -0,0 +1,754 @@
/** @odoo-module */
import { browser } from "@web/core/browser/browser";
import { computePositioning, DEFAULTS, usePosition } from "@web/core/position/position_hook";
import { registerCleanup } from "../helpers/cleanup";
import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../helpers/utils";
const { Component, mount, tags } = owl;
const { css, xml } = tags;
let container;
let reference;
/**
* @param {HTMLElement} popper
* @param {import("@web/core/position/position_hook").Position} [position="bottom"]
* @returns {boolean}
*/
function isWellPositioned(popper, position = "bottom") {
/** @type {import("@web/core/position/position_hook").Options} */
const options = {
...DEFAULTS,
container,
position,
};
const [direction, variant = "middle"] = position.split("-");
const d = /** @type {import("@web/core/position/position_hook").DirectionsDataKey} */ (direction[0]);
const v = /** @type {import("@web/core/position/position_hook").VariantsDataKey} */ (variant[0]);
const posSolution = computePositioning(reference, popper, options).get(d, v);
const hasCorrectClass = popper.classList.contains(posSolution.className);
const correctLeft = parseFloat(popper.style.left) === posSolution.left;
const correctTop = parseFloat(popper.style.top) === posSolution.top;
return hasCorrectClass && correctLeft && correctTop;
}
class TestComp extends Component {
setup() {
usePosition(reference, this.constructor.popperOptions);
}
}
TestComp.style = css`
#popper {
background-color: cyan;
height: 100px;
width: 100px;
}
`;
TestComp.template = xml`<div id="popper"/>`;
/** @type {import("@web/core/position/position_hook").Options} */
TestComp.popperOptions = {};
QUnit.module("usePosition Hook", {
async beforeEach() {
reference = document.createElement("div");
reference.id = "reference";
reference.style.backgroundColor = "yellow";
reference.style.height = "50px";
reference.style.width = "50px";
// Force container style, to make these tests independent of screen size
container = document.createElement("div");
container.id = "container";
container.style.backgroundColor = "pink";
container.style.height = "450px";
container.style.width = "450px";
container.style.display = "flex";
container.style.alignItems = "center";
container.style.justifyContent = "center";
container.appendChild(reference);
getFixture().prepend(container);
registerCleanup(() => {
getFixture().removeChild(container);
});
TestComp.popperOptions = { container };
patchWithCleanup(browser, { setTimeout: (func) => func() });
},
});
QUnit.test("default position is bottom", async (assert) => {
const popper = await mount(TestComp, { target: container });
assert.ok(isWellPositioned(popper.el, "bottom"));
});
QUnit.test("can add margin", async (assert) => {
let margin = 0;
Object.assign(TestComp.popperOptions, { margin });
let popper = await mount(TestComp, { target: container });
const popBox1 = popper.el.getBoundingClientRect();
popper.destroy();
margin = 20;
Object.assign(TestComp.popperOptions, { margin });
popper = await mount(TestComp, { target: container });
const popBox2 = popper.el.getBoundingClientRect();
popper.destroy();
assert.strictEqual(popBox1.top + margin, popBox2.top);
});
QUnit.test("can use a t-ref as popper", async (assert) => {
patchWithCleanup(TestComp, {
popperOptions: { ...TestComp.popperOptions, popper: "popperRef" },
template: xml`
<div id="not-popper">
<div id="popper" t-ref="popperRef"/>
</div>
`,
});
const comp = await mount(TestComp, { target: container });
assert.notOk(isWellPositioned(comp.el));
assert.ok(isWellPositioned(comp.el.querySelector("#popper")));
});
const getPositionTest = (position, positionToCheck) => {
return async (assert) => {
Object.assign(TestComp.popperOptions, { position });
const popper = await mount(TestComp, { target: container });
positionToCheck = positionToCheck || position;
assert.ok(isWellPositioned(popper.el, positionToCheck));
};
};
QUnit.test("position top", getPositionTest("top"));
QUnit.test("position left", getPositionTest("left"));
QUnit.test("position bottom", getPositionTest("bottom"));
QUnit.test("position right", getPositionTest("right"));
QUnit.test("position top-start", getPositionTest("top-start"));
QUnit.test("position top-middle", getPositionTest("top-middle"));
QUnit.test("position top-end", getPositionTest("top-end"));
QUnit.test("position left-start", getPositionTest("left-start"));
QUnit.test("position left-middle", getPositionTest("left-middle"));
QUnit.test("position left-end", getPositionTest("left-end"));
QUnit.test("position bottom-start", getPositionTest("bottom-start"));
QUnit.test("position bottom-middle", getPositionTest("bottom-middle"));
QUnit.test("position bottom-end", getPositionTest("bottom-end"));
QUnit.test("position right-start", getPositionTest("right-start"));
QUnit.test("position right-middle", getPositionTest("right-middle"));
QUnit.test("position right-end", getPositionTest("right-end"));
QUnit.test("position top === top-middle", getPositionTest("top", "top-middle"));
QUnit.test("position left === left-middle", getPositionTest("left", "left-middle"));
QUnit.test("position bottom === bottom-middle", getPositionTest("bottom", "bottom-middle"));
QUnit.test("position right === right-middle", getPositionTest("right", "right-middle"));
const CONTAINER_STYLE_MAP = {
top: { alignItems: "flex-start" },
bottom: { alignItems: "flex-end" },
left: { justifyContent: "flex-start" },
right: { justifyContent: "flex-end" },
slimfit: { height: "100px", width: "100px" }, // height and width of popper
h125: { height: "125px" }, // height of popper + 1/2 reference
w125: { width: "125px" }, // width of popper + 1/2 reference
};
const getRepositionTest = (from, to, containerStyleChanges) => {
return async (assert) => {
Object.assign(TestComp.popperOptions, { position: from });
const popper = await mount(TestComp, { target: container });
assert.ok(isWellPositioned(popper.el, from), `points to ${from} direction`);
// Change container style and force update
for (const styleToApply of containerStyleChanges.split(" ")) {
Object.assign(container.style, CONTAINER_STYLE_MAP[styleToApply]);
}
triggerEvent(document, null, "scroll");
await nextTick();
assert.ok(isWellPositioned(popper.el, to), `points to ${to} direction`);
};
};
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from top-start to bottom-start",
getRepositionTest("top-start", "bottom-start", "top")
);
QUnit.test(
"reposition from top-start to bottom-middle",
getRepositionTest("top-start", "bottom-middle", "top w125")
);
QUnit.test(
"reposition from top-start to bottom-end",
getRepositionTest("top-start", "bottom-end", "top right")
);
QUnit.test(
"reposition from top-start to right-start",
getRepositionTest("top-start", "right-start", "h125 top")
);
QUnit.test(
"reposition from top-start to right-middle",
getRepositionTest("top-start", "right-middle", "h125")
);
QUnit.test(
"reposition from top-start to right-end",
getRepositionTest("top-start", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from top-start to left-start",
getRepositionTest("top-start", "left-start", "h125 right top")
);
QUnit.test(
"reposition from top-start to left-middle",
getRepositionTest("top-start", "left-middle", "h125 right")
);
QUnit.test(
"reposition from top-start to left-end",
getRepositionTest("top-start", "left-end", "h125 right bottom")
);
QUnit.test(
"reposition from top-start to top-start",
getRepositionTest("top-start", "top-start", "slimfit")
);
QUnit.test(
"reposition from top-start to top-middle",
getRepositionTest("top-start", "top-middle", "w125")
);
QUnit.test(
"reposition from top-start to top-end",
getRepositionTest("top-start", "top-end", "right")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from top-middle to bottom-start",
getRepositionTest("top-middle", "bottom-start", "top left")
);
QUnit.test(
"reposition from top-middle to bottom-middle",
getRepositionTest("top-middle", "bottom-middle", "top")
);
QUnit.test(
"reposition from top-middle to bottom-end",
getRepositionTest("top-middle", "bottom-end", "top right")
);
QUnit.test(
"reposition from top-middle to right-start",
getRepositionTest("top-middle", "right-start", "h125 top")
);
QUnit.test(
"reposition from top-middle to right-middle",
getRepositionTest("top-middle", "right-middle", "h125")
);
QUnit.test(
"reposition from top-middle to right-end",
getRepositionTest("top-middle", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from top-middle to left-start",
getRepositionTest("top-middle", "left-start", "h125 right top")
);
QUnit.test(
"reposition from top-middle to left-middle",
getRepositionTest("top-middle", "left-middle", "h125 right")
);
QUnit.test(
"reposition from top-middle to left-end",
getRepositionTest("top-middle", "left-end", "h125 right bottom")
);
QUnit.test(
"reposition from top-middle to top-start",
getRepositionTest("top-middle", "top-start", "left")
);
QUnit.test(
"reposition from top-middle to top-middle",
getRepositionTest("top-middle", "top-middle", "slimfit")
);
QUnit.test(
"reposition from top-middle to top-end",
getRepositionTest("top-middle", "top-end", "right")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from top-end to bottom-start",
getRepositionTest("top-end", "bottom-start", "top left")
);
QUnit.test(
"reposition from top-end to bottom-middle",
getRepositionTest("top-end", "bottom-middle", "top w125")
);
QUnit.test(
"reposition from top-end to bottom-end",
getRepositionTest("top-end", "bottom-end", "top")
);
QUnit.test(
"reposition from top-end to right-start",
getRepositionTest("top-end", "right-start", "h125 top")
);
QUnit.test(
"reposition from top-end to right-middle",
getRepositionTest("top-end", "right-middle", "h125")
);
QUnit.test(
"reposition from top-end to right-end",
getRepositionTest("top-end", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from top-end to left-start",
getRepositionTest("top-end", "left-start", "h125 right top")
);
QUnit.test(
"reposition from top-end to left-middle",
getRepositionTest("top-end", "left-middle", "h125 right")
);
QUnit.test(
"reposition from top-end to left-end",
getRepositionTest("top-end", "left-end", "h125 right bottom")
);
QUnit.test(
"reposition from top-end to top-start",
getRepositionTest("top-end", "top-start", "left")
);
QUnit.test(
"reposition from top-end to top-middle",
getRepositionTest("top-end", "top-middle", "w125")
);
QUnit.test(
"reposition from top-end to top-end",
getRepositionTest("top-end", "top-end", "slimfit")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from left-start to bottom-start",
getRepositionTest("left-start", "bottom-start", "w125 left")
);
QUnit.test(
"reposition from left-start to bottom-middle",
getRepositionTest("left-start", "bottom-middle", "w125")
);
QUnit.test(
"reposition from left-start to bottom-end",
getRepositionTest("left-start", "bottom-end", "w125 right")
);
QUnit.test(
"reposition from left-start to right-start",
getRepositionTest("left-start", "right-start", "left")
);
QUnit.test(
"reposition from left-start to right-middle",
getRepositionTest("left-start", "right-middle", "left h125")
);
QUnit.test(
"reposition from left-start to right-end",
getRepositionTest("left-start", "right-end", "left bottom")
);
QUnit.test(
"reposition from left-start to left-start",
getRepositionTest("left-start", "left-start", "slimfit")
);
QUnit.test(
"reposition from left-start to left-middle",
getRepositionTest("left-start", "left-middle", "h125")
);
QUnit.test(
"reposition from left-start to left-end",
getRepositionTest("left-start", "left-end", "bottom")
);
QUnit.test(
"reposition from left-start to top-start",
getRepositionTest("left-start", "top-start", "w125 bottom left")
);
QUnit.test(
"reposition from left-start to top-middle",
getRepositionTest("left-start", "top-middle", "w125 bottom")
);
QUnit.test(
"reposition from left-start to top-end",
getRepositionTest("left-start", "top-end", "w125 bottom right")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from left-middle to bottom-start",
getRepositionTest("left-middle", "bottom-start", "w125 left")
);
QUnit.test(
"reposition from left-middle to bottom-middle",
getRepositionTest("left-middle", "bottom-middle", "w125")
);
QUnit.test(
"reposition from left-middle to bottom-end",
getRepositionTest("left-middle", "bottom-end", "w125 right")
);
QUnit.test(
"reposition from left-middle to right-start",
getRepositionTest("left-middle", "right-start", "left top")
);
QUnit.test(
"reposition from left-middle to right-middle",
getRepositionTest("left-middle", "right-middle", "left")
);
QUnit.test(
"reposition from left-middle to right-end",
getRepositionTest("left-middle", "right-end", "left bottom")
);
QUnit.test(
"reposition from left-middle to top-start",
getRepositionTest("left-middle", "top-start", "w125 bottom left")
);
QUnit.test(
"reposition from left-middle to top-middle",
getRepositionTest("left-middle", "top-middle", "w125 bottom")
);
QUnit.test(
"reposition from left-middle to top-end",
getRepositionTest("left-middle", "top-end", "w125 bottom right")
);
QUnit.test(
"reposition from left-middle to left-start",
getRepositionTest("left-middle", "left-start", "top")
);
QUnit.test(
"reposition from left-middle to left-middle",
getRepositionTest("left-middle", "left-middle", "slimfit")
);
QUnit.test(
"reposition from left-middle to left-end",
getRepositionTest("left-middle", "left-end", "bottom")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from left-end to bottom-start",
getRepositionTest("left-end", "bottom-start", "w125 left")
);
QUnit.test(
"reposition from left-end to bottom-middle",
getRepositionTest("left-end", "bottom-middle", "w125")
);
QUnit.test(
"reposition from left-end to bottom-end",
getRepositionTest("left-end", "bottom-end", "w125 right")
);
QUnit.test(
"reposition from left-end to right-start",
getRepositionTest("left-end", "right-start", "left top")
);
QUnit.test(
"reposition from left-end to right-middle",
getRepositionTest("left-end", "right-middle", "left h125")
);
QUnit.test(
"reposition from left-end to right-end",
getRepositionTest("left-end", "right-end", "left")
);
QUnit.test(
"reposition from left-end to top-start",
getRepositionTest("left-end", "top-start", "w125 bottom left")
);
QUnit.test(
"reposition from left-end to top-middle",
getRepositionTest("left-end", "top-middle", "w125 bottom")
);
QUnit.test(
"reposition from left-end to top-end",
getRepositionTest("left-end", "top-end", "w125 bottom right")
);
QUnit.test(
"reposition from left-end to left-start",
getRepositionTest("left-end", "left-start", "top")
);
QUnit.test(
"reposition from left-end to left-middle",
getRepositionTest("left-end", "left-middle", "h125")
);
QUnit.test(
"reposition from left-end to left-end",
getRepositionTest("left-end", "left-end", "slimfit")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from bottom-start to bottom-start",
getRepositionTest("bottom-start", "bottom-start", "slimfit")
);
QUnit.test(
"reposition from bottom-start to bottom-middle",
getRepositionTest("bottom-start", "bottom-middle", "w125")
);
QUnit.test(
"reposition from bottom-start to bottom-end",
getRepositionTest("bottom-start", "bottom-end", "right")
);
QUnit.test(
"reposition from bottom-start to right-start",
getRepositionTest("bottom-start", "right-start", "h125 top")
);
QUnit.test(
"reposition from bottom-start to right-middle",
getRepositionTest("bottom-start", "right-middle", "h125")
);
QUnit.test(
"reposition from bottom-start to right-end",
getRepositionTest("bottom-start", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from bottom-start to top-start",
getRepositionTest("bottom-start", "top-start", "bottom")
);
QUnit.test(
"reposition from bottom-start to top-middle",
getRepositionTest("bottom-start", "top-middle", "bottom w125")
);
QUnit.test(
"reposition from bottom-start to top-end",
getRepositionTest("bottom-start", "top-end", "bottom right")
);
QUnit.test(
"reposition from bottom-start to left-start",
getRepositionTest("bottom-start", "left-start", "h125 right top")
);
QUnit.test(
"reposition from bottom-start to left-middle",
getRepositionTest("bottom-start", "left-middle", "h125 right")
);
QUnit.test(
"reposition from bottom-start to left-end",
getRepositionTest("bottom-start", "left-end", "h125 right bottom")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from bottom-middle to bottom-start",
getRepositionTest("bottom-middle", "bottom-start", "left")
);
QUnit.test(
"reposition from bottom-middle to bottom-middle",
getRepositionTest("bottom-middle", "bottom-middle", "slimfit")
);
QUnit.test(
"reposition from bottom-middle to bottom-end",
getRepositionTest("bottom-middle", "bottom-end", "right")
);
QUnit.test(
"reposition from bottom-middle to right-start",
getRepositionTest("bottom-middle", "right-start", "h125 top")
);
QUnit.test(
"reposition from bottom-middle to right-middle",
getRepositionTest("bottom-middle", "right-middle", "h125")
);
QUnit.test(
"reposition from bottom-middle to right-end",
getRepositionTest("bottom-middle", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from bottom-middle to top-start",
getRepositionTest("bottom-middle", "top-start", "bottom left")
);
QUnit.test(
"reposition from bottom-middle to top-middle",
getRepositionTest("bottom-middle", "top-middle", "bottom")
);
QUnit.test(
"reposition from bottom-middle to top-end",
getRepositionTest("bottom-middle", "top-end", "bottom right")
);
QUnit.test(
"reposition from bottom-middle to left-start",
getRepositionTest("bottom-middle", "left-start", "h125 right top")
);
QUnit.test(
"reposition from bottom-middle to left-middle",
getRepositionTest("bottom-middle", "left-middle", "h125 right")
);
QUnit.test(
"reposition from bottom-middle to left-end",
getRepositionTest("bottom-middle", "left-end", "h125 right bottom")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from bottom-end to bottom-start",
getRepositionTest("bottom-end", "bottom-start", "left")
);
QUnit.test(
"reposition from bottom-end to bottom-middle",
getRepositionTest("bottom-end", "bottom-middle", "w125")
);
QUnit.test(
"reposition from bottom-end to bottom-end",
getRepositionTest("bottom-end", "bottom-end", "slimfit")
);
QUnit.test(
"reposition from bottom-end to right-start",
getRepositionTest("bottom-end", "right-start", "h125 top")
);
QUnit.test(
"reposition from bottom-end to right-middle",
getRepositionTest("bottom-end", "right-middle", "h125")
);
QUnit.test(
"reposition from bottom-end to right-end",
getRepositionTest("bottom-end", "right-end", "h125 bottom")
);
QUnit.test(
"reposition from bottom-end to top-start",
getRepositionTest("bottom-end", "top-start", "bottom left")
);
QUnit.test(
"reposition from bottom-end to top-middle",
getRepositionTest("bottom-end", "top-middle", "bottom w125")
);
QUnit.test(
"reposition from bottom-end to top-end",
getRepositionTest("bottom-end", "top-end", "bottom")
);
QUnit.test(
"reposition from bottom-end to left-start",
getRepositionTest("bottom-end", "left-start", "h125 right top")
);
QUnit.test(
"reposition from bottom-end to left-middle",
getRepositionTest("bottom-end", "left-middle", "h125 right")
);
QUnit.test(
"reposition from bottom-end to left-end",
getRepositionTest("bottom-end", "left-end", "h125 right bottom")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from right-start to bottom-start",
getRepositionTest("right-start", "bottom-start", "w125 top left")
);
QUnit.test(
"reposition from right-start to bottom-middle",
getRepositionTest("right-start", "bottom-middle", "w125 top")
);
QUnit.test(
"reposition from right-start to bottom-end",
getRepositionTest("right-start", "bottom-end", "w125 top right")
);
QUnit.test(
"reposition from right-start to right-start",
getRepositionTest("right-start", "right-start", "slimfit")
);
QUnit.test(
"reposition from right-start to right-middle",
getRepositionTest("right-start", "right-middle", "h125")
);
QUnit.test(
"reposition from right-start to right-end",
getRepositionTest("right-start", "right-end", "bottom")
);
QUnit.test(
"reposition from right-start to top-start",
getRepositionTest("right-start", "top-start", "w125 left")
);
QUnit.test(
"reposition from right-start to top-middle",
getRepositionTest("right-start", "top-middle", "w125")
);
QUnit.test(
"reposition from right-start to top-end",
getRepositionTest("right-start", "top-end", "w125 right")
);
QUnit.test(
"reposition from right-start to left-start",
getRepositionTest("right-start", "left-start", "right")
);
QUnit.test(
"reposition from right-start to left-middle",
getRepositionTest("right-start", "left-middle", "right h125")
);
QUnit.test(
"reposition from right-start to left-end",
getRepositionTest("right-start", "left-end", "right bottom")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from right-middle to bottom-start",
getRepositionTest("right-middle", "bottom-start", "w125 top left")
);
QUnit.test(
"reposition from right-middle to bottom-middle",
getRepositionTest("right-middle", "bottom-middle", "w125 top")
);
QUnit.test(
"reposition from right-middle to bottom-end",
getRepositionTest("right-middle", "bottom-end", "w125 top right")
);
QUnit.test(
"reposition from right-middle to right-start",
getRepositionTest("right-middle", "right-start", "top")
);
QUnit.test(
"reposition from right-middle to right-middle",
getRepositionTest("right-middle", "right-middle", "slimfit")
);
QUnit.test(
"reposition from right-middle to right-end",
getRepositionTest("right-middle", "right-end", "bottom")
);
QUnit.test(
"reposition from right-middle to left-start",
getRepositionTest("right-middle", "left-start", "right top")
);
QUnit.test(
"reposition from right-middle to left-middle",
getRepositionTest("right-middle", "left-middle", "right")
);
QUnit.test(
"reposition from right-middle to left-end",
getRepositionTest("right-middle", "left-end", "right bottom")
);
QUnit.test(
"reposition from right-middle to top-start",
getRepositionTest("right-middle", "top-start", "w125 left")
);
QUnit.test(
"reposition from right-middle to top-middle",
getRepositionTest("right-middle", "top-middle", "w125")
);
QUnit.test(
"reposition from right-middle to top-end",
getRepositionTest("right-middle", "top-end", "w125 right")
);
// -----------------------------------------------------------------------------
QUnit.test(
"reposition from right-end to bottom-start",
getRepositionTest("right-end", "bottom-start", "w125 top left")
);
QUnit.test(
"reposition from right-end to bottom-middle",
getRepositionTest("right-end", "bottom-middle", "w125 top")
);
QUnit.test(
"reposition from right-end to bottom-end",
getRepositionTest("right-end", "bottom-end", "w125 top right")
);
QUnit.test(
"reposition from right-end to right-start",
getRepositionTest("right-end", "right-start", "top")
);
QUnit.test(
"reposition from right-end to right-middle",
getRepositionTest("right-end", "right-middle", "h125")
);
QUnit.test(
"reposition from right-end to right-end",
getRepositionTest("right-end", "right-end", "slimfit")
);
QUnit.test(
"reposition from right-end to left-start",
getRepositionTest("right-end", "left-start", "right top")
);
QUnit.test(
"reposition from right-end to left-middle",
getRepositionTest("right-end", "left-middle", "right h125")
);
QUnit.test(
"reposition from right-end to left-end",
getRepositionTest("right-end", "left-end", "right")
);
QUnit.test(
"reposition from right-end to top-start",
getRepositionTest("right-end", "top-start", "w125 left")
);
QUnit.test(
"reposition from right-end to top-middle",
getRepositionTest("right-end", "top-middle", "w125")
);
QUnit.test(
"reposition from right-end to top-end",
getRepositionTest("right-end", "top-end", "w125 right")
);
+3
View File
@@ -46,6 +46,9 @@ export function patchWithCleanup(obj, patchValue, options) {
});
}
/**
* @returns {HTMLElement}
*/
export function getFixture() {
if (QUnit.config.debug) {
return document.body;