Commit d8eafecb60
Verified · cmc
internal/httpd/sidecol_test.go added +74
| @@ -0,0 +1,74 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 4 | "html/template" | |
| 5 | "net/url" | |
| 6 | "strings" | |
| 7 | "testing" | |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/store" | |
| 10 | "gitbay.org/gitbay/internal/web" | |
| 11 | ) | |
| 12 | ||
| 13 | // renderIssues renders issues.html around the facets given, which is the | |
| 14 | // shared sidecol partial every list page uses. | |
| 15 | func renderIssues(t *testing.T, facets []facetGroup) string { | |
| 16 | t.Helper() | |
| 17 | var sb strings.Builder | |
| 18 | err := web.Render(&sb, "issues.html", struct { | |
| 19 | repoPage | |
| 20 | State string | |
| 21 | Label string | |
| 22 | Query string | |
| 23 | Filters []listFilter | |
| 24 | Facets []facetGroup | |
| 25 | Issues []store.Issue | |
| 26 | LabelColors map[string]template.CSS | |
| 27 | Older string | |
| 28 | }{repoPage: testRepoPage(), State: "open", Facets: facets}) | |
| 29 | if err != nil { | |
| 30 | t.Fatalf("render: %v", err) | |
| 31 | } | |
| 32 | return sb.String() | |
| 33 | } | |
| 34 | ||
| 35 | // The column's groups sit inside a details.sidedrop whose summary names | |
| 36 | // the filters in force. Below 62rem that disclosure is the column; above | |
| 37 | // it style.css hides the summary and shows the content, so the markup is | |
| 38 | // the same either way (#237). | |
| 39 | func TestSidecolIsADisclosure(t *testing.T) { | |
| 40 | base := url.Values{"state": {"open"}, "label": {"bug"}} | |
| 41 | labels := []store.Label{{Name: "bug", Issues: 2}} | |
| 42 | facets := listFacets(base, []string{"open", "closed"}, "open", labels, nil, false) | |
| 43 | ||
| 44 | out := renderIssues(t, facets) | |
| 45 | nav := strings.Index(out, `<nav class="sidecol" aria-label="Filters">`) | |
| 46 | drop := strings.Index(out, `<details class="sidedrop">`) | |
| 47 | sum := strings.Index(out, "<summary>") | |
| 48 | grp := strings.Index(out, `<div class="grp">`) | |
| 49 | if nav < 0 || drop < nav || sum < drop || grp < sum { | |
| 50 | t.Fatalf("nav=%d details=%d summary=%d grp=%d\n%s", nav, drop, sum, grp, out) | |
| 51 | } | |
| 52 | if !strings.Contains(out, `<span class="sideword">Filters</span>`) { | |
| 53 | t.Errorf("summary does not head with its word:\n%s", out) | |
| 54 | } | |
| 55 | // state and label are both in force, so both are named | |
| 56 | for _, want := range []string{`<span class="sideon">open</span>`, `<span class="sideon">bug</span>`} { | |
| 57 | if !strings.Contains(out, want) { | |
| 58 | t.Errorf("summary does not name %q:\n%s", want, out) | |
| 59 | } | |
| 60 | } | |
| 61 | if n := strings.Count(out, "</details>"); n != 1 { | |
| 62 | t.Errorf("details not closed once: %d", n) | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | // The column leads the list in the markup, because below 62rem it is a | |
| 67 | // control above what it narrows rather than a note after it (#237). | |
| 68 | func TestSidecolPrecedesTheList(t *testing.T) { | |
| 69 | facets := listFacets(url.Values{"state": {"open"}}, []string{"open"}, "open", nil, nil, false) | |
| 70 | out := renderIssues(t, facets) | |
| 71 | if i, j := strings.Index(out, `class="sidecol"`), strings.Index(out, `class="colmain"`); i < 0 || j < i { | |
| 72 | t.Errorf("sidecol=%d colmain=%d", i, j) | |
| 73 | } | |
| 74 | } | |
internal/web/static/style.css +45 −2
| @@ -1430,6 +1430,46 @@ a.memberchip .role { color: var(--muted); } | ||
| 1430 | 1430 | .sidecol form.searchform { margin-top: var(--sp-2); } |
| 1431 | 1431 | .sidecol form.searchform input[type="text"] { min-width: 0; width: 100%; } |
| 1432 | 1432 | |
| 1433 | /* The column's groups sit in a disclosure. Below 62rem that is all the | |
| 1434 | reader sees of the column: one closed line before the list, because a | |
| 1435 | repository with two dozen labels stacked the whole vocabulary under | |
| 1436 | the content it filters. Above 62rem the summary is hidden and the | |
| 1437 | content shown whatever the open state, so the column is what it | |
| 1438 | always was. The @supports guard is the fail-safe: an engine without | |
| 1439 | ::details-content keeps a working toggle at every width instead of | |
| 1440 | losing the column (#237). */ | |
| 1441 | .sidedrop > summary { | |
| 1442 | display: inline-flex; | |
| 1443 | align-items: center; | |
| 1444 | gap: var(--sp-2); | |
| 1445 | height: 32px; | |
| 1446 | padding: 0 var(--sp-3); | |
| 1447 | background: var(--canvas); | |
| 1448 | border: 1px solid var(--line); | |
| 1449 | border-radius: var(--r-ctl); | |
| 1450 | color: var(--fg); | |
| 1451 | font-size: var(--fs-2); | |
| 1452 | cursor: pointer; | |
| 1453 | list-style: none; | |
| 1454 | } | |
| 1455 | .sidedrop[open] > summary { margin-bottom: var(--sp-3); } | |
| 1456 | .sidedrop > summary::-webkit-details-marker { display: none; } | |
| 1457 | .sidedrop > summary:hover { background: var(--hover); } | |
| 1458 | .sidedrop > summary::after { content: "\25be"; color: var(--muted); } | |
| 1459 | .sidedrop[open] > summary::after { content: "\25b4"; } | |
| 1460 | .sideword { font-weight: 600; } | |
| 1461 | /* the filters in force: where you are */ | |
| 1462 | .sideon { color: var(--warn); } | |
| 1463 | /* every group came out empty, so there is nothing to open */ | |
| 1464 | .sidedrop:not(:has(.grp)) { display: none; } | |
| 1465 | ||
| 1466 | @media (min-width: 62.01rem) { | |
| 1467 | @supports selector(::details-content) { | |
| 1468 | .sidedrop > summary { display: none; } | |
| 1469 | .sidedrop::details-content { content-visibility: visible; } | |
| 1470 | } | |
| 1471 | } | |
| 1472 | ||
| 1433 | 1473 | /* a sticky column taller than the viewport scrolls inside itself, or its |
| 1434 | 1474 | tail is unreachable once the page scrolls. Above 62rem only: below it |
| 1435 | 1475 | .filenav is hidden and the other two are static. .dashpins stops being |
| @@ -1655,9 +1695,12 @@ svg.icon { vertical-align: -0.125em; } | ||
| 1655 | 1695 | .blobgrid { grid-template-columns: 1fr; } |
| 1656 | 1696 | .filenav { display: none; } |
| 1657 | 1697 | |
| 1658 | /* the column follows the content on a phone, the way the aside does */ | |
| 1698 | /* the column is a closed disclosure before the content: a filter you | |
| 1699 | reach without scrolling the list, and nothing to scroll past when | |
| 1700 | you do not want it. It leads rather than follows because it is what | |
| 1701 | the list is narrowed by, not a note on one row (#237) */ | |
| 1659 | 1702 | .withcol, .withcol.narrow { grid-template-columns: 1fr; } |
| 1660 | .sidecol { position: static; order: 2; } | |
| 1703 | .sidecol { position: static; margin-bottom: var(--sp-3); } | |
| 1661 | 1704 | .sidecol .grp { display: inline-block; vertical-align: top; margin-right: var(--sp-5); } |
| 1662 | 1705 | } |
| 1663 | 1706 | |
internal/web/templates/account.html +3
| @@ -7,6 +7,8 @@ | ||
| 7 | 7 | |
| 8 | 8 | <div class="withcol narrow"> |
| 9 | 9 | <nav class="sidecol" aria-label="Sections"> |
| 10 | <details class="sidedrop"> | |
| 11 | <summary><span class="sideword">Sections</span></summary> | |
| 10 | 12 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 11 | 13 | <ul> |
| 12 | 14 | <li><a href="#profile">Profile</a></li> |
| @@ -18,6 +20,7 @@ | ||
| 18 | 20 | <li><a href="#export">Export</a></li> |
| 19 | 21 | <li><a href="#cli">On the command line</a></li> |
| 20 | 22 | </ul></div> |
| 23 | </details> | |
| 21 | 24 | </nav> |
| 22 | 25 | <div class="colmain"> |
| 23 | 26 | <section id="profile"><h2>Profile</h2> |
internal/web/templates/admin.html +3
| @@ -7,6 +7,8 @@ | ||
| 7 | 7 | |
| 8 | 8 | <div class="withcol narrow"> |
| 9 | 9 | <nav class="sidecol" aria-label="Sections"> |
| 10 | <details class="sidedrop"> | |
| 11 | <summary><span class="sideword">Sections</span></summary> | |
| 10 | 12 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 11 | 13 | <ul> |
| 12 | 14 | <li><a href="#webhooks">Webhook deliveries</a></li> |
| @@ -15,6 +17,7 @@ | ||
| 15 | 17 | <li><a href="#builds">Builds</a></li> |
| 16 | 18 | <li><a href="#deps">Dependency checks</a></li> |
| 17 | 19 | </ul></div> |
| 20 | </details> | |
| 18 | 21 | </nav> |
| 19 | 22 | <div class="colmain"> |
| 20 | 23 | |
internal/web/templates/builds.html +3
| @@ -3,6 +3,8 @@ | ||
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <div class="withcol"> |
| 5 | 5 | <nav class="sidecol" aria-label="Filters"> |
| 6 | <details class="sidedrop"> | |
| 7 | <summary><span class="sideword">Filters</span>{{range .Facets}}{{range .Items}}{{if .Active}} <span class="sideon">{{.Label}}</span>{{end}}{{end}}{{end}}</summary> | |
| 6 | 8 | {{range .Facets}}{{if .Items}}<div class="grp"> |
| 7 | 9 | <h2 class="colhead">{{.Title}}</h2> |
| 8 | 10 | <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}</a></li>{{end}}</ul> |
| @@ -15,6 +17,7 @@ | ||
| 15 | 17 | <input type="hidden" name="job" value="{{.Filter.Job}}"> |
| 16 | 18 | <button type="submit" class="btn">Filter</button> |
| 17 | 19 | </form> |
| 20 | </details> | |
| 18 | 21 | </nav> |
| 19 | 22 | <div class="colmain"> |
| 20 | 23 | <div class="listhead"> |
internal/web/templates/globalsearch.html +3
| @@ -3,6 +3,8 @@ | ||
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <div class="withcol"> |
| 5 | 5 | <nav class="sidecol" aria-label="Filters"> |
| 6 | <details class="sidedrop"> | |
| 7 | <summary><span class="sideword">Filters</span>{{if .Kind}} <span class="sideon">{{.Kind}}</span>{{end}}</summary> | |
| 6 | 8 | <div class="grp"><h2 class="colhead">Kind</h2> |
| 7 | 9 | <ul> |
| 8 | 10 | <li><a{{if eq .Kind ""}} aria-current="page"{{end}} href="?q={{.Query}}">everything</a></li> |
| @@ -10,6 +12,7 @@ | ||
| 10 | 12 | <li><a{{if eq .Kind "issue"}} aria-current="page"{{end}} href="?q={{.Query}}&kind=issue">issues</a></li> |
| 11 | 13 | <li><a{{if eq .Kind "mr"}} aria-current="page"{{end}} href="?q={{.Query}}&kind=mr">merge requests</a></li> |
| 12 | 14 | </ul></div> |
| 15 | </details> | |
| 13 | 16 | </nav> |
| 14 | 17 | <div class="colmain"> |
| 15 | 18 | <div class="listhead"> |
internal/web/templates/layout.html +9
| @@ -122,11 +122,20 @@ | ||
| 122 | 122 | {{end}}</ul> |
| 123 | 123 | </nav>{{end}} |
| 124 | 124 | |
| 125 | {{/* Every side column's groups sit in a details.sidedrop. Below 62rem | |
| 126 | that disclosure is what the reader sees of the column: one closed | |
| 127 | line they open. Above it style.css hides the summary and shows the | |
| 128 | content whatever the open state, so the column reads as it always | |
| 129 | has (#237). The facet columns name the filters in force in the | |
| 130 | summary; the section navs head with the word alone. */}} | |
| 125 | 131 | {{define "sidecol"}}<nav class="sidecol" aria-label="Filters"> |
| 132 | <details class="sidedrop"> | |
| 133 | <summary><span class="sideword">Filters</span>{{range .}}{{range .Items}}{{if .Active}} <span class="sideon">{{.Label}}</span>{{end}}{{end}}{{end}}</summary> | |
| 126 | 134 | {{range .}}{{if .Items}}<div class="grp"> |
| 127 | 135 | <h2 class="colhead">{{.Title}}</h2> |
| 128 | 136 | <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}{{if .Count}} <i>{{.Count}}</i>{{end}}</a></li>{{end}}</ul> |
| 129 | 137 | </div>{{end}}{{end}} |
| 138 | </details> | |
| 130 | 139 | </nav>{{end}} |
| 131 | 140 | |
| 132 | 141 | {{define "branchicon"}}<svg class="icon" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>{{end}} |
internal/web/templates/settings.html +3
| @@ -6,6 +6,8 @@ | ||
| 6 | 6 | {{if .Notice}}{{if .Saved}}<p class="notice" role="status">{{.Notice}}</p>{{else}}<p class="error" role="alert">{{.Notice}}</p>{{end}}{{end}} |
| 7 | 7 | <div class="withcol narrow"> |
| 8 | 8 | <nav class="sidecol" aria-label="Sections"> |
| 9 | <details class="sidedrop"> | |
| 10 | <summary><span class="sideword">Sections</span></summary> | |
| 9 | 11 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 10 | 12 | <ul> |
| 11 | 13 | <li><a href="#identity">Identity</a></li> |
| @@ -17,6 +19,7 @@ | ||
| 17 | 19 | <li><a href="#runners">Runners</a></li> |
| 18 | 20 | <li><a href="#lifecycle">Lifecycle</a></li> |
| 19 | 21 | </ul></div> |
| 22 | </details> | |
| 20 | 23 | </nav> |
| 21 | 24 | <div class="colmain"> |
| 22 | 25 | |