From 9ca62d82c5c4aac7e317e8650facf51aaef19252 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 25 Mar 2026 10:32:55 +0100 Subject: [PATCH 01/49] [IMP] runbot_merge: replace details-based dropdown by popovers Because popovers are always in the topmost layer they workaround the overflow issue I had with dropdowns, which means it's now possible to scroll the stagings horizontally while having the statuses remain glued to their trigger. And because popovers natively handle unicity, remove the bit of JS which did that for dropdowns. Using anchor positioning is *supposed* to correctly handle moving the popover around when it's not visible but that doesn't seem to work. I assume I did something wrong, but remove the positioning JS anyway, it *should* not be needed. Also just noticed that the branch folding doesn't work on mobile, apparently `click` doesn't trigger correctly on the titles even with the "enabler" the internet tells me about (`role=button`, `cursor: pointer`, ...), added a `touchstart` handler to fix it. Fixes #1356 --- runbot_merge/static/src/js/runbot_merge.js | 71 ++---------------- runbot_merge/static/src/runbot_merge.css | 84 ++++++++++++---------- runbot_merge/tests/test_basic.py | 2 +- runbot_merge/views/templates.xml | 39 ++++++---- 4 files changed, 77 insertions(+), 119 deletions(-) diff --git a/runbot_merge/static/src/js/runbot_merge.js b/runbot_merge/static/src/js/runbot_merge.js index df9095a8d..3d9fdf1f6 100644 --- a/runbot_merge/static/src/js/runbot_merge.js +++ b/runbot_merge/static/src/js/runbot_merge.js @@ -68,16 +68,6 @@ window.addEventListener("mouseout", (e) => { } }); /* endregion */ - -/* region dropdowns */ -// If there's an open dropdown and we click outside the dropdown, close the dropdown. -window.addEventListener("click", e => { - const dropdown = document.querySelector('details[name="dropdown"][open]'); - if (dropdown && !dropdown.contains(e.target)) { - dropdown.removeAttribute('open'); - } -}); - window.addEventListener("click", e => { const toggle = e.target.closest('a.dropdown-toggle'); if (toggle) { @@ -96,61 +86,10 @@ window.addEventListener("click", e => { title.classList.toggle('fold'); } }); - -/** - * Only implement flipping up if there's no space below, not the left/right - * toggling and sliding. - * - * TODO: use popper.js instead to get more flexible behaviour? - */ -function placeDropdown(details) { - const viewportHeight = document.documentElement.clientHeight; - - const detailsRect = details.getBoundingClientRect(); - const dropDown = details.querySelector(':scope > div'); - const dropdownRect = dropDown.getBoundingClientRect() - - // Amount of clipping in each direction (negative if dropdown is fully inside the viewport) - const clippingBottom = (detailsRect.bottom + dropdownRect.height) - viewportHeight; - // fastpath - if (clippingBottom <= 0 && !dropDown.style.inset) { - return; - } - - const clippingTop = -(detailsRect.top - dropdownRect.height); - let inset; - if (clippingBottom <= 0 || clippingBottom <= clippingTop) { - inset = `${detailsRect.height}px auto auto 0`; - } else { - inset = `auto auto ${detailsRect.height}px 0`; - } - dropDown.style.inset = inset; -} - -window.addEventListener("click", e => { - const summary = e.target.closest('summary'); - const details = summary?.parentNode; - if (details && !details.hasAttribute('open') && details.getAttribute('name') === 'dropdown') { - summary.nextElementSibling.style.visibility = 'hidden'; - } -}); -window.addEventListener("toggle", e => { - if (e.newState === 'open' && e.target.matches('details[name="dropdown"]')) { - placeDropdown(e.target); - } - e.target.querySelector(':scope>div').style.visibility = ''; -}, {capture: true}); - -window.addEventListener('scroll', _ => { - const openDetails = document.querySelector('details[name="dropdown"][open]'); - if (openDetails) { - placeDropdown(openDetails); - } -}); -window.addEventListener('resize', _ => { - const openDetails = document.querySelector('details[name="dropdown"][open]'); - if (openDetails) { - placeDropdown(openDetails); +window.addEventListener('touchstart', e => { + const title = e.target.tagName !== 'A' && e.target.closest('section>section>h2'); + if (title) { + title.classList.toggle('fold'); + return false; } }); -/* endregion */ \ No newline at end of file diff --git a/runbot_merge/static/src/runbot_merge.css b/runbot_merge/static/src/runbot_merge.css index 47cfd6bef..8412992b1 100644 --- a/runbot_merge/static/src/runbot_merge.css +++ b/runbot_merge/static/src/runbot_merge.css @@ -436,13 +436,7 @@ ul.stagings { grid-template-columns: repeat(12, minmax( min(11pc + var(--pad1) + var(--pad2), 49vw - var(--gutter-x) * 0.5), 1fr)); grid-template-rows: repeat(2, auto); grid-column-gap: 1px; - /* - This is Not Awesome, but apparently you can not combine auto in one - direction and visible in the other, so you can't have the dropdown - get out of the stagings at the bottom while having the stagings - horizontally scrollable (at least not without JS scrolling). - */ - overflow-x: clip; + overflow-x: auto; &>li.staging { grid-template-rows: subgrid; @@ -472,11 +466,17 @@ ul.stagings { } } } - &>details { - margin: 0.5em 0; - &>summary { - text-indent: 1em hanging; - } + /* + :open is supposed to work on popovertarget (in supporting browser) + but it doesn't seem to work in FF while matching + `@supports selector(:has)`, so FF gets neither version therefore + just use the legacy one + */ + &>button[popovertarget]:before { + content: '▸ '; + } + &:has([popover]:popover-open) > button[popovertarget]:before { + content: "▾ "; } } } @@ -492,38 +492,48 @@ ul.stagings { } } -details[name="dropdown"] { - position: relative; - &>summary { - padding: 0 var(--pad1, 0) 0 var(--pad2, 0); +div.dropdown[popover] { + :has(&) { + anchor-scope: all; + } + button:has(+ &) { + display: inline-block; + background: none; + border: none; cursor: pointer; user-select: none; + + text-align: start; + text-indent: 0.8em hanging; + margin: 0.5em 0; + padding: 0 var(--pad1, 0) 0 var(--pad2, 0); } - &>div { - position: absolute; - z-index: 100; - max-height: 70vh; - min-width: 10rem; - overflow: auto; - color: var(--color-foreground); - background-color: var(--color-background); + position: absolute; + inset: unset; + top: anchor(var(--my-anchor) bottom); + left: anchor(var(--my-anchor) left); + position-try-fallbacks: flip-block, flip-inline; - padding: 0.5rem 0; - border: 1px solid transparent; - border-radius: 0.25rem; + max-height: 70vh; + min-width: 10rem; + overflow: auto; - /*inset: 100% 0 auto auto;*/ + color: var(--color-foreground); + background-color: var(--color-background); - & > * { - display: block; - width: 100%; - padding: 0.25rem 1rem; - white-space: nowrap; - color: color-mix(in srgb, var(--link-color) 85%, var(--color-background)); - &:hover { - color: color-mix(in srgb, var(--link-color), var(--color-foreground)); - } + padding: 0.5rem 0; + border: 1px solid transparent; + border-radius: 0.25rem; + + & > * { + display: block; + width: 100%; + padding: 0.25rem 1rem; + white-space: nowrap; + color: color-mix(in srgb, var(--link-color) 85%, var(--color-background)); + &:hover { + color: color-mix(in srgb, var(--link-color), var(--color-foreground)); } } } diff --git a/runbot_merge/tests/test_basic.py b/runbot_merge/tests/test_basic.py index 11efec797..3acd58848 100644 --- a/runbot_merge/tests/test_basic.py +++ b/runbot_merge/tests/test_basic.py @@ -158,7 +158,7 @@ def test_trivial_flow(env, repo, page, users, config, project, partners, status_ } p = html.fromstring(page('/runbot_merge')) - s = p.cssselect('.staging details[name="dropdown"] a') + s = p.cssselect('.staging div.staging-statuses a') assert len(s) == 2, "not logged so only *required* statuses" for e, status in zip(s, ['legal/cla', 'ci/runbot']): assert set(e.classes) == {'bg-success'} diff --git a/runbot_merge/views/templates.xml b/runbot_merge/views/templates.xml index e7245a658..67cab3b24 100644 --- a/runbot_merge/views/templates.xml +++ b/runbot_merge/views/templates.xml @@ -23,11 +23,17 @@