mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 06:20:24 +00:00
feat(ui): allow dropdown to contain not just items (#9951)
Currently the dropdown component only supports having one `<summary>` and one `<ul>` (with interactive items) in it. This PR refactors it to add a `.content` container so that it is possible for the dropdown to contain things the more complex dropdowns do like `<hr>` and a searchbar. Also adds an `<hr>` to user actions as a little demo. Preview B: https://codeberg.org/attachments/8dfb98d2-52be-4c3c-8fc0-8fe470f34703 A: https://codeberg.org/attachments/53f2acfb-2e61-4420-b616-13d563f5c257 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9951 Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
parent
07d6663748
commit
382c3c3228
8 changed files with 235 additions and 114 deletions
|
|
@ -13,20 +13,22 @@
|
|||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "newest"}}active{{end}}" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "oldest"}}active{{end}}" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "alphabetically"}}active{{end}}" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "reversealphabetically"}}active{{end}}" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "newest"}}active{{end}}" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "oldest"}}active{{end}}" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "alphabetically"}}active{{end}}" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="{{if eq .SortType "reversealphabetically"}}active{{end}}" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,21 +22,23 @@
|
|||
<summary class="border" data-tooltip-content="{{ctx.Locale.Tr "profile.actions.tooltip"}}">
|
||||
{{svg "octicon-kebab-horizontal" 20}}
|
||||
</summary>
|
||||
<ul>
|
||||
{{if .EnableFeed}}
|
||||
<li>
|
||||
<a href="{{.Org.HomeLink}}.rss">{{svg "octicon-rss"}}{{ctx.Locale.Tr "rss_feed"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.Org.HomeLink}}.atom">{{svg "octicon-rss"}}{{ctx.Locale.Tr "feed.atom.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if $moderationEntryNeeded}}
|
||||
<li>
|
||||
<a class="orange text" href="{{AppSubUrl}}/report_abuse?type=org&id={{$.Org.ID}}">{{svg "octicon-stop"}}{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<div class="content">
|
||||
<ul>
|
||||
{{if .EnableFeed}}
|
||||
<li>
|
||||
<a href="{{.Org.HomeLink}}.rss">{{svg "octicon-rss"}}{{ctx.Locale.Tr "rss_feed"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.Org.HomeLink}}.atom">{{svg "octicon-rss"}}{{ctx.Locale.Tr "feed.atom.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if $moderationEntryNeeded}}
|
||||
<li>
|
||||
<a class="orange text" href="{{AppSubUrl}}/report_abuse?type=org&id={{$.Org.ID}}">{{svg "octicon-stop"}}{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -2,48 +2,53 @@
|
|||
<summary class="border" data-tooltip-content="{{ctx.Locale.Tr "profile.actions.tooltip"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
</summary>
|
||||
<ul>
|
||||
{{if eq .SignedUserID .ContextUser.ID}}
|
||||
<div class="content">
|
||||
<ul>
|
||||
{{if eq .SignedUserID .ContextUser.ID}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/user/settings">{{svg "octicon-pencil"}}{{ctx.Locale.Tr "profile.edit.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if .IsAdmin}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/admin/users/{{.ContextUser.ID}}">{{svg "octicon-gear"}}{{ctx.Locale.Tr "admin.users.details"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if and .EnableFeed (or .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate))}}
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.rss">{{svg "octicon-rss"}}{{ctx.Locale.Tr "rss_feed"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.atom">{{svg "octicon-rss"}}{{ctx.Locale.Tr "feed.atom.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/user/settings">{{svg "octicon-pencil"}}{{ctx.Locale.Tr "profile.edit.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if .IsAdmin}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/admin/users/{{.ContextUser.ID}}">{{svg "octicon-gear"}}{{ctx.Locale.Tr "admin.users.details"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if and .EnableFeed (or .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate))}}
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.rss">{{svg "octicon-rss"}}{{ctx.Locale.Tr "rss_feed"}}</a>
|
||||
<a href="{{.ContextUser.HomeLink}}.keys">{{svg "octicon-key"}}{{ctx.Locale.Tr "keys.ssh.link"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.atom">{{svg "octicon-rss"}}{{ctx.Locale.Tr "feed.atom.link"}}</a>
|
||||
<a href="{{.ContextUser.HomeLink}}.gpg">{{svg "octicon-key"}}{{ctx.Locale.Tr "keys.gpg.link"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.keys">{{svg "octicon-key"}}{{ctx.Locale.Tr "keys.ssh.link"}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.ContextUser.HomeLink}}.gpg">{{svg "octicon-key"}}{{ctx.Locale.Tr "keys.gpg.link"}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}}
|
||||
<li hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" id="action-block">
|
||||
{{if .IsBlocked}}
|
||||
<button class="orange text" hx-post="{{.ContextUser.HomeLink}}?action=unblock">
|
||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unblock"}}
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="orange text" data-modal-id="block-user" hx-post="{{.ContextUser.HomeLink}}?action=block" hx-confirm="-">
|
||||
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block"}}
|
||||
</button>
|
||||
<hr>
|
||||
<ul>
|
||||
<li hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" id="action-block">
|
||||
{{if .IsBlocked}}
|
||||
<button class="orange text" hx-post="{{.ContextUser.HomeLink}}?action=unblock">
|
||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unblock"}}
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="orange text" data-modal-id="block-user" hx-post="{{.ContextUser.HomeLink}}?action=block" hx-confirm="-">
|
||||
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</li>
|
||||
{{if .IsModerationEnabled}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/report_abuse?type=user&id={{.ContextUser.ID}}" class="orange text">{{svg "octicon-stop"}}{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
{{if and .IsModerationEnabled .IsSigned (ne .SignedUserID .ContextUser.ID)}}
|
||||
<li>
|
||||
<a href="{{AppSubUrl}}/report_abuse?type=user&id={{.ContextUser.ID}}" class="orange text">{{svg "octicon-stop"}}{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// @watch start
|
||||
// templates/shared/user/**
|
||||
// templates/shared/user/actions_menu.tmpl
|
||||
// templates/org/header.tmpl
|
||||
// templates/explore/search.tmpl
|
||||
// web_src/js/modules/dropdown.ts
|
||||
// @watch end
|
||||
|
||||
|
|
@ -17,8 +19,8 @@ test('JS enhanced interaction', async ({page}) => {
|
|||
await expect(nojsNotice).toBeHidden();
|
||||
|
||||
// Open and close by clicking summary
|
||||
const dropdownSummary = page.locator('details.dropdown summary');
|
||||
const dropdownContent = page.locator('details.dropdown ul');
|
||||
const dropdownSummary = page.locator('details.dropdown > summary');
|
||||
const dropdownContent = page.locator('details.dropdown > .content');
|
||||
await expect(dropdownContent).toBeHidden();
|
||||
await dropdownSummary.click();
|
||||
await expect(dropdownContent).toBeVisible();
|
||||
|
|
@ -50,7 +52,7 @@ test('JS enhanced interaction', async ({page}) => {
|
|||
await dropdownSummary.press(`Escape`);
|
||||
await expect(dropdownContent).toBeHidden();
|
||||
|
||||
// Open and close by opening a different dropdown
|
||||
// Open and then close by opening a different dropdown
|
||||
const languageMenu = page.locator('.language-menu');
|
||||
await dropdownSummary.click();
|
||||
await expect(dropdownContent).toBeVisible();
|
||||
|
|
@ -70,8 +72,8 @@ test('No JS interaction', async ({browser}) => {
|
|||
await expect(nojsPage.locator('body')).toContainClass('no-js');
|
||||
|
||||
// Open and close by clicking summary
|
||||
const dropdownSummary = nojsPage.locator('details.dropdown summary');
|
||||
const dropdownContent = nojsPage.locator('details.dropdown ul');
|
||||
const dropdownSummary = nojsPage.locator('details.dropdown > summary');
|
||||
const dropdownContent = nojsPage.locator('details.dropdown > .content');
|
||||
await expect(dropdownContent).toBeHidden();
|
||||
await dropdownSummary.click();
|
||||
await expect(dropdownContent).toBeVisible();
|
||||
|
|
@ -113,7 +115,7 @@ test('Visual properties', async ({browser, isMobile}) => {
|
|||
await page.goto('/user1');
|
||||
|
||||
// Has `.border` and pretty small default `inline-padding:`
|
||||
const summary = page.locator('details.dropdown summary');
|
||||
const summary = page.locator('details.dropdown > summary');
|
||||
expect(await summary.evaluate((el) => getComputedStyle(el).border)).toBe('1px solid rgba(0, 0, 0, 0.114)');
|
||||
expect(await summary.evaluate((el) => getComputedStyle(el).paddingInline)).toBe('7px');
|
||||
|
||||
|
|
@ -139,8 +141,8 @@ test('Visual properties', async ({browser, isMobile}) => {
|
|||
}
|
||||
|
||||
// Direction and item height
|
||||
const content = page.locator('details.dropdown > ul');
|
||||
const itemsSel = 'details.dropdown > ul > li';
|
||||
const content = page.locator('details.dropdown > .content');
|
||||
const itemsSel = 'details.dropdown > .content > ul > li';
|
||||
if (isMobile) {
|
||||
// `<ul>`'s direction is reversed
|
||||
expect(await content.evaluate((el) => getComputedStyle(el).direction)).toBe('rtl');
|
||||
|
|
@ -166,8 +168,8 @@ test('Visual properties', async ({browser, isMobile}) => {
|
|||
await evaluateDropdownItems(page, itemsSel, 'ltr', isMobile ? '40px' : '34px');
|
||||
|
||||
// Background of inactive and `.active` items
|
||||
const activeItem = page.locator('details.dropdown > ul > li:first-child > a');
|
||||
const inactiveItem = page.locator('details.dropdown > ul > li:last-child > a');
|
||||
const activeItem = page.locator('details.dropdown > .content > ul > li:first-child > a');
|
||||
const inactiveItem = page.locator('details.dropdown > .content > ul > li:last-child > a');
|
||||
expect(await activeItem.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgb(226, 226, 229)');
|
||||
expect(await inactiveItem.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgba(0, 0, 0, 0)');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func TestExploreOrg(t *testing.T) {
|
|||
req := NewRequest(t, "GET", "/explore/organizations?sort="+c.sortOrder)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
h := NewHTMLParser(t, resp.Body)
|
||||
href, _ := h.Find(`.list-header details.dropdown > ul > li > a.active[href^="?sort="]`).Attr("href")
|
||||
href, _ := h.Find(`.list-header details.dropdown > .content > ul > li > a.active[href^="?sort="]`).Attr("href")
|
||||
assert.Equal(t, c.expected, href)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func TestExploreUser(t *testing.T) {
|
|||
req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
h := NewHTMLParser(t, resp.Body)
|
||||
href, _ := h.Find(`.list-header details.dropdown > ul > li > a.active[href^="?sort="]`).Attr("href")
|
||||
href, _ := h.Find(`.list-header details.dropdown > .content > ul > li > a.active[href^="?sort="]`).Attr("href")
|
||||
assert.Equal(t, c.expected, href)
|
||||
}
|
||||
|
||||
|
|
|
|||
84
tests/integration/user_profile_actions_test.go
Normal file
84
tests/integration/user_profile_actions_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/tests"
|
||||
)
|
||||
|
||||
func TestUserProfileActions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
admSel := `details.dropdown a[href^="/admin/users/"]`
|
||||
blockSel := `details.dropdown button[hx-post$="?action=block"]`
|
||||
reportSel := `details.dropdown a[href^="/report_abuse?type=user"]`
|
||||
|
||||
t.Run("Guest user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Moderation.Enabled, true)()
|
||||
|
||||
// Can't do much
|
||||
page := NewHTMLParser(t, MakeRequest(t, NewRequest(t, "GET", "/user1"), http.StatusOK).Body)
|
||||
page.AssertElement(t, admSel, false)
|
||||
page.AssertElement(t, blockSel, false)
|
||||
page.AssertElement(t, reportSel, false)
|
||||
})
|
||||
|
||||
t.Run("User blocking", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Moderation.Enabled, true)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// Can block others
|
||||
page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user1"), http.StatusOK).Body)
|
||||
page.AssertElement(t, blockSel, true)
|
||||
|
||||
// Can't block self
|
||||
page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user2"), http.StatusOK).Body)
|
||||
page.AssertElement(t, blockSel, false)
|
||||
})
|
||||
|
||||
// To decrease the amount of requests, admin and moderation assertions are squashed together
|
||||
|
||||
t.Run("Moderation enabled, user is admin", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Moderation.Enabled, true)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
// The /admin/... link advertized to admins on all profiles
|
||||
|
||||
// Can report others
|
||||
page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user2"), http.StatusOK).Body)
|
||||
page.AssertElement(t, reportSel, true)
|
||||
page.AssertElement(t, admSel, true)
|
||||
|
||||
// Can't report self
|
||||
page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user1"), http.StatusOK).Body)
|
||||
page.AssertElement(t, reportSel, false)
|
||||
page.AssertElement(t, admSel, true)
|
||||
})
|
||||
|
||||
t.Run("Moderation disabled, user isn't admin", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Moderation.Enabled, false)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
// The /admin/... link is not advertized to non-admins
|
||||
|
||||
// Can report anyone
|
||||
page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user1"), http.StatusOK).Body)
|
||||
page.AssertElement(t, reportSel, false)
|
||||
page.AssertElement(t, admSel, false)
|
||||
|
||||
page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user2"), http.StatusOK).Body)
|
||||
page.AssertElement(t, reportSel, false)
|
||||
page.AssertElement(t, admSel, false)
|
||||
})
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@
|
|||
* 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 {
|
||||
|
|
@ -26,6 +29,7 @@ details.dropdown {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* Opener */
|
||||
details.dropdown > summary {
|
||||
/* Optional flex+gap in case summary contains multiple elements */
|
||||
display: flex;
|
||||
|
|
@ -39,25 +43,17 @@ details.dropdown > summary {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
details.dropdown > summary:hover,
|
||||
details.dropdown > summary + ul > li:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
details.dropdown[open] > summary,
|
||||
details.dropdown > summary + ul > li:focus-within {
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
/* NoJS mode. Creates a virtual fullscreen area. Clicking it closes the dropdown. */
|
||||
/* 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;
|
||||
|
|
@ -69,23 +65,52 @@ details.dropdown > summary + ul > li:focus-within {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul {
|
||||
details.dropdown > summary:hover,
|
||||
details.dropdown > .content > ul > li:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
details.dropdown[open] > summary,
|
||||
details.dropdown > .content > ul > li:focus-within {
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
details.dropdown > .content {
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
min-width: max-content;
|
||||
margin: 0;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
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 > summary + ul > li {
|
||||
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%;
|
||||
background: none;
|
||||
|
||||
|
|
@ -101,42 +126,43 @@ details.dropdown > summary + ul > li {
|
|||
/* 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);
|
||||
}
|
||||
}
|
||||
/* Cancel default styling of button elements */
|
||||
|
||||
/* Suppress default styling of <button> */
|
||||
> button {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
/* dir-auto option - switch the direction at a width point where most of layout changes occur. */
|
||||
/* There's no way to check with CSS if LTR dropdown will fit on screen without JS. */
|
||||
/* 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 > summary + ul {
|
||||
details.dropdown.dir-auto > .content {
|
||||
inset-inline: 0 auto;
|
||||
direction: rtl;
|
||||
> li {
|
||||
> ul > li {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Note: https://css-tricks.com/css-anchor-positioning-guide/
|
||||
* looks like a great thing but FF still doesn't support it. */
|
||||
|
||||
details.dropdown.dir-rtl > summary + ul {
|
||||
/* dir-rtl option - force right-to-left box direction */
|
||||
details.dropdown.dir-rtl > .content {
|
||||
inset-inline: 0 auto;
|
||||
direction: rtl;
|
||||
> li {
|
||||
> ul > li {
|
||||
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. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue