Skip to content

Commit bbcbab6

Browse files
committed
[FIX] web: prevent dropdown overflow
With this commit, the dropdown menu tries to stay in the viewport instead of overflowing. The position compute fn still tries to get the best position but it applies a maximum height if the popper overflows. closes odoo#217900 Signed-off-by: Mathieu Duckerts-Antoine (dam) <[email protected]>
1 parent 37a63ef commit bbcbab6

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

addons/web/static/src/core/popover/popover.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export class Popover extends Component {
150150
this.props.onPositioned?.(el, solution);
151151
},
152152
position: this.props.position,
153+
shrink: true,
153154
};
154155
}
155156

addons/web/static/src/core/position/utils.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { localization } from "@web/core/l10n/localization";
1313
* @typedef {{
1414
* top: number,
1515
* left: number,
16+
* maxHeight?: number;
1617
* direction: Direction,
1718
* variant: Variant,
1819
* variantOffset?: number,
@@ -26,6 +27,8 @@ import { localization } from "@web/core/l10n/localization";
2627
* position of the popper relative to the target
2728
* @property {boolean} [flip=true]
2829
* allow the popper to try a flipped direction when it overflows the container
30+
* @property {boolean} [shrink=false]
31+
* reduce the popper's height when it overflows the container
2932
*/
3033

3134
/** @type {ComputePositionOptions} */
@@ -96,7 +99,7 @@ export function reverseForRTL(direction, variant = "middle") {
9699
* the containing block of the popper.
97100
* => can be applied to popper.style.(top|left)
98101
*/
99-
function computePosition(popper, target, { container, flip, margin, position }) {
102+
function computePosition(popper, target, { container, flip, margin, position, shrink }) {
100103
// Retrieve directions and variants
101104
const [direction, variant = "middle"] = reverseForRTL(...position.split("-"));
102105
const directions = flip ? DIRECTION_FLIP_ORDER[direction] : [direction.at(0)];
@@ -210,6 +213,18 @@ function computePosition(popper, target, { container, flip, margin, position })
210213
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
211214
result.top = positioning.top - popBox.top;
212215
result.left = positioning.left - popBox.left;
216+
if (shrink && malus && v !== "f") {
217+
const minTop = Math.floor(!vertical && v === "s" ? targetBox.top : contBox.top);
218+
result.top = Math.max(minTop, result.top);
219+
const height = {
220+
vt: targetBox.top - directionMin,
221+
vb: directionMax - targetBox.bottom,
222+
hs: variantMax - targetBox.top,
223+
hm: variantMax - variantMin,
224+
he: targetBox.bottom - variantMin,
225+
}[variantPrefix + (vertical ? d : v)];
226+
result.maxHeight = Math.floor(height);
227+
}
213228
return { result, malus };
214229
}
215230

@@ -264,9 +279,12 @@ export function reposition(popper, target, options) {
264279
const solution = computePosition(popper, target, { ...DEFAULTS, ...options });
265280

266281
// Apply it
267-
const { top, left, direction, variant } = solution;
282+
const { top, left, maxHeight, direction, variant } = solution;
268283
popper.style.top = `${top}px`;
269284
popper.style.left = `${left}px`;
285+
if (maxHeight) {
286+
popper.style.maxHeight = `${maxHeight}px`;
287+
}
270288
if (variant === "fit") {
271289
const styleProperty = ["top", "bottom"].includes(direction) ? "width" : "height";
272290
popper.style[styleProperty] = target.getBoundingClientRect()[styleProperty] + "px";

addons/web/static/tests/core/position/position_hook.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,62 @@ test("not positioned if target not connected", async () => {
705705
expect.verifySteps([]);
706706
});
707707

708+
function shrinkPopperTest(position, offset, onPositioned) {
709+
return async () => {
710+
class TestComp extends Component {
711+
static template = xml`
712+
<div id="container" t-ref="container" style="background-color: salmon; display: flex; align-items: center; justify-content: center; width: 450px; height: 450px; margin: 25px;">
713+
<div id="target" t-ref="target" style="background-color: royalblue; width: 50px; height: 50px; margin-top: ${offset}px;"/>
714+
<div id="popper" t-ref="popper" style="background-color: maroon; width: 100px; overflow: auto;">
715+
<div id="popper-content" style="background-color: seagreen; height: 500px; width: 50px;"/>
716+
</div>
717+
</div>
718+
`;
719+
static props = ["*"];
720+
setup() {
721+
const target = useRef("target");
722+
const container = useRef("container");
723+
usePosition("popper", () => target.el, {
724+
position,
725+
container: () => container.el,
726+
onPositioned(el) {
727+
expect.step("onPositioned");
728+
onPositioned({
729+
c: container.el.getBoundingClientRect(),
730+
p: el.getBoundingClientRect(),
731+
t: target.el.getBoundingClientRect(),
732+
});
733+
},
734+
shrink: true,
735+
});
736+
}
737+
}
738+
await mountWithCleanup(TestComp);
739+
expect.verifySteps(["onPositioned"]);
740+
};
741+
}
742+
743+
test("max height to prevent container overflow - top", shrinkPopperTest("top", 10, ({ c, p, t }) => {
744+
expect(p.top).toBe(c.top);
745+
expect(p.bottom).toBe(t.top);
746+
}));
747+
test("max height to prevent container overflow - bottom", shrinkPopperTest("bottom", -10, ({ c, p, t }) => {
748+
expect(p.top).toBe(t.bottom);
749+
expect(p.bottom).toBe(c.bottom);
750+
}));
751+
test("max height to prevent container overflow - right-start", shrinkPopperTest("right-start", 0, ({ c, p, t }) => {
752+
expect(p.top).toBe(t.top);
753+
expect(p.bottom).toBe(c.bottom);
754+
}));
755+
test("max height to prevent container overflow - right-middle", shrinkPopperTest("right-middle", 0, ({ c, p }) => {
756+
expect(p.top).toBe(c.top);
757+
expect(p.bottom).toBe(c.bottom);
758+
}));
759+
test("max height to prevent container overflow - right-end", shrinkPopperTest("right-end", 0, ({ c, p, t }) => {
760+
expect(p.bottom).toBe(t.bottom);
761+
expect(p.top).toBe(c.top);
762+
}));
763+
708764
function getPositionTest(position, positionToCheck) {
709765
return async () => {
710766
expect.assertions(2);

0 commit comments

Comments
 (0)