From dcc0eebb7da0749139092e6c8bded4d23ad05425 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Thu, 23 Apr 2026 14:25:35 +0200 Subject: [PATCH] fix(report): Choices.js dropdown inside filter popovers now visible + scrollable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systematic-debugging: Konrad reported the project/team popovers showed no options ('can not see any options') and wheel scroll fell through to the page instead of scrolling the dropdown. Root cause chain: 1. The 0bbf2ca/ffb3ef6 CSS on .filter-popover had `overflow: hidden` (to hide anything past max-height) and the body had `overflow-y: auto; flex: 1 1 auto`. 2. Choices.js renders its option list with the default `.choices__list--dropdown { position: absolute; }`. 3. Absolutely-positioned elements do NOT contribute to an ancestor's scrollHeight, so the body's overflow-y: auto never created a scroll context — wheel events bubbled to the page. 4. The dropdown extended past the popover's bottom edge and got clipped by the popover's overflow: hidden, so no options were visible. Single-point fix: - Remove `overflow: hidden` from .filter-popover (it was only there to enforce the sticky footer, which the flex layout already does). - Scoped CSS override on .choices__list--dropdown inside .filter-popover to force `position: static` — dropdown now flows inline, the body grows to contain it, and the sticky footer pushes below naturally. The dropdown gets its own `max-height: 260px; overflow-y: auto` for long option lists, which gives a clean internal scroll. Specificity gotcha: Choices.js's rule is `.choices__list--dropdown, .choices__list[aria-expanded]` — the second branch has class+attribute specificity (0,0,2,0) that TIES with a naive two-class override, and since Choices.js's stylesheet loads after ours, source order gave them the win. The fix is to mirror the selector list, lifting our specificity to (0,0,2,1) on the aria-expanded branch, which wins cleanly without `!important`. Inline comment in custom.css explains this for future reference. Scoping: the override is gated to `.filter-popover` descendants, so Choices.js widgets elsewhere in the app (worker / team / project picker on edit pages, payroll modals, etc.) keep their default absolute- positioned dropdown. Co-Authored-By: Claude Opus 4.7 (1M context) --- static/css/custom.css | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/static/css/custom.css b/static/css/custom.css index c9c0cd3..9bf287e 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1780,11 +1780,12 @@ body, .card, .modal-content, .form-control, .form-select, } /* --- Popover positioned under the pill --- */ -/* max-height + flex column keeps the sticky footer visible even when the - popover body has to scroll (Choices.js can render a long list of options). - Border + shadow beefed up 2026-04-23 so the popover visually detaches +/* Border + shadow beefed up 2026-04-23 so the popover visually detaches from the report body behind it — previous subtle shadow was getting - lost against the amber-accented report cards. */ + lost against the amber-accented report cards. + The popover uses a flex column so a sticky footer stays pinned at the + bottom even when the body scrolls. We DO NOT set overflow: hidden on + the popover itself — see the Choices.js override below for why. */ .filter-popover { position: absolute; top: calc(100% + 6px); @@ -1804,7 +1805,6 @@ body, .card, .modal-content, .form-control, .form-select, 0 18px 44px rgba(0, 0, 0, 0.55), /* deep drop shadow */ 0 6px 12px rgba(0, 0, 0, 0.35); /* near shadow for edge crispness */ padding: 0; - overflow: hidden; /* clip Choices.js dropdown so the sticky footer wins */ } /* Light theme: shadow and halo need different opacity to read against white */ :root.light .filter-popover { @@ -1837,6 +1837,38 @@ body, .card, .modal-content, .form-control, .form-select, z-index: 2; } +/* --- Choices.js dropdown override (scoped to filter popovers) --- + Choices.js renders its option list as position: absolute beneath the + input. Inside our popovers (a flex column with a max-height and a + sticky footer) that's a problem: + 1. The absolute-positioned dropdown doesn't contribute to the body's + scrollHeight, so the body's overflow-y: auto never creates a + scrollbar — and the user's wheel scroll falls through to the page. + 2. The dropdown's rendered position overlaps / sits behind the sticky + footer, so options aren't visible. + Forcing the dropdown to position: static lets it flow inline: the body + grows to contain it, the sticky footer pushes below, and for long + option lists the dropdown's own max-height + overflow-y gives a clean + internal scroll. + + Specificity note: we mirror Choices.js's selector list + (`.choices__list--dropdown, .choices__list[aria-expanded]`) because + the second branch carries a class+attribute specificity (0,0,2,0) + that ties with a naive two-class override. Source order then decides + the winner and Choices.js's stylesheet loads AFTER ours. Mirroring + the selector lifts our specificity one step on the aria-expanded + branch and wins cleanly without needing !important. + + Scoped to .filter-popover so other Choices.js usages in the app + (worker/team pickers on edit pages, etc.) keep their default behaviour. */ +.filter-popover .choices__list--dropdown, +.filter-popover .choices__list[aria-expanded] { + position: static; + margin-top: 0.35rem; + max-height: 260px; + overflow-y: auto; +} + /* --- Mobile: popovers stretch full-width below the pill strip --- */ @media (max-width: 576px) { .filter-popover {