Files
forgejo/web_src/css/modules/dropdown.css
0ko f0b4e3b943 feat(ui): JS-less dropdowns in navbar (#10025)
Replaced dropdowns in the navbar with JS-less ones from https://codeberg.org/forgejo/forgejo/pulls/7906.

Also made some changes to the dropdown component:
* fixed variable name
* painted backgrounds (hover, focus) are now consistently applied to the actual interactive items (`<a>`, `<button>`), not to `<li>`. This is consistent with how backgrounds are conditionally applied to pre-selected (`.active`) items and is better, as it allows to place additional things to `<li>`...
* ...`<hr>` can now be placed in some `<li>` instead of requiring splitting into multiple `<ul>`. This is simpler in code and I am guessing this should be better for a11y as screen readers can cast one continuous list instead of multiple ones. But have no hard proof that this is actually better. My main motivation was to avoid ugly mistake-prone tmpl logic where unconditional `<ul>` was getting closed and reopened inside of a condition.

I should note that on mobile all items, including these dropdowns, are hidden in another dropdown, and it stays JS-dependand for now. So this PR only makes this part of the UI JS-less for desktop.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10025
Reviewed-by: Robert Wolff <mahlzahn@posteo.de>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
2025-11-16 14:56:42 +01:00

187 lines
5.3 KiB
CSS

/* Copyright 2025 The Forgejo Authors. All rights reserved.
* SPDX-License-Identifier: GPL-3.0-or-later */
/* This is an implementation of a dropdown menu based on details HTML tag.
* It is inspired by https://picocss.com/docs/dropdown.
*
* NoJS mode could be improved by forcing the same [name] onto all dropdowns, so
* that the browser will automatically close all but the one that was just opened
* using keyboard. But the code doing that will not be as clean.
*
* Note: when implementing this dropdown, please use `dropdown` as the 1st class,
* so it is possible to search for all dropdowns with `details class="dropdown`
*/
:root details.dropdown {
--dropdown-box-shadow: 0 6px 18px var(--color-shadow);
--dropdown-item-min-height: 34px;
--dropdown-padding-inline: 0.75rem;
}
@media (pointer: coarse) {
:root details.dropdown {
--dropdown-item-min-height: 40px;
--dropdown-padding-inline: 1rem;
}
}
details.dropdown {
position: relative;
}
/* Opener */
details.dropdown > summary {
/* Optional flex+gap in case summary contains multiple elements */
display: flex;
gap: 0.25rem;
align-items: center;
justify-content: center;
/* Main visual properties */
border-radius: var(--border-radius);
padding: 0.5rem;
/* Unset unwanted default properties */
user-select: none;
list-style-type: none;
/* Display a border around opener */
&.border {
border: 1px solid var(--color-light-border);
}
/* Increase inline padding - for openers with text, like filter menus */
&.options {
padding-inline: 0.75rem;
}
}
/* NoJS mode: create a virtual fullscreen area which closes the dropdown when clicked on */
.no-js details.dropdown[open] > summary::before {
z-index: 1;
position: fixed;
width: 100vw;
height: 100vh;
inset: 0;
background: 0 0;
content: "";
cursor: default;
}
details.dropdown > summary:hover,
details.dropdown > .content > ul > li > :is(a, button):hover {
background: var(--color-hover);
}
details.dropdown[open] > summary,
details.dropdown > .content > ul > li:focus-within > :is(a, button) {
background: var(--color-active);
}
details.dropdown > .content {
z-index: 99;
position: absolute;
min-width: max-content;
border-radius: var(--border-radius);
background: var(--color-body);
box-shadow: var(--dropdown-box-shadow);
border: 1px solid var(--color-secondary);
margin-top: 0.5rem;
/* ToDo: upstream to base.css, remove from normalize.css */
hr {
height: 1px;
margin-block: 0.25rem;
background-color: var(--color-secondary);
}
}
details.dropdown > .content > ul {
/* Suppress default styling of <ul> */
margin: 0;
padding: 0;
list-style-type: none;
/* Round first item of first list and last item of last list. Each of these
* selectors should only resolve to one element in any .content */
&:first-of-type > li:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-of-type > li:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
}
/* General styling of list items */
details.dropdown > .content > ul > li {
width: 100%;
> :is(a, button) {
min-height: var(--dropdown-item-min-height);
padding-block: 0;
width: 100%;
padding-inline: var(--dropdown-padding-inline);
display: flex;
gap: 0.75rem;
align-items: center;
color: var(--color-text);
/* Interactable items should be transparent by default. <button> can also have a default background set by the browser */
background: none;
/* Same rounding should apply to both <li> and it's items with paintable backgrounds */
border-radius: inherit;
/* Suppress underline - hover is indicated by background color */
text-decoration: none;
/* Items that are pre-selected in template or by JS */
&.active {
background: var(--color-active);
font-weight: var(--font-weight-medium);
}
}
}
/* Special styling for "headers" */
/* A few dropdowns contain such headers, however, they are not semantically considered as headers */
details.dropdown > .content > .header {
display: flex;
padding-block: 0.5rem;
padding-inline: var(--dropdown-padding-inline);
font-weight: var(--font-weight-medium);
text-transform: uppercase;
font-size: 0.8rem;
}
/* Decrease bottom padding if an <hr> follows, which adds it's own vertical padding */
details.dropdown > .content > .header:has(+ hr) {
padding-bottom: 0.25rem;
}
/* dir-auto option - switch the direction at a width point where most of layout changes occur */
@media (max-width: 767.98px) {
details.dropdown.dir-auto > .content {
inset-inline: 0 auto;
direction: rtl;
> :is(span, ul) {
direction: ltr;
}
}
}
/* dir-rtl option - force right-to-left box direction */
details.dropdown.dir-rtl > .content {
inset-inline: 0 auto;
direction: rtl;
> :is(span, ul) {
direction: ltr;
}
}
/* Note: CSS anchor positioning will be a huge help in content positioning w/o JS
* - https://css-tricks.com/css-anchor-positioning-guide/
* - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning/
* - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning/Using
* It can already be implemented if the implementation won't interfere with the
* normal behavior on unsupported browsers. Or it can wait until Firefox gets
* starts supporting it. FF145 got this feature behind a feature flag. */