[FIX] web: fix sub-dropdown alignment

Since [1], the sub-dropdowns are misaligned as soon as the user
interacts with them.

**Before this commit**
![before]

**After this commit**
![after]

[1]: e6bdce9025fb106bc9dd7bc99c67be4df6365f22
[before]: https://github.com/odoo/odoo/assets/1159815/a293ebe1-30cc-4397-9e17-c0813846922b
[after]: https://github.com/odoo/odoo/assets/1159815/7a151e51-a4e5-45e0-a1bf-61e89ae8aabb

closes odoo/odoo#124439

X-original-commit: 2f5055861f46cce77ae8687f546aff68ff262366
Signed-off-by: Michaël Mattiello (mcm) <mcm@odoo.com>
Signed-off-by: Bruno Boi (boi) <boi@odoo.com>
This commit is contained in:
Bruno Boi
2023-06-09 13:50:04 +02:00
parent 3006be6184
commit 8d1700c3af
5 changed files with 99 additions and 48 deletions
@@ -103,16 +103,8 @@ export class Dropdown extends Component {
const positioningOptions = {
popper: "menuRef",
position,
onPositioned: (el, { direction, variant }) => {
onPositioned: (el, { direction }) => {
this.state.directionCaretClass = DIRECTION_CARET_CLASS[direction];
if (this.parentDropdown && ["right", "left"].includes(direction)) {
// Correctly align sub dropdowns items with its parent's
if (variant === "start") {
el.style.marginTop = "calc(-.5rem - 1px)";
} else if (variant === "end") {
el.style.marginTop = "calc(.5rem - 2px)";
}
}
},
};
if (this.props.container) {
@@ -20,6 +20,14 @@
cursor: pointer;
}
}
// Correctly align sub dropdowns items with its parent's
.o-dropdown > .o-dropdown--menu {
// the value comes from bootstrap's ".dropdown-menu" padding and border style
--o-dropdown--submenu-margin: calc(-.5rem - 1px);
margin-top: var(--o-dropdown--submenu-margin);
margin-bottom: var(--o-dropdown--submenu-margin);
}
}
/*!rtl:begin:ignore*/
&.dropup > .o-dropdown--menu,
+8 -8
View File
@@ -141,21 +141,21 @@ function getBestPosition(target, popper, iframe, { container, margin, position }
// Compute positioning data
/** @type {DirectionsData} */
const directionsData = {
t: iframeBox.top + targetBox.top - popMargins.bottom - popBox.height - margin,
t: iframeBox.top + targetBox.top - popMargins.bottom - margin - popBox.height,
b: iframeBox.top + targetBox.bottom + popMargins.top + margin,
r: iframeBox.left + targetBox.right + popMargins.left + margin,
l: iframeBox.left + targetBox.left - popMargins.right - popBox.width - margin,
l: iframeBox.left + targetBox.left - popMargins.right - margin - popBox.width,
};
/** @type {VariantsData} */
const variantsData = {
vf: iframeBox.left + targetBox.left,
vs: iframeBox.left + targetBox.left,
vm: iframeBox.left + targetBox.left + targetBox.width / 2 + -popBox.width / 2,
ve: iframeBox.left + targetBox.right - popBox.width,
vs: iframeBox.left + targetBox.left + popMargins.left,
vm: iframeBox.left + targetBox.left + targetBox.width / 2 - popBox.width / 2,
ve: iframeBox.left + targetBox.right - popMargins.right - popBox.width,
hf: iframeBox.top + targetBox.top,
hs: iframeBox.top + targetBox.top,
hm: iframeBox.top + targetBox.top + targetBox.height / 2 + -popBox.height / 2,
he: iframeBox.top + targetBox.bottom - popBox.height,
hs: iframeBox.top + targetBox.top + popMargins.top,
hm: iframeBox.top + targetBox.top + targetBox.height / 2 - popBox.height / 2,
he: iframeBox.top + targetBox.bottom - popMargins.bottom - popBox.height,
};
function getPositioningData(d = directions[0], v = variants[0], containerRestricted = false) {
+64 -1
View File
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { App, Component, xml } from "@odoo/owl";
import { App, Component, onMounted, onPatched, useRef, useState, xml } from "@odoo/owl";
import { templates } from "@web/core/assets";
import { browser } from "@web/core/browser/browser";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
@@ -20,6 +20,7 @@ import {
mouseEnter,
nextTick,
patchWithCleanup,
triggerEvent,
triggerHotkey,
} from "../helpers/utils";
import { makeParent } from "./tooltip/tooltip_service_tests";
@@ -989,6 +990,68 @@ QUnit.module("Components", ({ beforeEach }) => {
}
});
QUnit.test("multi-level dropdown: submenu keeps position when patched", async (assert) => {
assert.expect(9);
patchWithCleanup(Dropdown.prototype, {
setup() {
this._super(...arguments);
const isSubmenu = Boolean(this.parentDropdown);
if (isSubmenu) {
onMounted(() => {
assert.step(`submenu mounted`);
});
const menuRef = useRef("menuRef");
let previousMenuRect;
onPatched(() => {
assert.step(`submenu patched`);
if (this.state.open) {
const subMenuRect = menuRef.el.getBoundingClientRect();
if (previousMenuRect) {
assert.strictEqual(subMenuRect.top, previousMenuRect.top);
assert.strictEqual(subMenuRect.left, previousMenuRect.left);
}
previousMenuRect = subMenuRect;
}
});
}
},
});
let parentState;
class Parent extends Component {
setup() {
this.state = useState({ foo: false });
parentState = this.state;
}
}
Parent.template = /* xml */ xml`
<Dropdown class="'outer'">
<t t-set-slot="toggler">Outer</t>
<Dropdown class="'inner'">
<t t-set-slot="toggler">Inner</t>
<DropdownItem t-if="state.foo">Inner</DropdownItem>
</Dropdown>
</Dropdown>
`;
Parent.components = { Dropdown, DropdownItem };
env = await makeTestEnv();
await mount(Parent, target, { env });
assert.verifySteps([]);
// Open the menu
await click(target, ".outer .dropdown-toggle");
assert.verifySteps(["submenu mounted"]);
// Open the submenu
await triggerEvent(target, ".inner .dropdown-toggle", "mouseenter");
assert.verifySteps(["submenu patched"]);
// Change submenu content
parentState.foo = true;
await nextTick();
assert.verifySteps(["submenu patched"]);
});
QUnit.test("showCaret props adds caret class", async (assert) => {
class Parent extends Component {}
Parent.template = xml`
@@ -153,21 +153,9 @@ QUnit.test("can add margin", async (assert) => {
QUnit.test("is restricted to its container, even with margins", async (assert) => {
// Add a sheet to set a margin on the popper
const SHEET_MARGINS = {
top: 11,
right: 12,
bottom: 13,
left: 14,
};
const SHEET_MARGIN = 11;
const sheet = document.createElement("style");
sheet.textContent = `
#popper {
margin-top: ${SHEET_MARGINS.top}px;
margin-right: ${SHEET_MARGINS.right}px;
margin-bottom: ${SHEET_MARGINS.bottom}px;
margin-left: ${SHEET_MARGINS.left}px;
}
`;
sheet.textContent = `#popper { margin: ${SHEET_MARGIN}px; }`;
document.head.appendChild(sheet);
registerCleanup(() => sheet.remove());
@@ -193,21 +181,21 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
// --> Without additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.top}px` });
Object.assign(container.style, { height: `${minSize + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "bottom" });
assert.verifySteps(["bottom-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.top - 1}px` });
Object.assign(container.style, { height: `${minSize + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "bottom" });
assert.verifySteps(["right-start"]);
// --> With additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.top + margin}px` });
Object.assign(container.style, { height: `${minSize + margin + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "bottom", margin });
assert.verifySteps(["bottom-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.top + margin - 1}px` });
Object.assign(container.style, { height: `${minSize + margin + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "bottom", margin });
assert.verifySteps(["right-start"]);
@@ -217,22 +205,22 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
// --> Without additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.bottom}px` });
Object.assign(container.style, { height: `${minSize + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "top" });
assert.verifySteps(["top-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.bottom - 1}px` });
Object.assign(container.style, { height: `${minSize + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "top" });
assert.verifySteps(["right-end"]);
// --> With additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { height: `${minSize + SHEET_MARGINS.bottom + margin}px` });
Object.assign(container.style, { height: `${minSize + margin + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "top", margin });
assert.verifySteps(["top-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, {
height: `${minSize + SHEET_MARGINS.bottom + margin - 1}px`,
height: `${minSize + margin + SHEET_MARGIN - 1}px`,
});
await _mountTestComponentAndDestroy({ position: "top", margin });
assert.verifySteps(["right-end"]);
@@ -245,22 +233,22 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
// --> Without additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.right}px` });
Object.assign(container.style, { width: `${minSize + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "left" });
assert.verifySteps(["left-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.right - 1}px` });
Object.assign(container.style, { width: `${minSize + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "left" });
assert.verifySteps(["bottom-end"]);
// --> With additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.right + margin}px` });
Object.assign(container.style, { width: `${minSize + margin + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "left", margin });
assert.verifySteps(["left-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, {
width: `${minSize + SHEET_MARGINS.right + margin - 1}px`,
width: `${minSize + margin + SHEET_MARGIN - 1}px`,
});
await _mountTestComponentAndDestroy({ position: "left", margin });
assert.verifySteps(["bottom-end"]);
@@ -271,21 +259,21 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
// --> Without additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.left}px` });
Object.assign(container.style, { width: `${minSize + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "right" });
assert.verifySteps(["right-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.left - 1}px` });
Object.assign(container.style, { width: `${minSize + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "right" });
assert.verifySteps(["top-start"]);
// --> With additional margin
// Leave just enough space for the popper to be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.left + margin}px` });
Object.assign(container.style, { width: `${minSize + margin + SHEET_MARGIN}px` });
await _mountTestComponentAndDestroy({ position: "right", margin });
assert.verifySteps(["right-middle"]);
// Remove 1px => popper should switch direction as it can't be contained
Object.assign(container.style, { width: `${minSize + SHEET_MARGINS.left + margin - 1}px` });
Object.assign(container.style, { width: `${minSize + margin + SHEET_MARGIN - 1}px` });
await _mountTestComponentAndDestroy({ position: "right", margin });
assert.verifySteps(["top-start"]);
});