fix(dropdown): flip-up menu must override top-[105%] class

The flip-up branch cleared inline `top` to "", which let the menu's
`top-[105%]` utility class reassert top:105% on the now-fixed element —
collapsing the menu to a 2px sliver below the viewport, so toggles near the
viewport bottom appeared not to open. Set the unused anchor to "auto" so the
inline value wins over the class. Add an e2e regression for the flip-up path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 23:47:46 +02:00
parent 3bd14e8c89
commit 184749bf6d
2 changed files with 58 additions and 2 deletions
+53
View File
@@ -68,3 +68,56 @@ def test_device_dropdown_not_clipped_on_short_table(
page.wait_for_timeout(200)
session.refresh_from_db()
assert session.device == devices[14]
def test_device_dropdown_flips_up_near_viewport_bottom(
authenticated_page: Page, live_server
):
"""A dropdown whose toggle sits near the viewport bottom must open upward
and stay fully visible — not collapse off-screen.
Regression: the menu keeps a ``top-[105%]`` utility class; clearing inline
``top`` to "" in the flip-up branch let that class reassert ``top: 105%``
on the now-``fixed`` menu, collapsing it to a 2px sliver below the viewport.
"""
page = authenticated_page
page.set_viewport_size({"width": 1280, "height": 760})
platform = Platform.objects.create(name="PC", icon="pc", group="PC")
game = Game.objects.create(name="Tunic")
game.platform = platform
game.save()
devices = [Device.objects.create(name=f"Device {i:02d}") for i in range(15)]
sessions = [
Session.objects.create(
game=game, device=devices[0], timestamp_start=timezone.now()
)
for _ in range(10)
]
page.goto(f"{live_server.url}{reverse('games:list_sessions')}")
# Scroll the table so the lower rows sit near the viewport bottom, where the
# menu cannot fit below and must flip up.
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
page.wait_for_timeout(200)
bottom_row = sessions[-3]
page.locator(f"#session-row-{bottom_row.pk} [data-toggle]").click()
menu = page.locator("[data-menu]:not([hidden])")
menu.wait_for(state="visible")
geometry = page.evaluate(
"""() => {
const menu = document.querySelector('[data-menu]:not([hidden])');
const rect = menu.getBoundingClientRect();
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height,
viewportHeight: window.innerHeight,
};
}"""
)
# The flipped-up menu is a real, fully on-screen box (not a 2px sliver).
assert geometry["height"] > 50, geometry
assert geometry["top"] >= -1, geometry
assert geometry["bottom"] <= geometry["viewportHeight"] + 1, geometry
+5 -2
View File
@@ -33,11 +33,14 @@ export function initDropdown(host: HTMLElement, config: DropdownConfig): void {
menu.style.width = `${rect.width}px`;
menu.style.maxHeight = `${Math.max(0, openUp ? spaceAbove : spaceBelow)}px`;
menu.style.overflowY = "auto";
// Set the unused anchor to "auto" (not "") so this inline value overrides
// the menu's `top-[105%]` utility class; clearing it to "" would let the
// class reassert top:105% and collapse the fixed menu off-screen.
if (openUp) {
menu.style.top = "";
menu.style.top = "auto";
menu.style.bottom = `${window.innerHeight - rect.top}px`;
} else {
menu.style.bottom = "";
menu.style.bottom = "auto";
menu.style.top = `${rect.bottom}px`;
}
};