From 184749bf6da89fd5b5c2a50085374520c32ad79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Sat, 20 Jun 2026 23:47:46 +0200 Subject: [PATCH] fix(dropdown): flip-up menu must override top-[105%] class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- e2e/test_dropdown_clipping_e2e.py | 53 +++++++++++++++++++++++++++++++ ts/elements/dropdown.ts | 7 ++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/e2e/test_dropdown_clipping_e2e.py b/e2e/test_dropdown_clipping_e2e.py index 1b16108..1ee31a0 100644 --- a/e2e/test_dropdown_clipping_e2e.py +++ b/e2e/test_dropdown_clipping_e2e.py @@ -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 diff --git a/ts/elements/dropdown.ts b/ts/elements/dropdown.ts index 9c066fd..5ddefcd 100644 --- a/ts/elements/dropdown.ts +++ b/ts/elements/dropdown.ts @@ -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`; } };