detached b32c168

Merge f5cbe228467121e74ebd07e3ef6b53f3fa387721 into cf2636510abd18ed4875895a18b54026dbca22e1

commit
b32c168178ca845ff25dd635dd6ad7765ea021b5
author
ermite · · 2026-10-22 08:23 UTC
merge of
cf26365 + f5cbe22 · diff against the first parent
signature
unsigned
55 files · +1051 -671
A internal/adapters/web/accessibility_test.go +53 -0
@@ -0,0 +1,53 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestIssueFieldAccessibility(t *testing.T) {
12+ for _, field := range []string{"title", "body", "author"} {
13+ t.Run(field, func(t *testing.T) {
14+ html := renderToString(t, IssueForm(Layout{}, IssueFormPage{ErrorField: field, Error: "Too long"}))
15+ assert.Contains(t, html, `id="issue-`+field+`" aria-describedby="issue-`+field+`-count issue-`+field+`-error" aria-invalid="true"`)
16+ assert.Contains(t, html, `<p id="issue-`+field+`-error" class="f-error" role="alert">Too long</p>`)
17+ for _, f := range []string{"title", "body", "author"} {
18+ assert.Contains(t, html, `<output id="issue-`+f+`-count" aria-live="off"`)
19+ }
20+ })
21+ }
22+}
23+
24+func TestHintAccessibility(t *testing.T) {
25+ html := renderToString(t, Hint(Layout{Live: true, LiveURL: "/events"}))
26+ assert.Contains(t, html, `<div class="hint"><span aria-hidden="true" class="hint-nav"`)
27+ assert.Contains(t, html, `<span role="status" aria-live="polite"><span class="hint-sel"`)
28+ assert.Contains(t, html, `</span></span><span class="hint-live" role="status"`)
29+}
30+
31+func TestPaletteAccessibilityWithoutNavigationItems(t *testing.T) {
32+ html := renderToString(t, OverlayCmd(nil))
33+ // The shortcut preference action is always present, even with no targets.
34+ assert.Contains(t, html, `aria-expanded="true"`)
35+ assert.Contains(t, html, `aria-activedescendant="cmd-o0"`)
36+ assert.Contains(t, html, `id="cmd-o0" role="option" aria-selected="true" data-cursor`)
37+ assert.Contains(t, html, `<a role="presentation" data-fire="keys">`)
38+ assert.Contains(t, html, `aria-label="Close (esc)"`)
39+ assert.NotContains(t, html, ` href=`)
40+ assert.NotContains(t, html, `tabindex="-1"`)
41+}
42+
43+func TestFinderStatusOutsideHiddenResults(t *testing.T) {
44+ html := renderToString(t, Tree(Layout{}, TreePage{}))
45+ assert.Contains(t, html, `<p id="finder-status" class="dim" role="status"></p><div id="finder" class="tree-card" hidden>`)
46+}
47+
48+func TestPaletteNavigationOptionsAccessibility(t *testing.T) {
49+ html := renderToString(t, OverlayCmd([]CmdItem{{Kind: "section", Label: "Files", Href: "/files"}}))
50+ assert.Contains(t, html, `<a role="presentation" data-href="/files">`)
51+ assert.NotContains(t, html, ` href=`)
52+ assert.NotContains(t, html, `tabindex="-1"`)
53+}
M internal/adapters/web/fragments.templ +1 -0
@@ -94,6 +94,7 @@ templ Live(l Layout) {
9494 if l.Live && l.LiveURL != "" {
9595 <span
9696 class="hint-live"
97+ role="status"
9798 data-state="connecting"
9899 hx-sse:connect={ l.LiveURL }
99100 hx-swap="none"
M internal/adapters/web/fragments_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/issues_form.templ +47 -9
@@ -29,22 +29,52 @@ templ IssueForm(l Layout, p IssueFormPage) {
2929 <input type="hidden" name="form_token" value={ p.FormToken }/>
3030 <input type="hidden" name="solution" value=""/>
3131 <label for="issue-title">title</label>
32- <input id="issue-title" name="title" type="text" required autocomplete="off" value={ p.Title }/>
33- <output data-for="issue-title" data-max={ itoa(p.MaxTitle) } for="issue-title">{ itoa(runeCount(p.Title)) } / { itoa(p.MaxTitle) }</output>
32+ <input
33+ id="issue-title"
34+ aria-describedby={ issueFieldDescription("title", p.ErrorField) }
35+ if p.ErrorField == "title" {
36+ aria-invalid="true"
37+ }
38+ name="title"
39+ type="text"
40+ required
41+ autocomplete="off"
42+ value={ p.Title }
43+ />
44+ <output id="issue-title-count" aria-live="off" data-for="issue-title" data-max={ itoa(p.MaxTitle) } for="issue-title">{ itoa(runeCount(p.Title)) } / { itoa(p.MaxTitle) }</output>
3445 if p.ErrorField == "title" {
35- <p class="f-error" role="alert">{ p.Error }</p>
46+ <p id="issue-title-error" class="f-error" role="alert">{ p.Error }</p>
3647 }
3748 <label for="issue-body">description</label>
38- <textarea id="issue-body" name="body" rows="8" placeholder="Markdown, optional">{ p.Body }</textarea>
39- <output data-for="issue-body" data-max={ itoa(p.MaxBody) } for="issue-body">{ itoa(runeCount(p.Body)) } / { itoa(p.MaxBody) }</output>
49+ <textarea
50+ id="issue-body"
51+ aria-describedby={ issueFieldDescription("body", p.ErrorField) }
52+ if p.ErrorField == "body" {
53+ aria-invalid="true"
54+ }
55+ name="body"
56+ rows="8"
57+ placeholder="Markdown, optional"
58+ >{ p.Body }</textarea>
59+ <output id="issue-body-count" aria-live="off" data-for="issue-body" data-max={ itoa(p.MaxBody) } for="issue-body">{ itoa(runeCount(p.Body)) } / { itoa(p.MaxBody) }</output>
4060 if p.ErrorField == "body" {
41- <p class="f-error" role="alert">{ p.Error }</p>
61+ <p id="issue-body-error" class="f-error" role="alert">{ p.Error }</p>
4262 }
4363 <label for="issue-author">name (optional, not verified)</label>
44- <input id="issue-author" name="author" type="text" autocomplete="off" value={ p.Author }/>
45- <output data-for="issue-author" data-max={ itoa(p.MaxAuthor) } for="issue-author">{ itoa(runeCount(p.Author)) } / { itoa(p.MaxAuthor) }</output>
64+ <input
65+ id="issue-author"
66+ aria-describedby={ issueFieldDescription("author", p.ErrorField) }
67+ if p.ErrorField == "author" {
68+ aria-invalid="true"
69+ }
70+ name="author"
71+ type="text"
72+ autocomplete="off"
73+ value={ p.Author }
74+ />
75+ <output id="issue-author-count" aria-live="off" data-for="issue-author" data-max={ itoa(p.MaxAuthor) } for="issue-author">{ itoa(runeCount(p.Author)) } / { itoa(p.MaxAuthor) }</output>
4676 if p.ErrorField == "author" {
47- <p class="f-error" role="alert">{ p.Error }</p>
77+ <p id="issue-author-error" class="f-error" role="alert">{ p.Error }</p>
4878 }
4979 <div class="hp" aria-hidden="true">
5080 <label for="issue-website">Leave this field empty</label>
@@ -65,3 +95,11 @@ templ issueFormRail() {
6595 <p class="dim">The owner reads every issue before it appears. Plain text and Markdown only; no address of yours is kept.</p>
6696 </section>
6797 }
98+
99+func issueFieldDescription(field, invalid string) string {
100+ ids := "issue-" + field + "-count"
101+ if field == invalid {
102+ ids += " issue-" + field + "-error"
103+ }
104+ return ids
105+}
M internal/adapters/web/issues_form_templ.go +161 -84 generated file: collapsed
M internal/adapters/web/issues_form_test.go +2 -2
@@ -79,9 +79,9 @@ func TestIssueFormGet(t *testing.T) {
7979 `action="/proj/issues/new"`, ` hidden`, `data-issue-form`, `data-nonce="AAECAwQFBgcICQoLDA0ODw"`,
8080 `data-difficulty="18"`, `data-min-submit-at="1700000003000"`, `data-worker="/-/static/js/pow-worker.`,
8181 `name="token" value="tok-1.sig"`, `name="form_token" value="form-1"`, `name="solution" value=""`,
82- `name="title" type="text" required`, `<output data-for="issue-body" data-max="10000"`, `<output data-for="issue-author" data-max="40"`,
82+ `name="title" type="text" required`, `<output id="issue-body-count" aria-live="off" data-for="issue-body" data-max="10000"`, `<output id="issue-author-count" aria-live="off" data-for="issue-author" data-max="40"`,
8383 `<div class="hp" aria-hidden="true">`, `name="website" type="text" tabindex="-1" autocomplete="off"`,
84- `<output data-for="issue-title" data-max="120"`, `0 / 10000`, `Checking your browser… (this takes a few seconds)`,
84+ `<output id="issue-title-count" aria-live="off" data-for="issue-title" data-max="120"`, `0 / 10000`, `Checking your browser… (this takes a few seconds)`,
8585 `/-/static/js/issue-form.`,
8686 } {
8787 assert.Contains(t, body, want)
M internal/adapters/web/jstest/a11y.test.mjs +79 -1
@@ -83,7 +83,7 @@ function keydown(target, key, elementHandler, extra = {}) {
8383 // ---- the palette --------------------------------------------------------
8484
8585 function palette() {
86- const li = (k, label) => el('li', { attrs: { 'data-k': k } }, el('a', { attrs: { href: '/' + label } }));
86+ const li = (k, label) => el('li', { attrs: { 'data-k': k } }, el('a', { attrs: { 'data-href': '/' + label, role: 'presentation' } }));
8787 const items = [li('go home', 'home'), li('section files', 'files'), li('branch main', 'main')];
8888 items[0].setAttribute('data-cursor', '');
8989 const input = el('input', { id: 'cmd-q', attrs: { role: 'combobox', 'aria-controls': 'cmd-list' } });
@@ -137,6 +137,8 @@ test('the palette: the file hits that arrive later get ids, roles and the active
137137 assert.ok(hits.length > 0, 'a file hit came');
138138 for (const h of hits) {
139139 assert.equal(h.getAttribute('role'), 'option');
140+ assert.equal(h.querySelector('a[href]'), null);
141+ assert.equal(h.querySelector('a').hasAttribute('tabindex'), false);
140142 assert.ok(h.id);
141143 }
142144 assert.equal(p.list.children.length, new Set(p.list.children.map((i) => i.id)).size, 'ids stay unique');
@@ -173,6 +175,7 @@ test('Escape in the finder clears the query, keeps the focus in the field and do
173175 f.q.closest = () => f.q; // the global handler sees a field being typed in
174176 const e = keydown(f.q, 'Escape', (ev) => F.key(f.q, ev));
175177 assert.equal(f.q.value, '');
178+ assert.equal(doc.getElementById('finder-status').textContent, '');
176179 assert.equal(e.stopped, true);
177180 assert.equal(e.prevented, true);
178181 assert.equal(blurred, false, 'the field keeps the focus');
@@ -381,3 +384,78 @@ test('mobile MR layout shortcuts preserve the saved preference until back on des
381384 doc.querySelector = oldQuery;
382385 }
383386 });
387+
388+test('dialog restores its row opener on close, but not over a replacement dialog', () => {
389+ const row = el('li', { tabindex: '0' });
390+ const first = el('dialog', { id: 'focus-first' });
391+ const second = el('dialog', { id: 'focus-second' });
392+ dom.body.append(row, first, second);
393+ const queued = [];
394+ const all = doc.querySelectorAll;
395+ const one = doc.querySelector;
396+ const open = () => [first, second].filter((d) => d.hasAttribute('open'));
397+ doc.querySelectorAll = (sel) => sel === 'dialog[open]' ? open() : all.call(doc, sel);
398+ doc.querySelector = (sel) => sel === 'dialog[open]' ? open()[0] ?? null : one.call(doc, sel);
399+ for (const d of [first, second]) {
400+ let onClose;
401+ d.addEventListener = (type, fn) => { if (type === 'close') onClose = fn; };
402+ d.showModal = () => { d.setAttribute('open', ''); d.focus(); };
403+ d.close = () => { d.removeAttribute('open'); queued.push(onClose); };
404+ }
405+ row.focus();
406+ window.ermite.dialog('focus-first');
407+ first.close();
408+ queued.shift()();
409+ assert.equal(doc.activeElement, row);
410+
411+ row.focus();
412+ window.ermite.dialog('focus-first');
413+ window.ermite.dialog('focus-second');
414+ queued.shift()();
415+ assert.equal(doc.activeElement, second);
416+ second.close();
417+ queued.shift()();
418+ doc.querySelectorAll = all;
419+ doc.querySelector = one;
420+});
421+
422+
423+test('listbox result wrappers have no links; Enter and delegated clicks navigate', async () => {
424+ doc.querySelector = query.one;
425+ doc.querySelectorAll = query.all;
426+ const assigned = [];
427+ location.assign = (href) => assigned.push(href);
428+ const activate = (anchor) => {
429+ const e = { target: anchor, button: 0, preventDefault() {} };
430+ listeners.click.find((fn) => fn.toString().includes('location.assign'))(e);
431+ };
432+ const p = palette();
433+ p.list.setAttribute('role', 'listbox');
434+ window.ermiteCmd.sync();
435+ for (const item of p.items) assert.equal(item.querySelector('a[href]'), null);
436+ const a = p.items[0].querySelector('a');
437+ a.click = () => activate(a);
438+ window.ermite.cmdKey({ key: 'Enter', preventDefault() {} });
439+ assert.deepEqual(assigned, ['/home']);
440+ activate(p.items[1].querySelector('a'));
441+ assert.deepEqual(assigned, ['/home', '/files']);
442+
443+ const f = finder();
444+ f.list.setAttribute('role', 'listbox');
445+ f.q.value = 'main';
446+ F.input(f.q);
447+ flushFrames();
448+ await settle();
449+ assert.equal(f.list.querySelector('a[href]'), null);
450+ const hit = f.list.children[0].querySelector('a');
451+ assert.equal(hit.hasAttribute('tabindex'), false);
452+ hit.click = () => activate(hit);
453+ F.key(f.q, { key: 'Enter', preventDefault() {} });
454+ assert.equal(assigned.at(-1), hit.dataset.href);
455+ activate(hit);
456+ assert.equal(assigned.at(-1), hit.dataset.href);
457+ assert.notEqual(doc.getElementById('finder-status').textContent, '');
458+ f.q.value = '';
459+ F.input(f.q);
460+ assert.equal(doc.getElementById('finder-status').textContent, '');
461+});
M internal/adapters/web/jstest/finder.test.mjs +9 -8
@@ -53,7 +53,7 @@ const key = (k, extra = {}) => {
5353 return e;
5454 };
5555 const status = () => document.getElementById('finder-status').textContent;
56-const hrefs = (list) => list.children.map((li) => li.querySelector('a').href);
56+const hrefs = (list) => list.children.map((li) => li.querySelector('a').dataset.href);
5757
5858 test('Enter before the results opens the top hit once they come', async () => {
5959 const p = page();
@@ -66,8 +66,8 @@ test('Enter before the results opens the top hit once they come', async () => {
6666 await settle();
6767 assert.equal(p.finderList.children.length > 0, true);
6868 assert.equal(dom.clicked.length, 1);
69- assert.equal(dom.clicked[0].href, hrefs(p.finderList)[0]);
70- assert.match(dom.clicked[0].href, /^\/blob\/main\/.*tree/);
69+ assert.equal(dom.clicked[0].dataset.href, hrefs(p.finderList)[0]);
70+ assert.match(dom.clicked[0].dataset.href, /^\/blob\/main\/.*tree/);
7171 assert.equal(fetches, 1);
7272 });
7373
@@ -112,7 +112,7 @@ test('Enter on current results opens the cursor row, whichever way the cursor mo
112112 assert.equal(items[2].getAttribute('aria-selected'), 'true');
113113 assert.equal(items[0].getAttribute('aria-selected'), 'false');
114114 assert.equal(p.q.getAttribute('aria-activedescendant'), items[2].id);
115- assert.equal(dom.clicked[0].href, items[2].querySelector('a').href);
115+ assert.equal(dom.clicked[0].dataset.href, items[2].querySelector('a').dataset.href);
116116 // The arrows move from there.
117117 F.key(p.q, key('ArrowUp'));
118118 assert.equal(p.q.getAttribute('aria-activedescendant'), items[1].id);
@@ -179,13 +179,14 @@ test('the palette lists the file hits under its entries', async () => {
179179 const hits = p.cmdList.children.filter((li) => li.className === 'finder-hit');
180180 assert.equal(hits.length, 4);
181181 assert.equal(p.cmdList.children[0].className, 'entry', 'the entries stay first');
182- assert.deepEqual(hits.map((li) => li.querySelector('a').href).sort(), [
182+ assert.deepEqual(hits.map((li) => li.querySelector('a').dataset.href).sort(), [
183183 '/blob/main/docs/web/tree-view.md',
184184 '/blob/main/internal/adapters/web/tree.go',
185185 '/blob/main/internal/adapters/web/tree.templ',
186186 '/blob/main/src/treehouse.txt',
187187 ]);
188- assert.equal(hits[0].querySelector('a').tabIndex, -1);
188+ assert.equal(hits[0].querySelector('a').hasAttribute('href'), false);
189+ assert.equal(hits[0].querySelector('a').hasAttribute('tabindex'), false);
189190 assert.equal(hits[0].querySelector('.k').textContent, 'file');
190191 assert.equal(p.cmdList.children.filter((li) => li.hasAttribute('data-cursor')).length, 1, 'one cursor');
191192
@@ -213,7 +214,7 @@ test('Enter typed before the frame of the query runs opens the top hit', async (
213214 assert.equal(dom.clicked.length, 0, 'nothing to open before the frame');
214215 flushFrames();
215216 await settle();
216- assert.deepEqual(dom.clicked.map((a) => a.href), ['/blob/main/cmd/ermite/main.go']);
217+ assert.deepEqual(dom.clicked.map((a) => a.dataset.href), ['/blob/main/cmd/ermite/main.go']);
217218 } finally {
218219 dom.holdFrames = false;
219220 }
@@ -257,7 +258,7 @@ test('the palette drops the results of a query that is stale', async () => {
257258 p.cmdQ.value = 'readme'; // typed before the search of "tree" finished
258259 F.palette(p.cmdQ);
259260 await settle();
260- const got = p.cmdList.children.filter((li) => li.className === 'finder-hit').map((li) => li.querySelector('a').href);
261+ const got = p.cmdList.children.filter((li) => li.className === 'finder-hit').map((li) => li.querySelector('a').dataset.href);
261262 assert.deepEqual(got, ['/blob/main/README.md']);
262263
263264 // A field changed without a new call: the pending search is dropped.
M internal/adapters/web/jstest/mr.test.mjs +33 -1
@@ -106,7 +106,7 @@ function footerHarness(extra = {}) {
106106 const nav = node('tabs'); nav.querySelectorAll = () => tabs;
107107 const listeners = {}, replaced = [];
108108 globalThis.document = { querySelector: (s) => s === '.mr-footer' ? footer : s === '[data-mr-tabs]' ? nav : s === '#mr-ck' ? extra.summary ?? null : null,
109- querySelectorAll: (s) => s === '[data-mr-tabs] a' ? tabs : [], addEventListener(k, v) { listeners[k] = v; } };
109+ querySelectorAll: (s) => s === '[data-mr-tabs] button' ? tabs : [], addEventListener(k, v) { listeners[k] = v; } };
110110 globalThis.location = { hash: '', pathname: '/demo/merge-requests/1', search: '?x=1' };
111111 globalThis.addEventListener = (k, v) => { listeners[k] = v; };
112112 globalThis.history = { replaceState: (...args) => replaced.push(args) };
@@ -531,3 +531,35 @@ test('intercepted Cancel resets the entire comment form', () => {
531531 assert.equal(window.ermiteMR.hasDraft(), false);
532532 } finally { Object.assign(globalThis, saved); c.restore(); }
533533 });
534+
535+
536+test('live refresh preserves the open button panel even when the hash points elsewhere', async () => {
537+ const { readFileSync } = await import('node:fs');
538+ const src = readFileSync(new URL('../static/js/mr.js', import.meta.url), 'utf8');
539+ const start = src.indexOf(' async function refreshPage(page)');
540+ const end = src.indexOf(' function refreshNotice', start);
541+ const { el, installDom } = await import('./dom.mjs');
542+ const saved = globalThis.document;
543+ const h = footerHarness();
544+ try {
545+ window.ermiteMR.panels();
546+ h.tabs[1].events.click({ preventDefault() {} });
547+ location.hash = '#f99';
548+ const doc = installDom();
549+ Object.defineProperty(doc, 'activeElement', { get: () => null });
550+ const diffs = el('div');
551+ diffs.dataset.diff = 'unified';
552+ const fresh = el('footer', {}, el('section', { attrs: { 'data-mr-panel': '' } }), el('section', { attrs: { 'data-mr-panel': '' } }));
553+ const page = { body: { dataset: {} } };
554+ const $ = (s, root) => s === '#diffs' ? diffs : s === '.mr-footer' ? (root === page ? fresh : h.footer) : el('div');
555+ const $$ = (s, root) => s === '[data-mr-tabs] button' ? h.tabs : s === '[data-mr-panel]' ? fresh.children : [];
556+ let selected;
557+ const lazy = { reset() {}, watch() {} };
558+ const refresh = new Function('$', '$$', 'scrollAnchor', 'hasDraft', 'replaceRegion', 'panels', 'watchBar', 'foldFiles', 'touchComments', 'restoreAnchor', 'window', 'document',
559+ 'let revealGen = 0; ' + src.slice(start, end) + '; return refreshPage;')(
560+ $, $$, () => ({}), () => false, async () => {}, (i) => { selected = i; }, () => {}, () => {}, () => {}, () => {}, { ermiteLazy: lazy }, doc);
561+ await refresh(page);
562+ assert.equal(selected, 1);
563+ assert.deepEqual(fresh.children.map((p) => p.hidden), [true, false]);
564+ } finally { h.restore(); globalThis.document = saved; }
565+});
M internal/adapters/web/mr.templ +9 -9
@@ -296,7 +296,7 @@ templ commentView(c CommentView, suffix string) {
296296 <button type="button" class="btn" data-js data-quote={ c.Quote }>Quote</button>
297297 }
298298 if c.Edit != nil {
299- <details><summary>Edit</summary><form method="post" action={ templ.SafeURL(c.Edit.Action) }><input type="hidden" name="from" value={ itoa(c.Edit.From) }/><textarea name="text" maxlength="8000">{ c.Edit.Text }</textarea><button class="btn" type="submit">Save</button></form></details>
299+ <details><summary>Edit</summary><form method="post" action={ templ.SafeURL(c.Edit.Action) }><input type="hidden" name="from" value={ itoa(c.Edit.From) }/><textarea name="text" maxlength="8000" aria-label="Edit note">{ c.Edit.Text }</textarea><button class="btn" type="submit">Save</button></form></details>
300300 }
301301 if c.Delete != nil {
302302 <details><summary>Delete</summary><form method="post" action={ templ.SafeURL(c.Delete.Action) }><input type="hidden" name="from" value={ itoa(c.Delete.From) }/><p>Delete this note?</p><button class="btn" type="submit">Confirm delete</button></form></details>
@@ -373,9 +373,9 @@ templ commentForm(f CommentForm, suffix string) {
373373
374374 templ mrFooter(p MRPage) {
375375 <footer class="mr-footer">
376- <nav data-mr-tabs aria-label="Branch details">
377- <a id="mr-tab-commits" href="#mr-commits">Commits <span class="count">{ p.CommitCount }</span></a>
378- <a id="mr-tab-checks" href="#mr-checks">
376+ <div data-mr-tabs data-js role="group" aria-label="Branch details">
377+ <button type="button" id="mr-tab-commits" aria-expanded="true" aria-controls="mr-commits">Commits <span class="count">{ p.CommitCount }</span></button>
378+ <button type="button" id="mr-tab-checks" aria-expanded="true" aria-controls="mr-checks-panel">
379379 { "Checks " }
380380 <span data-checks-label>
381381 if p.ChecksSum.Total > 0 {
@@ -384,10 +384,10 @@ templ mrFooter(p MRPage) {
384384 no checks
385385 }
386386 </span>
387- </a>
388- </nav>
389- <div id="mr-commits" data-mr-panel role="region" aria-labelledby="mr-tab-commits">
390- <table class="mr-commits">
387+ </button>
388+ </div>
389+ <div id="mr-commits" data-mr-panel role="group" aria-labelledby="mr-tab-commits">
390+ <table class="mr-commits" aria-label="Commits">
391391 <thead><tr><th>Commit</th><th>Subject</th><th>Author</th><th>Date</th><th>Signature</th><th>Status</th></tr></thead>
392392 <tbody>
393393 for _, c := range p.Commits {
@@ -421,7 +421,7 @@ templ mrFooter(p MRPage) {
421421 <p class="note">{ p.CommitsNote }</p>
422422 }
423423 </div>
424- <div id="mr-checks-panel" data-mr-panel role="region" aria-labelledby="mr-tab-checks">
424+ <div id="mr-checks-panel" data-mr-panel role="group" aria-labelledby="mr-tab-checks">
425425 @MRChecks(p.Checks)
426426 </div>
427427 </footer>
M internal/adapters/web/mr_templ.go +7 -7 generated file: collapsed
M internal/adapters/web/mr_test.go +8 -7
@@ -137,21 +137,22 @@ func TestMRPanelsAndMenus(t *testing.T) {
137137 p := MRPage{CommitCount: "500+", CommitsNote: "more commits", Modes: []string{"squash", "merge"}, ChecksBlock: true,
138138 ChecksSum: ChecksSummary{Total: 2, Label: "1/2", Title: "1/2 failed"}, Box: &MergeBox{Signed: true, Fingerprint: "fingerprint", HeadGone: true}}
139139 footer := renderToString(t, mrFooter(p))
140- assert.Contains(t, footer, `<footer class="mr-footer"><nav data-mr-tabs`)
140+ assert.Contains(t, footer, `<footer class="mr-footer"><div data-mr-tabs data-js role="group"`)
141141 assert.NotContains(t, footer, `<details`)
142142 assert.NotContains(t, footer, `<summary`)
143- // No hidden attribute: the CSS shows the panel of the URL fragment until
144- // mr.js takes over, so the tabs work as anchors without scripts.
145- assert.Contains(t, footer, `id="mr-commits" data-mr-panel role="region"`)
146- assert.Contains(t, footer, `id="mr-checks-panel" data-mr-panel role="region"`)
143+ // Both panels are readable without scripts; JS enhances native buttons.
144+ assert.Contains(t, footer, `id="mr-commits" data-mr-panel role="group"`)
145+ assert.Contains(t, footer, `id="mr-checks-panel" data-mr-panel role="group"`)
147146 assert.NotContains(t, footer, `data-mr-panel hidden`)
148147 css, err := staticFS.ReadFile("static/css/60-mr.css")
149148 require.NoError(t, err)
150- assert.Contains(t, string(css), `&:not([data-panels]) [data-mr-panel]:not(:target, :has(:target)) { display: none; }`)
149+ assert.NotContains(t, string(css), `:not([data-panels])`)
150+ assert.Contains(t, footer, `<button type="button" id="mr-tab-commits" aria-expanded="true" aria-controls="mr-commits">`)
151+ assert.Contains(t, footer, `<button type="button" id="mr-tab-checks" aria-expanded="true" aria-controls="mr-checks-panel">`)
151152 assert.Contains(t, footer, `data-mr-tabs`)
152153 assert.Contains(t, footer, `id="mr-commits"`)
153154 assert.Contains(t, footer, `id="mr-checks"`)
154- assert.Contains(t, footer, `<table class="mr-commits">`)
155+ assert.Contains(t, footer, `<table class="mr-commits" aria-label="Commits">`)
155156 assert.Contains(t, footer, "500+")
156157 assert.Contains(t, footer, "more commits")
157158 assert.Contains(t, footer, "1/2 failed")
M internal/adapters/web/overlays.templ +5 -5
@@ -11,7 +11,7 @@ import (
1111 // show on that page only.
1212 templ OverlayHelp(mr bool) {
1313 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
14- <div class="panel-head"><h2>Keyboard shortcuts</h2><button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div>
14+ <div class="panel-head"><h2>Keyboard shortcuts</h2><button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div>
1515 <div class="keys-pref">
1616 <label><input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"/> Turn off single-key shortcuts</label>
1717 <p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
@@ -60,7 +60,7 @@ templ OverlayRefs(l Layout) {
6060 data-pick="#panel-refs li"
6161 hx-on:input="ermite.filter(this)"
6262 />
63- <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>
63+ <button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>
6464 </div>
6565 <div class="panel-cols" data-picklist>
6666 <section aria-label="Branches">
@@ -123,13 +123,13 @@ templ OverlayCmd(items []CmdItem) {
123123 hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)"
124124 hx-on:keydown="ermite.cmdKey(event)"
125125 />
126- <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>
126+ <button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>
127127 </div>
128128 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
129129 for i, c := range items {
130- <li id={ "cmd-o" + itoa(i) } role="option" aria-selected={ strconv.FormatBool(i == 0) } data-k={ strings.ToLower(c.Kind + " " + c.Label) } data-cursor?={ i == 0 }><a href={ templ.SafeURL(c.Href) } tabindex="-1"><span class="lbl">{ c.Label }</span><span class="k">{ c.Kind }</span></a></li>
130+ <li id={ "cmd-o" + itoa(i) } role="option" aria-selected={ strconv.FormatBool(i == 0) } data-k={ strings.ToLower(c.Kind + " " + c.Label) } data-cursor?={ i == 0 }><a role="presentation" data-href={ c.Href }><span class="lbl">{ c.Label }</span><span class="k">{ c.Kind }</span></a></li>
131131 }
132- <li id={ "cmd-o" + itoa(len(items)) } role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"><a href="#" data-fire="keys" tabindex="-1"><span class="lbl">turn single-key shortcuts off</span><span class="k">action</span></a></li>
132+ <li id={ "cmd-o" + itoa(len(items)) } role="option" aria-selected={ strconv.FormatBool(len(items) == 0) } data-cursor?={ len(items) == 0 } data-k="action turn single-key shortcuts on off keyboard"><a role="presentation" data-fire="keys"><span class="lbl">turn single-key shortcuts off</span><span class="k">action</span></a></li>
133133 </ul>
134134 <p class="panel-foot"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p>
135135 </dialog>
M internal/adapters/web/overlays_templ.go +35 -12
@@ -38,7 +38,7 @@ func OverlayHelp(mr bool) templ.Component {
3838 templ_7745c5c3_Var1 = templ.NopComponent
3939 }
4040 ctx = templ.ClearChildren(ctx)
41- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><div class=\"keys-pref\"><label><input type=\"checkbox\" id=\"keys-off\" aria-describedby=\"keys-note\" hx-on:change=\"ermite.setKeysOff(this.checked)\"> Turn off single-key shortcuts</label><p id=\"keys-note\" class=\"dim\">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor; on Files, a directory opens or closes in place</dd></div><div><dt><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div><div><dt><kbd>b</kbd></dt><dd>")
41+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<dialog id=\"panel-help\" class=\"panel modal\" aria-label=\"Keyboard shortcuts\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><h2>Keyboard shortcuts</h2><button type=\"button\" class=\"panel-x\" aria-label=\"Close (esc)\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd> close</button></div><div class=\"keys-pref\"><label><input type=\"checkbox\" id=\"keys-off\" aria-describedby=\"keys-note\" hx-on:change=\"ermite.setKeysOff(this.checked)\"> Turn off single-key shortcuts</label><p id=\"keys-note\" class=\"dim\">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p></div><dl class=\"keys\"><div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div><div><dt><kbd>?</kbd></dt><dd>this list</dd></div><div><dt><kbd>j</kbd> <kbd>k</kbd></dt><dd>move the row cursor down / up</dd></div><div><dt><kbd>enter</kbd></dt><dd>open the row under the cursor; on Files, a directory opens or closes in place</dd></div><div><dt><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> <kbd>home</kbd> <kbd>end</kbd></dt><dd>in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file</dd></div><div><dt><kbd>l</kbd> <kbd>h</kbd></dt><dd>on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls</dd></div><div><dt><kbd>g g</kbd> <kbd>G</kbd></dt><dd>first / last row</dd></div><div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div><div><dt><kbd>b</kbd></dt><dd>")
4242 if templ_7745c5c3_Err != nil {
4343 return templ_7745c5c3_Err
4444 }
@@ -102,7 +102,7 @@ func OverlayRefs(l Layout) templ.Component {
102102 }
103103 ctx = templ.ClearChildren(ctx)
104104 if l.Ref != "" {
105- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<dialog id=\"panel-refs\" class=\"panel modal\" aria-label=\"Switch ref\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><label for=\"refs-filter\" class=\"sr\">filter branches and tags</label> <input id=\"refs-filter\" type=\"search\" placeholder=\"Switch branch or tag…\" autocomplete=\"off\" data-pick=\"#panel-refs li\" hx-on:input=\"ermite.filter(this)\"> <button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd></button></div><div class=\"panel-cols\" data-picklist><section aria-label=\"Branches\"><h2>Branches <span class=\"count\">")
105+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<dialog id=\"panel-refs\" class=\"panel modal\" aria-label=\"Switch ref\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><label for=\"refs-filter\" class=\"sr\">filter branches and tags</label> <input id=\"refs-filter\" type=\"search\" placeholder=\"Switch branch or tag…\" autocomplete=\"off\" data-pick=\"#panel-refs li\" hx-on:input=\"ermite.filter(this)\"> <button type=\"button\" class=\"panel-x\" aria-label=\"Close (esc)\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd></button></div><div class=\"panel-cols\" data-picklist><section aria-label=\"Branches\"><h2>Branches <span class=\"count\">")
106106 if templ_7745c5c3_Err != nil {
107107 return templ_7745c5c3_Err
108108 }
@@ -260,7 +260,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
260260 templ_7745c5c3_Var9 = templ.NopComponent
261261 }
262262 ctx = templ.ClearChildren(ctx)
263- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<dialog id=\"panel-cmd\" class=\"panel modal\" aria-label=\"Go to\" hx-on:close=\"ermiteFinder.paletteClose()\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><label for=\"cmd-q\" class=\"sr\">go to</label> <input id=\"cmd-q\" type=\"search\" placeholder=\"Go to a section, branch, tag, file or repository…\" autocomplete=\"off\" spellcheck=\"false\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"true\" aria-controls=\"cmd-list\" aria-activedescendant=\"cmd-o0\" hx-on:input=\"ermite.cmdFilter(this); ermiteFinder.palette(this)\" hx-on:keydown=\"ermite.cmdKey(event)\"> <button type=\"button\" class=\"panel-x\" aria-label=\"Close\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd></button></div><ul id=\"cmd-list\" class=\"cmd-list\" role=\"listbox\" aria-label=\"Targets\">")
263+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<dialog id=\"panel-cmd\" class=\"panel modal\" aria-label=\"Go to\" hx-on:close=\"ermiteFinder.paletteClose()\" hx-on:keydown=\"event.key === 'Escape' && (event.preventDefault(), this.close())\" hx-on:click=\"event.target === this && this.close()\"><div class=\"panel-head\"><label for=\"cmd-q\" class=\"sr\">go to</label> <input id=\"cmd-q\" type=\"search\" placeholder=\"Go to a section, branch, tag, file or repository…\" autocomplete=\"off\" spellcheck=\"false\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"true\" aria-controls=\"cmd-list\" aria-activedescendant=\"cmd-o0\" hx-on:input=\"ermite.cmdFilter(this); ermiteFinder.palette(this)\" hx-on:keydown=\"ermite.cmdKey(event)\"> <button type=\"button\" class=\"panel-x\" aria-label=\"Close (esc)\" hx-on:click=\"closest('dialog').close()\"><kbd>esc</kbd></button></div><ul id=\"cmd-list\" class=\"cmd-list\" role=\"listbox\" aria-label=\"Targets\">")
264264 if templ_7745c5c3_Err != nil {
265265 return templ_7745c5c3_Err
266266 }
@@ -314,27 +314,27 @@ func OverlayCmd(items []CmdItem) templ.Component {
314314 return templ_7745c5c3_Err
315315 }
316316 }
317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a href=\"")
317+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a role=\"presentation\" data-href=\"")
318318 if templ_7745c5c3_Err != nil {
319319 return templ_7745c5c3_Err
320320 }
321- var templ_7745c5c3_Var13 templ.SafeURL
322- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
321+ var templ_7745c5c3_Var13 string
322+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Href)
323323 if templ_7745c5c3_Err != nil {
324- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 198}
324+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 208}
325325 }
326- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
326+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
327327 if templ_7745c5c3_Err != nil {
328328 return templ_7745c5c3_Err
329329 }
330- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" tabindex=\"-1\"><span class=\"lbl\">")
330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><span class=\"lbl\">")
331331 if templ_7745c5c3_Err != nil {
332332 return templ_7745c5c3_Err
333333 }
334334 var templ_7745c5c3_Var14 string
335335 templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
336336 if templ_7745c5c3_Err != nil {
337- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 242}
337+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 238}
338338 }
339339 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
340340 if templ_7745c5c3_Err != nil {
@@ -347,7 +347,7 @@ func OverlayCmd(items []CmdItem) templ.Component {
347347 var templ_7745c5c3_Var15 string
348348 templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(c.Kind)
349349 if templ_7745c5c3_Err != nil {
350- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 275}
350+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 130, Col: 271}
351351 }
352352 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
353353 if templ_7745c5c3_Err != nil {
@@ -371,7 +371,30 @@ func OverlayCmd(items []CmdItem) templ.Component {
371371 if templ_7745c5c3_Err != nil {
372372 return templ_7745c5c3_Err
373373 }
374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" role=\"option\" aria-selected=\"false\" data-k=\"action turn single-key shortcuts on off keyboard\"><a href=\"#\" data-fire=\"keys\" tabindex=\"-1\"><span class=\"lbl\">turn single-key shortcuts off</span><span class=\"k\">action</span></a></li></ul><p class=\"panel-foot\"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p></dialog>")
374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" role=\"option\" aria-selected=\"")
375+ if templ_7745c5c3_Err != nil {
376+ return templ_7745c5c3_Err
377+ }
378+ var templ_7745c5c3_Var17 string
379+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(len(items) == 0))
380+ if templ_7745c5c3_Err != nil {
381+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/overlays.templ`, Line: 132, Col: 106}
382+ }
383+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
384+ if templ_7745c5c3_Err != nil {
385+ return templ_7745c5c3_Err
386+ }
387+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"")
388+ if templ_7745c5c3_Err != nil {
389+ return templ_7745c5c3_Err
390+ }
391+ if len(items) == 0 {
392+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " data-cursor")
393+ if templ_7745c5c3_Err != nil {
394+ return templ_7745c5c3_Err
395+ }
396+ }
397+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " data-k=\"action turn single-key shortcuts on off keyboard\"><a role=\"presentation\" data-fire=\"keys\"><span class=\"lbl\">turn single-key shortcuts off</span><span class=\"k\">action</span></a></li></ul><p class=\"panel-foot\"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p></dialog>")
375398 if templ_7745c5c3_Err != nil {
376399 return templ_7745c5c3_Err
377400 }
M internal/adapters/web/shell.templ +4 -4
@@ -207,10 +207,10 @@ templ Footer(l Layout) {
207207 // move, the selected lines of a file, a flash message (permalink copied) and
208208 // a lost event stream. Empty, it takes no room.
209209 templ Hint(l Layout) {
210- <div class="hint" role="status" aria-live="polite">
211- <span class="hint-nav" :text="document.body.dataset.nav || ''"></span>
212- <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"></span>
213- <span class="hint-flash" :text="document.body.dataset.flash || ''"></span>
210+ <div class="hint">
211+ <span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''"></span>
212+ <span role="status" aria-live="polite"><span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''"></span>
213+ <span class="hint-flash" :text="document.body.dataset.flash || ''"></span></span>
214214 @Live(l)
215215 </div>
216216 }
M internal/adapters/web/shell_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/static/css/20-shell.css +2 -1
@@ -204,12 +204,13 @@
204204 background: var(--bg-overlay); border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: var(--shadow);
205205 pointer-events: none;
206206 & > :empty { display: none; }
207+ & > [role="status"] { display: contents; }
207208 & .hint-live { color: var(--red); }
208209 }
209210 /* empty, the badge is out of sight but stays a status region (display: none
210211 would drop it from the accessibility tree and its next message would not be
211212 announced) */
212- .hint:not(:has(> :not(:empty))) {
213+ .hint:not(:has(.hint-nav:not(:empty), .hint-sel:not(:empty), .hint-flash:not(:empty), .hint-live:not(:empty))) {
213214 width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); box-shadow: none;
214215 }
215216
M internal/adapters/web/static/css/60-mr.css +5 -8
@@ -17,22 +17,19 @@
1717 font-size: var(--fs-s);
1818 /* the tabs are buttons: a thin border, the count in a darker badge;
1919 the open one in the teal accent */
20- & nav { display: flex; flex-wrap: wrap; gap: var(--s2); }
21- & [data-mr-tabs] a {
20+ & [data-mr-tabs] { display: flex; flex-wrap: wrap; gap: var(--s2); }
21+ & [data-mr-tabs] button {
2222 display: inline-flex; align-items: center; gap: var(--s1);
2323 padding: 2px var(--s2); border: 1px solid var(--border-strong); border-radius: var(--r-s);
2424 color: var(--ink-dim); text-decoration: none; cursor: pointer;
2525 }
2626 & [data-checks-label] { color: var(--ink-dim); font-size: var(--fs-xs); }
2727 & [data-mr-tabs] .count { margin-left: 0; color: var(--ink-hi); background: var(--bg-canvas); box-shadow: inset 0 0 0 1px var(--border); }
28- & [data-mr-tabs] a:hover { background: var(--bg-strong); color: var(--ink-hi); }
29- & [data-mr-tabs] a[aria-expanded="true"] { border-color: var(--teal); color: var(--teal); background: var(--bg-surface); }
28+ & [data-mr-tabs] button:hover { background: var(--bg-strong); color: var(--ink-hi); }
29+ & [data-mr-tabs] button[aria-expanded="true"] { border-color: var(--teal); color: var(--teal); background: var(--bg-surface); }
3030 & [data-mr-panel] { padding-block-start: var(--s2); overflow-x: auto; }
3131 & [hidden] { display: none; }
32- /* Until mr.js drives the panels (data-panels, then [hidden]), only the
33- panel of the URL fragment shows: the tabs work as anchors without
34- scripts, and no panel flashes open before the script runs. */
35- &:not([data-panels]) [data-mr-panel]:not(:target, :has(:target)) { display: none; }
32+ /* Without scripts both panels remain visible; disclosure controls use data-js. */
3633 & section h2 { font-size: var(--fs-s); color: var(--ink-dim); }
3734 & table { width: 100%; border-collapse: collapse; }
3835 & th, & td { text-align: left; padding: var(--s2); border-bottom: 1px solid var(--border); }
M internal/adapters/web/static/css/85-mobile.css +1 -1
@@ -48,6 +48,6 @@
4848 #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); }
4949 body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); }
5050 #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); }
51- .mr-footer [data-mr-tabs] a { display: inline-flex; align-items: center; min-height: 40px; }
51+ .mr-footer [data-mr-tabs] button { display: inline-flex; align-items: center; min-height: 40px; }
5252 }
5353 }
M internal/adapters/web/static/js/ermite.js +15 -0
@@ -134,7 +134,12 @@
134134 dialog(id, focusId) {
135135 const d = document.getElementById(id);
136136 if (!d) return;
137+ const opener = document.activeElement;
137138 ermite.closeDialogs();
139+ d.addEventListener('close', () => {
140+ // close events are queued: a replacement dialog may already be open.
141+ if (!document.querySelector('dialog[open]') && opener?.isConnected) opener.focus();
142+ }, { once: true });
138143 d.showModal();
139144 if (id === 'panel-cmd') window.ermiteCmd?.sync();
140145 if (focusId) document.getElementById(focusId)?.focus();
@@ -672,6 +677,16 @@
672677 const menu = el.closest('details.menu');
673678 if (menu) closeMenu(menu);
674679 });
680+ // Listbox options own the interaction; their anchors are presentation only.
681+ // Keyboard activation also clicks this wrapper (palette, finder and j/k).
682+ addEventListener('click', (e) => {
683+ const target = e.target.closest?.('a[data-href]');
684+ const list = target?.closest?.('[role="listbox"]');
685+ if (!list || !['cmd-list', 'finder-list'].includes(list.id) || e.button) return;
686+ e.preventDefault();
687+ location.assign(target.dataset.href);
688+ });
689+
675690 // A click outside an open menu closes it.
676691 addEventListener('click', (e) => {
677692 for (const d of $$('details.menu[open]')) if (!d.contains(e.target)) d.open = false;
M internal/adapters/web/static/js/files.js +4 -3
@@ -635,12 +635,12 @@ window.ermiteFinder = (() => {
635635 return lbl;
636636 }
637637
638- // hitLink is the link of path i of the result: its label, out of the tab order.
638+ // hitLink is the presentation wrapper of path i; the option owns activation.
639639 function hitLink(res, i) {
640640 const path = res.idx.paths[i];
641641 const a = document.createElement('a');
642- a.href = blobHref(path);
643- a.tabIndex = -1;
642+ a.dataset.href = blobHref(path);
643+ a.setAttribute('role', 'presentation');
644644 a.append(label(path, positions(res.idx, i, res.q)));
645645 return a;
646646 }
@@ -684,6 +684,7 @@ window.ermiteFinder = (() => {
684684
685685 // showTree: no query, the directory again, with j/k and Enter on its rows.
686686 function showTree(el) {
687+ status('');
687688 rendered = null;
688689 pendingEnter = null;
689690 $('finder').hidden = true;
M internal/adapters/web/static/js/mr.js +3 -3
@@ -241,7 +241,7 @@ window.ermiteMR = (() => {
241241 reveal(id);
242242 }
243243
244- // Enhance the ordinary anchor navigation only when scripts are available.
244+ // Enhance the server-rendered disclosures when scripts are available.
245245 let disposePanels = () => {};
246246 // The tabs are disclosure buttons (aria-expanded + aria-controls), not a
247247 // tablist: a tablist always has a selected tab, and here every panel can be
@@ -250,7 +250,7 @@ window.ermiteMR = (() => {
250250 disposePanels();
251251 const footer = $('.mr-footer'), nav = $('[data-mr-tabs]');
252252 if (!footer || !nav) return;
253- const tabs = $$('a', nav), panels = $$('[data-mr-panel]', footer);
253+ const tabs = $$('button', nav), panels = $$('[data-mr-panel]', footer);
254254 const ids = new Set(['#mr-commits', '#mr-checks', '#mr-checks-panel']);
255255 let current = -1;
256256 // show applies the state; fromUser also keeps the URL fragment in step.
@@ -522,7 +522,7 @@ window.ermiteMR = (() => {
522522 const files = new Map($$('#diffs details.dfile').map((f) => [f.dataset.file, f.open]));
523523 const folded = $('.r-files')?.hasAttribute('data-folded');
524524 const railY = $('.rail')?.scrollTop, filesY = $('.r-files .files')?.scrollTop;
525- const openPanel = $$('[data-mr-tabs] a').findIndex((t) => t.getAttribute('aria-expanded') === 'true');
525+ const openPanel = $$('[data-mr-tabs] button').findIndex((t) => t.getAttribute('aria-expanded') === 'true');
526526 const active = document.activeElement;
527527 const focus = active && { file: active.closest?.('.files a')?.getAttribute('title'), region: regions.find((sel) => $(sel)?.contains(active)), menu: active.closest?.('details.menu')?.id, id: active.id, tag: active.tagName, name: active.getAttribute('name'), href: active.getAttribute('href') };
528528 for (const f of $$('#diffs details.dfile', page)) if (files.has(f.dataset.file)) f.open = files.get(f.dataset.file);
M internal/adapters/web/tree.templ +1 -1
@@ -28,8 +28,8 @@ templ Tree(l Layout, p TreePage) {
2828 </div>
2929 <p class="tree-keys" data-js aria-hidden="true"><kbd>j</kbd> <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p>
3030 </div>
31+ <p id="finder-status" class="dim" role="status"></p>
3132 <div id="finder" class="tree-card" hidden>
32- <p id="finder-status" class="dim" aria-live="polite"></p>
3333 <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files"></ul>
3434 </div>
3535 <div id="tree-view" class="tree-card">
M internal/adapters/web/tree_templ.go +1 -1 generated file: collapsed
M internal/app/testdata/web/blob-binary.html +19 -17
@@ -141,18 +141,20 @@
141141 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
142142 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
143143 </footer>
144-<div class="hint" role="status" aria-live="polite">
145-<span class="hint-nav" :text="document.body.dataset.nav || ''">
144+<div class="hint">
145+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
146146 </span>
147+<span role="status" aria-live="polite">
147148 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
148149 </span>
149150 <span class="hint-flash" :text="document.body.dataset.flash || ''">
150151 </span>
152+</span>
151153 </div>
152154 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
153155 <div class="panel-head">
154156 <h2>Keyboard shortcuts</h2>
155-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
157+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
156158 <kbd>esc</kbd> close</button>
157159 </div>
158160 <div class="keys-pref">
@@ -286,7 +288,7 @@
286288 <div class="panel-head">
287289 <label for="refs-filter" class="sr">filter branches and tags</label>
288290 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
289-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
291+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
290292 <kbd>esc</kbd>
291293 </button>
292294 </div>
@@ -321,79 +323,79 @@
321323 <div class="panel-head">
322324 <label for="cmd-q" class="sr">go to</label>
323325 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
324-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
326+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
325327 <kbd>esc</kbd>
326328 </button>
327329 </div>
328330 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
329331 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
330-<a href="/" tabindex="-1">
332+<a role="presentation" data-href="/">
331333 <span class="lbl">home</span>
332334 <span class="k">go</span>
333335 </a>
334336 </li>
335337 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
336-<a href="/files" tabindex="-1">
338+<a role="presentation" data-href="/files">
337339 <span class="lbl">summary</span>
338340 <span class="k">section</span>
339341 </a>
340342 </li>
341343 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
342-<a href="/files/tree/main" tabindex="-1">
344+<a role="presentation" data-href="/files/tree/main">
343345 <span class="lbl">files</span>
344346 <span class="k">section</span>
345347 </a>
346348 </li>
347349 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
348-<a href="/files/commits/main" tabindex="-1">
350+<a role="presentation" data-href="/files/commits/main">
349351 <span class="lbl">commits</span>
350352 <span class="k">section</span>
351353 </a>
352354 </li>
353355 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
354-<a href="/files/refs" tabindex="-1">
356+<a role="presentation" data-href="/files/refs">
355357 <span class="lbl">branches &amp; tags</span>
356358 <span class="k">section</span>
357359 </a>
358360 </li>
359361 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
360-<a href="/files/merge-requests" tabindex="-1">
362+<a role="presentation" data-href="/files/merge-requests">
361363 <span class="lbl">merge requests</span>
362364 <span class="k">section</span>
363365 </a>
364366 </li>
365367 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
366-<a href="/files/releases" tabindex="-1">
368+<a role="presentation" data-href="/files/releases">
367369 <span class="lbl">releases</span>
368370 <span class="k">section</span>
369371 </a>
370372 </li>
371373 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
372-<a href="/files/actions" tabindex="-1">
374+<a role="presentation" data-href="/files/actions">
373375 <span class="lbl">actions</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377379 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
378-<a href="/files/tree/feature/x" tabindex="-1">
380+<a role="presentation" data-href="/files/tree/feature/x">
379381 <span class="lbl">feature/x</span>
380382 <span class="k">branch</span>
381383 </a>
382384 </li>
383385 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
384-<a href="/files/tree/main" tabindex="-1">
386+<a role="presentation" data-href="/files/tree/main">
385387 <span class="lbl">main</span>
386388 <span class="k">branch</span>
387389 </a>
388390 </li>
389391 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
390-<a href="/files/tree/v1.0" tabindex="-1">
392+<a role="presentation" data-href="/files/tree/v1.0">
391393 <span class="lbl">v1.0</span>
392394 <span class="k">tag</span>
393395 </a>
394396 </li>
395397 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
396-<a href="#" data-fire="keys" tabindex="-1">
398+<a role="presentation" data-fire="keys">
397399 <span class="lbl">turn single-key shortcuts off</span>
398400 <span class="k">action</span>
399401 </a>
M internal/app/testdata/web/blob-code.html +19 -17
@@ -211,18 +211,20 @@
211211 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
212212 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
213213 </footer>
214-<div class="hint" role="status" aria-live="polite">
215-<span class="hint-nav" :text="document.body.dataset.nav || ''">
214+<div class="hint">
215+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
216216 </span>
217+<span role="status" aria-live="polite">
217218 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
218219 </span>
219220 <span class="hint-flash" :text="document.body.dataset.flash || ''">
220221 </span>
222+</span>
221223 </div>
222224 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
223225 <div class="panel-head">
224226 <h2>Keyboard shortcuts</h2>
225-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
227+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
226228 <kbd>esc</kbd> close</button>
227229 </div>
228230 <div class="keys-pref">
@@ -356,7 +358,7 @@
356358 <div class="panel-head">
357359 <label for="refs-filter" class="sr">filter branches and tags</label>
358360 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
359-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
361+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
360362 <kbd>esc</kbd>
361363 </button>
362364 </div>
@@ -391,79 +393,79 @@
391393 <div class="panel-head">
392394 <label for="cmd-q" class="sr">go to</label>
393395 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
394-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
396+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
395397 <kbd>esc</kbd>
396398 </button>
397399 </div>
398400 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
399401 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
400-<a href="/" tabindex="-1">
402+<a role="presentation" data-href="/">
401403 <span class="lbl">home</span>
402404 <span class="k">go</span>
403405 </a>
404406 </li>
405407 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
406-<a href="/files" tabindex="-1">
408+<a role="presentation" data-href="/files">
407409 <span class="lbl">summary</span>
408410 <span class="k">section</span>
409411 </a>
410412 </li>
411413 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
412-<a href="/files/tree/main" tabindex="-1">
414+<a role="presentation" data-href="/files/tree/main">
413415 <span class="lbl">files</span>
414416 <span class="k">section</span>
415417 </a>
416418 </li>
417419 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
418-<a href="/files/commits/main" tabindex="-1">
420+<a role="presentation" data-href="/files/commits/main">
419421 <span class="lbl">commits</span>
420422 <span class="k">section</span>
421423 </a>
422424 </li>
423425 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
424-<a href="/files/refs" tabindex="-1">
426+<a role="presentation" data-href="/files/refs">
425427 <span class="lbl">branches &amp; tags</span>
426428 <span class="k">section</span>
427429 </a>
428430 </li>
429431 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
430-<a href="/files/merge-requests" tabindex="-1">
432+<a role="presentation" data-href="/files/merge-requests">
431433 <span class="lbl">merge requests</span>
432434 <span class="k">section</span>
433435 </a>
434436 </li>
435437 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
436-<a href="/files/releases" tabindex="-1">
438+<a role="presentation" data-href="/files/releases">
437439 <span class="lbl">releases</span>
438440 <span class="k">section</span>
439441 </a>
440442 </li>
441443 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
442-<a href="/files/actions" tabindex="-1">
444+<a role="presentation" data-href="/files/actions">
443445 <span class="lbl">actions</span>
444446 <span class="k">section</span>
445447 </a>
446448 </li>
447449 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
448-<a href="/files/tree/feature/x" tabindex="-1">
450+<a role="presentation" data-href="/files/tree/feature/x">
449451 <span class="lbl">feature/x</span>
450452 <span class="k">branch</span>
451453 </a>
452454 </li>
453455 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
454-<a href="/files/tree/main" tabindex="-1">
456+<a role="presentation" data-href="/files/tree/main">
455457 <span class="lbl">main</span>
456458 <span class="k">branch</span>
457459 </a>
458460 </li>
459461 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
460-<a href="/files/tree/v1.0" tabindex="-1">
462+<a role="presentation" data-href="/files/tree/v1.0">
461463 <span class="lbl">v1.0</span>
462464 <span class="k">tag</span>
463465 </a>
464466 </li>
465467 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
466-<a href="#" data-fire="keys" tabindex="-1">
468+<a role="presentation" data-fire="keys">
467469 <span class="lbl">turn single-key shortcuts off</span>
468470 <span class="k">action</span>
469471 </a>
M internal/app/testdata/web/blob-empty.html +19 -17
@@ -141,18 +141,20 @@
141141 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
142142 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
143143 </footer>
144-<div class="hint" role="status" aria-live="polite">
145-<span class="hint-nav" :text="document.body.dataset.nav || ''">
144+<div class="hint">
145+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
146146 </span>
147+<span role="status" aria-live="polite">
147148 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
148149 </span>
149150 <span class="hint-flash" :text="document.body.dataset.flash || ''">
150151 </span>
152+</span>
151153 </div>
152154 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
153155 <div class="panel-head">
154156 <h2>Keyboard shortcuts</h2>
155-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
157+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
156158 <kbd>esc</kbd> close</button>
157159 </div>
158160 <div class="keys-pref">
@@ -286,7 +288,7 @@
286288 <div class="panel-head">
287289 <label for="refs-filter" class="sr">filter branches and tags</label>
288290 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
289-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
291+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
290292 <kbd>esc</kbd>
291293 </button>
292294 </div>
@@ -321,79 +323,79 @@
321323 <div class="panel-head">
322324 <label for="cmd-q" class="sr">go to</label>
323325 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
324-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
326+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
325327 <kbd>esc</kbd>
326328 </button>
327329 </div>
328330 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
329331 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
330-<a href="/" tabindex="-1">
332+<a role="presentation" data-href="/">
331333 <span class="lbl">home</span>
332334 <span class="k">go</span>
333335 </a>
334336 </li>
335337 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
336-<a href="/files" tabindex="-1">
338+<a role="presentation" data-href="/files">
337339 <span class="lbl">summary</span>
338340 <span class="k">section</span>
339341 </a>
340342 </li>
341343 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
342-<a href="/files/tree/main" tabindex="-1">
344+<a role="presentation" data-href="/files/tree/main">
343345 <span class="lbl">files</span>
344346 <span class="k">section</span>
345347 </a>
346348 </li>
347349 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
348-<a href="/files/commits/main" tabindex="-1">
350+<a role="presentation" data-href="/files/commits/main">
349351 <span class="lbl">commits</span>
350352 <span class="k">section</span>
351353 </a>
352354 </li>
353355 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
354-<a href="/files/refs" tabindex="-1">
356+<a role="presentation" data-href="/files/refs">
355357 <span class="lbl">branches &amp; tags</span>
356358 <span class="k">section</span>
357359 </a>
358360 </li>
359361 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
360-<a href="/files/merge-requests" tabindex="-1">
362+<a role="presentation" data-href="/files/merge-requests">
361363 <span class="lbl">merge requests</span>
362364 <span class="k">section</span>
363365 </a>
364366 </li>
365367 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
366-<a href="/files/releases" tabindex="-1">
368+<a role="presentation" data-href="/files/releases">
367369 <span class="lbl">releases</span>
368370 <span class="k">section</span>
369371 </a>
370372 </li>
371373 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
372-<a href="/files/actions" tabindex="-1">
374+<a role="presentation" data-href="/files/actions">
373375 <span class="lbl">actions</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377379 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
378-<a href="/files/tree/feature/x" tabindex="-1">
380+<a role="presentation" data-href="/files/tree/feature/x">
379381 <span class="lbl">feature/x</span>
380382 <span class="k">branch</span>
381383 </a>
382384 </li>
383385 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
384-<a href="/files/tree/main" tabindex="-1">
386+<a role="presentation" data-href="/files/tree/main">
385387 <span class="lbl">main</span>
386388 <span class="k">branch</span>
387389 </a>
388390 </li>
389391 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
390-<a href="/files/tree/v1.0" tabindex="-1">
392+<a role="presentation" data-href="/files/tree/v1.0">
391393 <span class="lbl">v1.0</span>
392394 <span class="k">tag</span>
393395 </a>
394396 </li>
395397 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
396-<a href="#" data-fire="keys" tabindex="-1">
398+<a role="presentation" data-fire="keys">
397399 <span class="lbl">turn single-key shortcuts off</span>
398400 <span class="k">action</span>
399401 </a>
M internal/app/testdata/web/blob-image.html +19 -17
@@ -143,18 +143,20 @@
143143 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
144144 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
145145 </footer>
146-<div class="hint" role="status" aria-live="polite">
147-<span class="hint-nav" :text="document.body.dataset.nav || ''">
146+<div class="hint">
147+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
148148 </span>
149+<span role="status" aria-live="polite">
149150 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
150151 </span>
151152 <span class="hint-flash" :text="document.body.dataset.flash || ''">
152153 </span>
154+</span>
153155 </div>
154156 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
155157 <div class="panel-head">
156158 <h2>Keyboard shortcuts</h2>
157-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
159+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
158160 <kbd>esc</kbd> close</button>
159161 </div>
160162 <div class="keys-pref">
@@ -288,7 +290,7 @@
288290 <div class="panel-head">
289291 <label for="refs-filter" class="sr">filter branches and tags</label>
290292 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
291-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
293+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
292294 <kbd>esc</kbd>
293295 </button>
294296 </div>
@@ -323,79 +325,79 @@
323325 <div class="panel-head">
324326 <label for="cmd-q" class="sr">go to</label>
325327 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
326-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
328+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
327329 <kbd>esc</kbd>
328330 </button>
329331 </div>
330332 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
331333 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
332-<a href="/" tabindex="-1">
334+<a role="presentation" data-href="/">
333335 <span class="lbl">home</span>
334336 <span class="k">go</span>
335337 </a>
336338 </li>
337339 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
338-<a href="/files" tabindex="-1">
340+<a role="presentation" data-href="/files">
339341 <span class="lbl">summary</span>
340342 <span class="k">section</span>
341343 </a>
342344 </li>
343345 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
344-<a href="/files/tree/main" tabindex="-1">
346+<a role="presentation" data-href="/files/tree/main">
345347 <span class="lbl">files</span>
346348 <span class="k">section</span>
347349 </a>
348350 </li>
349351 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
350-<a href="/files/commits/main" tabindex="-1">
352+<a role="presentation" data-href="/files/commits/main">
351353 <span class="lbl">commits</span>
352354 <span class="k">section</span>
353355 </a>
354356 </li>
355357 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
356-<a href="/files/refs" tabindex="-1">
358+<a role="presentation" data-href="/files/refs">
357359 <span class="lbl">branches &amp; tags</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361363 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
362-<a href="/files/merge-requests" tabindex="-1">
364+<a role="presentation" data-href="/files/merge-requests">
363365 <span class="lbl">merge requests</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367369 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
368-<a href="/files/releases" tabindex="-1">
370+<a role="presentation" data-href="/files/releases">
369371 <span class="lbl">releases</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373375 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
374-<a href="/files/actions" tabindex="-1">
376+<a role="presentation" data-href="/files/actions">
375377 <span class="lbl">actions</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
379381 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
380-<a href="/files/tree/feature/x" tabindex="-1">
382+<a role="presentation" data-href="/files/tree/feature/x">
381383 <span class="lbl">feature/x</span>
382384 <span class="k">branch</span>
383385 </a>
384386 </li>
385387 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
386-<a href="/files/tree/main" tabindex="-1">
388+<a role="presentation" data-href="/files/tree/main">
387389 <span class="lbl">main</span>
388390 <span class="k">branch</span>
389391 </a>
390392 </li>
391393 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
392-<a href="/files/tree/v1.0" tabindex="-1">
394+<a role="presentation" data-href="/files/tree/v1.0">
393395 <span class="lbl">v1.0</span>
394396 <span class="k">tag</span>
395397 </a>
396398 </li>
397399 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
398-<a href="#" data-fire="keys" tabindex="-1">
400+<a role="presentation" data-fire="keys">
399401 <span class="lbl">turn single-key shortcuts off</span>
400402 <span class="k">action</span>
401403 </a>
M internal/app/testdata/web/blob-markdown.html +19 -17
@@ -208,18 +208,20 @@
208208 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
209209 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
210210 </footer>
211-<div class="hint" role="status" aria-live="polite">
212-<span class="hint-nav" :text="document.body.dataset.nav || ''">
211+<div class="hint">
212+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
213213 </span>
214+<span role="status" aria-live="polite">
214215 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
215216 </span>
216217 <span class="hint-flash" :text="document.body.dataset.flash || ''">
217218 </span>
219+</span>
218220 </div>
219221 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
220222 <div class="panel-head">
221223 <h2>Keyboard shortcuts</h2>
222-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
224+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
223225 <kbd>esc</kbd> close</button>
224226 </div>
225227 <div class="keys-pref">
@@ -353,7 +355,7 @@
353355 <div class="panel-head">
354356 <label for="refs-filter" class="sr">filter branches and tags</label>
355357 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
356-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
358+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
357359 <kbd>esc</kbd>
358360 </button>
359361 </div>
@@ -388,79 +390,79 @@
388390 <div class="panel-head">
389391 <label for="cmd-q" class="sr">go to</label>
390392 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
391-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
393+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
392394 <kbd>esc</kbd>
393395 </button>
394396 </div>
395397 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
396398 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
397-<a href="/" tabindex="-1">
399+<a role="presentation" data-href="/">
398400 <span class="lbl">home</span>
399401 <span class="k">go</span>
400402 </a>
401403 </li>
402404 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
403-<a href="/files" tabindex="-1">
405+<a role="presentation" data-href="/files">
404406 <span class="lbl">summary</span>
405407 <span class="k">section</span>
406408 </a>
407409 </li>
408410 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
409-<a href="/files/tree/main" tabindex="-1">
411+<a role="presentation" data-href="/files/tree/main">
410412 <span class="lbl">files</span>
411413 <span class="k">section</span>
412414 </a>
413415 </li>
414416 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
415-<a href="/files/commits/main" tabindex="-1">
417+<a role="presentation" data-href="/files/commits/main">
416418 <span class="lbl">commits</span>
417419 <span class="k">section</span>
418420 </a>
419421 </li>
420422 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
421-<a href="/files/refs" tabindex="-1">
423+<a role="presentation" data-href="/files/refs">
422424 <span class="lbl">branches &amp; tags</span>
423425 <span class="k">section</span>
424426 </a>
425427 </li>
426428 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
427-<a href="/files/merge-requests" tabindex="-1">
429+<a role="presentation" data-href="/files/merge-requests">
428430 <span class="lbl">merge requests</span>
429431 <span class="k">section</span>
430432 </a>
431433 </li>
432434 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
433-<a href="/files/releases" tabindex="-1">
435+<a role="presentation" data-href="/files/releases">
434436 <span class="lbl">releases</span>
435437 <span class="k">section</span>
436438 </a>
437439 </li>
438440 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
439-<a href="/files/actions" tabindex="-1">
441+<a role="presentation" data-href="/files/actions">
440442 <span class="lbl">actions</span>
441443 <span class="k">section</span>
442444 </a>
443445 </li>
444446 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
445-<a href="/files/tree/feature/x" tabindex="-1">
447+<a role="presentation" data-href="/files/tree/feature/x">
446448 <span class="lbl">feature/x</span>
447449 <span class="k">branch</span>
448450 </a>
449451 </li>
450452 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
451-<a href="/files/tree/main" tabindex="-1">
453+<a role="presentation" data-href="/files/tree/main">
452454 <span class="lbl">main</span>
453455 <span class="k">branch</span>
454456 </a>
455457 </li>
456458 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
457-<a href="/files/tree/v1.0" tabindex="-1">
459+<a role="presentation" data-href="/files/tree/v1.0">
458460 <span class="lbl">v1.0</span>
459461 <span class="k">tag</span>
460462 </a>
461463 </li>
462464 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
463-<a href="#" data-fire="keys" tabindex="-1">
465+<a role="presentation" data-fire="keys">
464466 <span class="lbl">turn single-key shortcuts off</span>
465467 <span class="k">action</span>
466468 </a>
M internal/app/testdata/web/blob-svg.html +19 -17
@@ -166,18 +166,20 @@
166166 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
167167 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
168168 </footer>
169-<div class="hint" role="status" aria-live="polite">
170-<span class="hint-nav" :text="document.body.dataset.nav || ''">
169+<div class="hint">
170+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
171171 </span>
172+<span role="status" aria-live="polite">
172173 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
173174 </span>
174175 <span class="hint-flash" :text="document.body.dataset.flash || ''">
175176 </span>
177+</span>
176178 </div>
177179 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
178180 <div class="panel-head">
179181 <h2>Keyboard shortcuts</h2>
180-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
182+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
181183 <kbd>esc</kbd> close</button>
182184 </div>
183185 <div class="keys-pref">
@@ -311,7 +313,7 @@
311313 <div class="panel-head">
312314 <label for="refs-filter" class="sr">filter branches and tags</label>
313315 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
314-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
316+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
315317 <kbd>esc</kbd>
316318 </button>
317319 </div>
@@ -346,79 +348,79 @@
346348 <div class="panel-head">
347349 <label for="cmd-q" class="sr">go to</label>
348350 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
349-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
351+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
350352 <kbd>esc</kbd>
351353 </button>
352354 </div>
353355 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
354356 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
355-<a href="/" tabindex="-1">
357+<a role="presentation" data-href="/">
356358 <span class="lbl">home</span>
357359 <span class="k">go</span>
358360 </a>
359361 </li>
360362 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
361-<a href="/files" tabindex="-1">
363+<a role="presentation" data-href="/files">
362364 <span class="lbl">summary</span>
363365 <span class="k">section</span>
364366 </a>
365367 </li>
366368 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
367-<a href="/files/tree/main" tabindex="-1">
369+<a role="presentation" data-href="/files/tree/main">
368370 <span class="lbl">files</span>
369371 <span class="k">section</span>
370372 </a>
371373 </li>
372374 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
373-<a href="/files/commits/main" tabindex="-1">
375+<a role="presentation" data-href="/files/commits/main">
374376 <span class="lbl">commits</span>
375377 <span class="k">section</span>
376378 </a>
377379 </li>
378380 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
379-<a href="/files/refs" tabindex="-1">
381+<a role="presentation" data-href="/files/refs">
380382 <span class="lbl">branches &amp; tags</span>
381383 <span class="k">section</span>
382384 </a>
383385 </li>
384386 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
385-<a href="/files/merge-requests" tabindex="-1">
387+<a role="presentation" data-href="/files/merge-requests">
386388 <span class="lbl">merge requests</span>
387389 <span class="k">section</span>
388390 </a>
389391 </li>
390392 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
391-<a href="/files/releases" tabindex="-1">
393+<a role="presentation" data-href="/files/releases">
392394 <span class="lbl">releases</span>
393395 <span class="k">section</span>
394396 </a>
395397 </li>
396398 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
397-<a href="/files/actions" tabindex="-1">
399+<a role="presentation" data-href="/files/actions">
398400 <span class="lbl">actions</span>
399401 <span class="k">section</span>
400402 </a>
401403 </li>
402404 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
403-<a href="/files/tree/feature/x" tabindex="-1">
405+<a role="presentation" data-href="/files/tree/feature/x">
404406 <span class="lbl">feature/x</span>
405407 <span class="k">branch</span>
406408 </a>
407409 </li>
408410 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
409-<a href="/files/tree/main" tabindex="-1">
411+<a role="presentation" data-href="/files/tree/main">
410412 <span class="lbl">main</span>
411413 <span class="k">branch</span>
412414 </a>
413415 </li>
414416 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
415-<a href="/files/tree/v1.0" tabindex="-1">
417+<a role="presentation" data-href="/files/tree/v1.0">
416418 <span class="lbl">v1.0</span>
417419 <span class="k">tag</span>
418420 </a>
419421 </li>
420422 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
421-<a href="#" data-fire="keys" tabindex="-1">
423+<a role="presentation" data-fire="keys">
422424 <span class="lbl">turn single-key shortcuts off</span>
423425 <span class="k">action</span>
424426 </a>
M internal/app/testdata/web/blob-text.html +19 -17
@@ -161,18 +161,20 @@
161161 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
162162 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
163163 </footer>
164-<div class="hint" role="status" aria-live="polite">
165-<span class="hint-nav" :text="document.body.dataset.nav || ''">
164+<div class="hint">
165+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
166166 </span>
167+<span role="status" aria-live="polite">
167168 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
168169 </span>
169170 <span class="hint-flash" :text="document.body.dataset.flash || ''">
170171 </span>
172+</span>
171173 </div>
172174 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
173175 <div class="panel-head">
174176 <h2>Keyboard shortcuts</h2>
175-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
177+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
176178 <kbd>esc</kbd> close</button>
177179 </div>
178180 <div class="keys-pref">
@@ -306,7 +308,7 @@
306308 <div class="panel-head">
307309 <label for="refs-filter" class="sr">filter branches and tags</label>
308310 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
309-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
311+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
310312 <kbd>esc</kbd>
311313 </button>
312314 </div>
@@ -341,79 +343,79 @@
341343 <div class="panel-head">
342344 <label for="cmd-q" class="sr">go to</label>
343345 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
344-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
346+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
345347 <kbd>esc</kbd>
346348 </button>
347349 </div>
348350 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
349351 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
350-<a href="/" tabindex="-1">
352+<a role="presentation" data-href="/">
351353 <span class="lbl">home</span>
352354 <span class="k">go</span>
353355 </a>
354356 </li>
355357 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
356-<a href="/files" tabindex="-1">
358+<a role="presentation" data-href="/files">
357359 <span class="lbl">summary</span>
358360 <span class="k">section</span>
359361 </a>
360362 </li>
361363 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
362-<a href="/files/tree/main" tabindex="-1">
364+<a role="presentation" data-href="/files/tree/main">
363365 <span class="lbl">files</span>
364366 <span class="k">section</span>
365367 </a>
366368 </li>
367369 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
368-<a href="/files/commits/main" tabindex="-1">
370+<a role="presentation" data-href="/files/commits/main">
369371 <span class="lbl">commits</span>
370372 <span class="k">section</span>
371373 </a>
372374 </li>
373375 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
374-<a href="/files/refs" tabindex="-1">
376+<a role="presentation" data-href="/files/refs">
375377 <span class="lbl">branches &amp; tags</span>
376378 <span class="k">section</span>
377379 </a>
378380 </li>
379381 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
380-<a href="/files/merge-requests" tabindex="-1">
382+<a role="presentation" data-href="/files/merge-requests">
381383 <span class="lbl">merge requests</span>
382384 <span class="k">section</span>
383385 </a>
384386 </li>
385387 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
386-<a href="/files/releases" tabindex="-1">
388+<a role="presentation" data-href="/files/releases">
387389 <span class="lbl">releases</span>
388390 <span class="k">section</span>
389391 </a>
390392 </li>
391393 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
392-<a href="/files/actions" tabindex="-1">
394+<a role="presentation" data-href="/files/actions">
393395 <span class="lbl">actions</span>
394396 <span class="k">section</span>
395397 </a>
396398 </li>
397399 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
398-<a href="/files/tree/feature/x" tabindex="-1">
400+<a role="presentation" data-href="/files/tree/feature/x">
399401 <span class="lbl">feature/x</span>
400402 <span class="k">branch</span>
401403 </a>
402404 </li>
403405 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
404-<a href="/files/tree/main" tabindex="-1">
406+<a role="presentation" data-href="/files/tree/main">
405407 <span class="lbl">main</span>
406408 <span class="k">branch</span>
407409 </a>
408410 </li>
409411 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
410-<a href="/files/tree/v1.0" tabindex="-1">
412+<a role="presentation" data-href="/files/tree/v1.0">
411413 <span class="lbl">v1.0</span>
412414 <span class="k">tag</span>
413415 </a>
414416 </li>
415417 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
416-<a href="#" data-fire="keys" tabindex="-1">
418+<a role="presentation" data-fire="keys">
417419 <span class="lbl">turn single-key shortcuts off</span>
418420 <span class="k">action</span>
419421 </a>
M internal/app/testdata/web/blob-too-large.html +19 -17
@@ -141,18 +141,20 @@
141141 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
142142 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
143143 </footer>
144-<div class="hint" role="status" aria-live="polite">
145-<span class="hint-nav" :text="document.body.dataset.nav || ''">
144+<div class="hint">
145+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
146146 </span>
147+<span role="status" aria-live="polite">
147148 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
148149 </span>
149150 <span class="hint-flash" :text="document.body.dataset.flash || ''">
150151 </span>
152+</span>
151153 </div>
152154 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
153155 <div class="panel-head">
154156 <h2>Keyboard shortcuts</h2>
155-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
157+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
156158 <kbd>esc</kbd> close</button>
157159 </div>
158160 <div class="keys-pref">
@@ -286,7 +288,7 @@
286288 <div class="panel-head">
287289 <label for="refs-filter" class="sr">filter branches and tags</label>
288290 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
289-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
291+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
290292 <kbd>esc</kbd>
291293 </button>
292294 </div>
@@ -321,79 +323,79 @@
321323 <div class="panel-head">
322324 <label for="cmd-q" class="sr">go to</label>
323325 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
324-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
326+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
325327 <kbd>esc</kbd>
326328 </button>
327329 </div>
328330 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
329331 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
330-<a href="/" tabindex="-1">
332+<a role="presentation" data-href="/">
331333 <span class="lbl">home</span>
332334 <span class="k">go</span>
333335 </a>
334336 </li>
335337 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
336-<a href="/files" tabindex="-1">
338+<a role="presentation" data-href="/files">
337339 <span class="lbl">summary</span>
338340 <span class="k">section</span>
339341 </a>
340342 </li>
341343 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
342-<a href="/files/tree/main" tabindex="-1">
344+<a role="presentation" data-href="/files/tree/main">
343345 <span class="lbl">files</span>
344346 <span class="k">section</span>
345347 </a>
346348 </li>
347349 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
348-<a href="/files/commits/main" tabindex="-1">
350+<a role="presentation" data-href="/files/commits/main">
349351 <span class="lbl">commits</span>
350352 <span class="k">section</span>
351353 </a>
352354 </li>
353355 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
354-<a href="/files/refs" tabindex="-1">
356+<a role="presentation" data-href="/files/refs">
355357 <span class="lbl">branches &amp; tags</span>
356358 <span class="k">section</span>
357359 </a>
358360 </li>
359361 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
360-<a href="/files/merge-requests" tabindex="-1">
362+<a role="presentation" data-href="/files/merge-requests">
361363 <span class="lbl">merge requests</span>
362364 <span class="k">section</span>
363365 </a>
364366 </li>
365367 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
366-<a href="/files/releases" tabindex="-1">
368+<a role="presentation" data-href="/files/releases">
367369 <span class="lbl">releases</span>
368370 <span class="k">section</span>
369371 </a>
370372 </li>
371373 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
372-<a href="/files/actions" tabindex="-1">
374+<a role="presentation" data-href="/files/actions">
373375 <span class="lbl">actions</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377379 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
378-<a href="/files/tree/feature/x" tabindex="-1">
380+<a role="presentation" data-href="/files/tree/feature/x">
379381 <span class="lbl">feature/x</span>
380382 <span class="k">branch</span>
381383 </a>
382384 </li>
383385 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
384-<a href="/files/tree/main" tabindex="-1">
386+<a role="presentation" data-href="/files/tree/main">
385387 <span class="lbl">main</span>
386388 <span class="k">branch</span>
387389 </a>
388390 </li>
389391 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
390-<a href="/files/tree/v1.0" tabindex="-1">
392+<a role="presentation" data-href="/files/tree/v1.0">
391393 <span class="lbl">v1.0</span>
392394 <span class="k">tag</span>
393395 </a>
394396 </li>
395397 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
396-<a href="#" data-fire="keys" tabindex="-1">
398+<a role="presentation" data-fire="keys">
397399 <span class="lbl">turn single-key shortcuts off</span>
398400 <span class="k">action</span>
399401 </a>
M internal/app/testdata/web/blob-trojan.html +19 -17
@@ -208,18 +208,20 @@
208208 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
209209 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
210210 </footer>
211-<div class="hint" role="status" aria-live="polite">
212-<span class="hint-nav" :text="document.body.dataset.nav || ''">
211+<div class="hint">
212+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
213213 </span>
214+<span role="status" aria-live="polite">
214215 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
215216 </span>
216217 <span class="hint-flash" :text="document.body.dataset.flash || ''">
217218 </span>
219+</span>
218220 </div>
219221 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
220222 <div class="panel-head">
221223 <h2>Keyboard shortcuts</h2>
222-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
224+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
223225 <kbd>esc</kbd> close</button>
224226 </div>
225227 <div class="keys-pref">
@@ -353,7 +355,7 @@
353355 <div class="panel-head">
354356 <label for="refs-filter" class="sr">filter branches and tags</label>
355357 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
356-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
358+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
357359 <kbd>esc</kbd>
358360 </button>
359361 </div>
@@ -388,79 +390,79 @@
388390 <div class="panel-head">
389391 <label for="cmd-q" class="sr">go to</label>
390392 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
391-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
393+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
392394 <kbd>esc</kbd>
393395 </button>
394396 </div>
395397 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
396398 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
397-<a href="/" tabindex="-1">
399+<a role="presentation" data-href="/">
398400 <span class="lbl">home</span>
399401 <span class="k">go</span>
400402 </a>
401403 </li>
402404 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
403-<a href="/files" tabindex="-1">
405+<a role="presentation" data-href="/files">
404406 <span class="lbl">summary</span>
405407 <span class="k">section</span>
406408 </a>
407409 </li>
408410 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
409-<a href="/files/tree/main" tabindex="-1">
411+<a role="presentation" data-href="/files/tree/main">
410412 <span class="lbl">files</span>
411413 <span class="k">section</span>
412414 </a>
413415 </li>
414416 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
415-<a href="/files/commits/main" tabindex="-1">
417+<a role="presentation" data-href="/files/commits/main">
416418 <span class="lbl">commits</span>
417419 <span class="k">section</span>
418420 </a>
419421 </li>
420422 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
421-<a href="/files/refs" tabindex="-1">
423+<a role="presentation" data-href="/files/refs">
422424 <span class="lbl">branches &amp; tags</span>
423425 <span class="k">section</span>
424426 </a>
425427 </li>
426428 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
427-<a href="/files/merge-requests" tabindex="-1">
429+<a role="presentation" data-href="/files/merge-requests">
428430 <span class="lbl">merge requests</span>
429431 <span class="k">section</span>
430432 </a>
431433 </li>
432434 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
433-<a href="/files/releases" tabindex="-1">
435+<a role="presentation" data-href="/files/releases">
434436 <span class="lbl">releases</span>
435437 <span class="k">section</span>
436438 </a>
437439 </li>
438440 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
439-<a href="/files/actions" tabindex="-1">
441+<a role="presentation" data-href="/files/actions">
440442 <span class="lbl">actions</span>
441443 <span class="k">section</span>
442444 </a>
443445 </li>
444446 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
445-<a href="/files/tree/feature/x" tabindex="-1">
447+<a role="presentation" data-href="/files/tree/feature/x">
446448 <span class="lbl">feature/x</span>
447449 <span class="k">branch</span>
448450 </a>
449451 </li>
450452 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
451-<a href="/files/tree/main" tabindex="-1">
453+<a role="presentation" data-href="/files/tree/main">
452454 <span class="lbl">main</span>
453455 <span class="k">branch</span>
454456 </a>
455457 </li>
456458 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
457-<a href="/files/tree/v1.0" tabindex="-1">
459+<a role="presentation" data-href="/files/tree/v1.0">
458460 <span class="lbl">v1.0</span>
459461 <span class="k">tag</span>
460462 </a>
461463 </li>
462464 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
463-<a href="#" data-fire="keys" tabindex="-1">
465+<a role="presentation" data-fire="keys">
464466 <span class="lbl">turn single-key shortcuts off</span>
465467 <span class="k">action</span>
466468 </a>
M internal/app/testdata/web/commit-big.html +19 -17
@@ -193,18 +193,20 @@
193193 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
194194 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
195195 </footer>
196-<div class="hint" role="status" aria-live="polite">
197-<span class="hint-nav" :text="document.body.dataset.nav || ''">
196+<div class="hint">
197+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
198198 </span>
199+<span role="status" aria-live="polite">
199200 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
200201 </span>
201202 <span class="hint-flash" :text="document.body.dataset.flash || ''">
202203 </span>
204+</span>
203205 </div>
204206 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
205207 <div class="panel-head">
206208 <h2>Keyboard shortcuts</h2>
207-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
209+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
208210 <kbd>esc</kbd> close</button>
209211 </div>
210212 <div class="keys-pref">
@@ -338,7 +340,7 @@
338340 <div class="panel-head">
339341 <label for="refs-filter" class="sr">filter branches and tags</label>
340342 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
341-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
343+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
342344 <kbd>esc</kbd>
343345 </button>
344346 </div>
@@ -373,79 +375,79 @@
373375 <div class="panel-head">
374376 <label for="cmd-q" class="sr">go to</label>
375377 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
376-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
378+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
377379 <kbd>esc</kbd>
378380 </button>
379381 </div>
380382 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
381383 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
382-<a href="/" tabindex="-1">
384+<a role="presentation" data-href="/">
383385 <span class="lbl">home</span>
384386 <span class="k">go</span>
385387 </a>
386388 </li>
387389 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
388-<a href="/diffs" tabindex="-1">
390+<a role="presentation" data-href="/diffs">
389391 <span class="lbl">summary</span>
390392 <span class="k">section</span>
391393 </a>
392394 </li>
393395 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
394-<a href="/diffs/tree/main" tabindex="-1">
396+<a role="presentation" data-href="/diffs/tree/main">
395397 <span class="lbl">files</span>
396398 <span class="k">section</span>
397399 </a>
398400 </li>
399401 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
400-<a href="/diffs/commits/main" tabindex="-1">
402+<a role="presentation" data-href="/diffs/commits/main">
401403 <span class="lbl">commits</span>
402404 <span class="k">section</span>
403405 </a>
404406 </li>
405407 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
406-<a href="/diffs/refs" tabindex="-1">
408+<a role="presentation" data-href="/diffs/refs">
407409 <span class="lbl">branches &amp; tags</span>
408410 <span class="k">section</span>
409411 </a>
410412 </li>
411413 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
412-<a href="/diffs/merge-requests" tabindex="-1">
414+<a role="presentation" data-href="/diffs/merge-requests">
413415 <span class="lbl">merge requests</span>
414416 <span class="k">section</span>
415417 </a>
416418 </li>
417419 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
418-<a href="/diffs/releases" tabindex="-1">
420+<a role="presentation" data-href="/diffs/releases">
419421 <span class="lbl">releases</span>
420422 <span class="k">section</span>
421423 </a>
422424 </li>
423425 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
424-<a href="/diffs/actions" tabindex="-1">
426+<a role="presentation" data-href="/diffs/actions">
425427 <span class="lbl">actions</span>
426428 <span class="k">section</span>
427429 </a>
428430 </li>
429431 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
430-<a href="/diffs/tree/main" tabindex="-1">
432+<a role="presentation" data-href="/diffs/tree/main">
431433 <span class="lbl">main</span>
432434 <span class="k">branch</span>
433435 </a>
434436 </li>
435437 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
436-<a href="/diffs/tree/side" tabindex="-1">
438+<a role="presentation" data-href="/diffs/tree/side">
437439 <span class="lbl">side</span>
438440 <span class="k">branch</span>
439441 </a>
440442 </li>
441443 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
442-<a href="/diffs/tree/v1.0.0" tabindex="-1">
444+<a role="presentation" data-href="/diffs/tree/v1.0.0">
443445 <span class="lbl">v1.0.0</span>
444446 <span class="k">tag</span>
445447 </a>
446448 </li>
447449 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
448-<a href="#" data-fire="keys" tabindex="-1">
450+<a role="presentation" data-fire="keys">
449451 <span class="lbl">turn single-key shortcuts off</span>
450452 <span class="k">action</span>
451453 </a>
M internal/app/testdata/web/commit-binary.html +19 -17
@@ -184,18 +184,20 @@
184184 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
185185 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
186186 </footer>
187-<div class="hint" role="status" aria-live="polite">
188-<span class="hint-nav" :text="document.body.dataset.nav || ''">
187+<div class="hint">
188+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
189189 </span>
190+<span role="status" aria-live="polite">
190191 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
191192 </span>
192193 <span class="hint-flash" :text="document.body.dataset.flash || ''">
193194 </span>
195+</span>
194196 </div>
195197 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
196198 <div class="panel-head">
197199 <h2>Keyboard shortcuts</h2>
198-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
200+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
199201 <kbd>esc</kbd> close</button>
200202 </div>
201203 <div class="keys-pref">
@@ -329,7 +331,7 @@
329331 <div class="panel-head">
330332 <label for="refs-filter" class="sr">filter branches and tags</label>
331333 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
332-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
334+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
333335 <kbd>esc</kbd>
334336 </button>
335337 </div>
@@ -364,79 +366,79 @@
364366 <div class="panel-head">
365367 <label for="cmd-q" class="sr">go to</label>
366368 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
367-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
369+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
368370 <kbd>esc</kbd>
369371 </button>
370372 </div>
371373 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
372374 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
373-<a href="/" tabindex="-1">
375+<a role="presentation" data-href="/">
374376 <span class="lbl">home</span>
375377 <span class="k">go</span>
376378 </a>
377379 </li>
378380 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
379-<a href="/diffs" tabindex="-1">
381+<a role="presentation" data-href="/diffs">
380382 <span class="lbl">summary</span>
381383 <span class="k">section</span>
382384 </a>
383385 </li>
384386 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
385-<a href="/diffs/tree/main" tabindex="-1">
387+<a role="presentation" data-href="/diffs/tree/main">
386388 <span class="lbl">files</span>
387389 <span class="k">section</span>
388390 </a>
389391 </li>
390392 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
391-<a href="/diffs/commits/main" tabindex="-1">
393+<a role="presentation" data-href="/diffs/commits/main">
392394 <span class="lbl">commits</span>
393395 <span class="k">section</span>
394396 </a>
395397 </li>
396398 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
397-<a href="/diffs/refs" tabindex="-1">
399+<a role="presentation" data-href="/diffs/refs">
398400 <span class="lbl">branches &amp; tags</span>
399401 <span class="k">section</span>
400402 </a>
401403 </li>
402404 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
403-<a href="/diffs/merge-requests" tabindex="-1">
405+<a role="presentation" data-href="/diffs/merge-requests">
404406 <span class="lbl">merge requests</span>
405407 <span class="k">section</span>
406408 </a>
407409 </li>
408410 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
409-<a href="/diffs/releases" tabindex="-1">
411+<a role="presentation" data-href="/diffs/releases">
410412 <span class="lbl">releases</span>
411413 <span class="k">section</span>
412414 </a>
413415 </li>
414416 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
415-<a href="/diffs/actions" tabindex="-1">
417+<a role="presentation" data-href="/diffs/actions">
416418 <span class="lbl">actions</span>
417419 <span class="k">section</span>
418420 </a>
419421 </li>
420422 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
421-<a href="/diffs/tree/main" tabindex="-1">
423+<a role="presentation" data-href="/diffs/tree/main">
422424 <span class="lbl">main</span>
423425 <span class="k">branch</span>
424426 </a>
425427 </li>
426428 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
427-<a href="/diffs/tree/side" tabindex="-1">
429+<a role="presentation" data-href="/diffs/tree/side">
428430 <span class="lbl">side</span>
429431 <span class="k">branch</span>
430432 </a>
431433 </li>
432434 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
433-<a href="/diffs/tree/v1.0.0" tabindex="-1">
435+<a role="presentation" data-href="/diffs/tree/v1.0.0">
434436 <span class="lbl">v1.0.0</span>
435437 <span class="k">tag</span>
436438 </a>
437439 </li>
438440 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
439-<a href="#" data-fire="keys" tabindex="-1">
441+<a role="presentation" data-fire="keys">
440442 <span class="lbl">turn single-key shortcuts off</span>
441443 <span class="k">action</span>
442444 </a>
M internal/app/testdata/web/commit-merge.html +19 -17
@@ -226,18 +226,20 @@
226226 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
227227 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
228228 </footer>
229-<div class="hint" role="status" aria-live="polite">
230-<span class="hint-nav" :text="document.body.dataset.nav || ''">
229+<div class="hint">
230+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
231231 </span>
232+<span role="status" aria-live="polite">
232233 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
233234 </span>
234235 <span class="hint-flash" :text="document.body.dataset.flash || ''">
235236 </span>
237+</span>
236238 </div>
237239 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
238240 <div class="panel-head">
239241 <h2>Keyboard shortcuts</h2>
240-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
242+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
241243 <kbd>esc</kbd> close</button>
242244 </div>
243245 <div class="keys-pref">
@@ -371,7 +373,7 @@
371373 <div class="panel-head">
372374 <label for="refs-filter" class="sr">filter branches and tags</label>
373375 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
374-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
376+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
375377 <kbd>esc</kbd>
376378 </button>
377379 </div>
@@ -406,79 +408,79 @@
406408 <div class="panel-head">
407409 <label for="cmd-q" class="sr">go to</label>
408410 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
409-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
411+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
410412 <kbd>esc</kbd>
411413 </button>
412414 </div>
413415 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
414416 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
415-<a href="/" tabindex="-1">
417+<a role="presentation" data-href="/">
416418 <span class="lbl">home</span>
417419 <span class="k">go</span>
418420 </a>
419421 </li>
420422 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
421-<a href="/diffs" tabindex="-1">
423+<a role="presentation" data-href="/diffs">
422424 <span class="lbl">summary</span>
423425 <span class="k">section</span>
424426 </a>
425427 </li>
426428 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
427-<a href="/diffs/tree/main" tabindex="-1">
429+<a role="presentation" data-href="/diffs/tree/main">
428430 <span class="lbl">files</span>
429431 <span class="k">section</span>
430432 </a>
431433 </li>
432434 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
433-<a href="/diffs/commits/main" tabindex="-1">
435+<a role="presentation" data-href="/diffs/commits/main">
434436 <span class="lbl">commits</span>
435437 <span class="k">section</span>
436438 </a>
437439 </li>
438440 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
439-<a href="/diffs/refs" tabindex="-1">
441+<a role="presentation" data-href="/diffs/refs">
440442 <span class="lbl">branches &amp; tags</span>
441443 <span class="k">section</span>
442444 </a>
443445 </li>
444446 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
445-<a href="/diffs/merge-requests" tabindex="-1">
447+<a role="presentation" data-href="/diffs/merge-requests">
446448 <span class="lbl">merge requests</span>
447449 <span class="k">section</span>
448450 </a>
449451 </li>
450452 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
451-<a href="/diffs/releases" tabindex="-1">
453+<a role="presentation" data-href="/diffs/releases">
452454 <span class="lbl">releases</span>
453455 <span class="k">section</span>
454456 </a>
455457 </li>
456458 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
457-<a href="/diffs/actions" tabindex="-1">
459+<a role="presentation" data-href="/diffs/actions">
458460 <span class="lbl">actions</span>
459461 <span class="k">section</span>
460462 </a>
461463 </li>
462464 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
463-<a href="/diffs/tree/main" tabindex="-1">
465+<a role="presentation" data-href="/diffs/tree/main">
464466 <span class="lbl">main</span>
465467 <span class="k">branch</span>
466468 </a>
467469 </li>
468470 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
469-<a href="/diffs/tree/side" tabindex="-1">
471+<a role="presentation" data-href="/diffs/tree/side">
470472 <span class="lbl">side</span>
471473 <span class="k">branch</span>
472474 </a>
473475 </li>
474476 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
475-<a href="/diffs/tree/v1.0.0" tabindex="-1">
477+<a role="presentation" data-href="/diffs/tree/v1.0.0">
476478 <span class="lbl">v1.0.0</span>
477479 <span class="k">tag</span>
478480 </a>
479481 </li>
480482 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
481-<a href="#" data-fire="keys" tabindex="-1">
483+<a role="presentation" data-fire="keys">
482484 <span class="lbl">turn single-key shortcuts off</span>
483485 <span class="k">action</span>
484486 </a>
M internal/app/testdata/web/commit-modify-split.html +19 -17
@@ -349,18 +349,20 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
349349 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
350350 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
351351 </footer>
352-<div class="hint" role="status" aria-live="polite">
353-<span class="hint-nav" :text="document.body.dataset.nav || ''">
352+<div class="hint">
353+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
354354 </span>
355+<span role="status" aria-live="polite">
355356 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
356357 </span>
357358 <span class="hint-flash" :text="document.body.dataset.flash || ''">
358359 </span>
360+</span>
359361 </div>
360362 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
361363 <div class="panel-head">
362364 <h2>Keyboard shortcuts</h2>
363-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
365+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
364366 <kbd>esc</kbd> close</button>
365367 </div>
366368 <div class="keys-pref">
@@ -494,7 +496,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
494496 <div class="panel-head">
495497 <label for="refs-filter" class="sr">filter branches and tags</label>
496498 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
497-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
499+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
498500 <kbd>esc</kbd>
499501 </button>
500502 </div>
@@ -529,79 +531,79 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
529531 <div class="panel-head">
530532 <label for="cmd-q" class="sr">go to</label>
531533 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
532-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
534+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
533535 <kbd>esc</kbd>
534536 </button>
535537 </div>
536538 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
537539 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
538-<a href="/" tabindex="-1">
540+<a role="presentation" data-href="/">
539541 <span class="lbl">home</span>
540542 <span class="k">go</span>
541543 </a>
542544 </li>
543545 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
544-<a href="/diffs" tabindex="-1">
546+<a role="presentation" data-href="/diffs">
545547 <span class="lbl">summary</span>
546548 <span class="k">section</span>
547549 </a>
548550 </li>
549551 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
550-<a href="/diffs/tree/main" tabindex="-1">
552+<a role="presentation" data-href="/diffs/tree/main">
551553 <span class="lbl">files</span>
552554 <span class="k">section</span>
553555 </a>
554556 </li>
555557 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
556-<a href="/diffs/commits/main" tabindex="-1">
558+<a role="presentation" data-href="/diffs/commits/main">
557559 <span class="lbl">commits</span>
558560 <span class="k">section</span>
559561 </a>
560562 </li>
561563 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
562-<a href="/diffs/refs" tabindex="-1">
564+<a role="presentation" data-href="/diffs/refs">
563565 <span class="lbl">branches &amp; tags</span>
564566 <span class="k">section</span>
565567 </a>
566568 </li>
567569 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
568-<a href="/diffs/merge-requests" tabindex="-1">
570+<a role="presentation" data-href="/diffs/merge-requests">
569571 <span class="lbl">merge requests</span>
570572 <span class="k">section</span>
571573 </a>
572574 </li>
573575 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
574-<a href="/diffs/releases" tabindex="-1">
576+<a role="presentation" data-href="/diffs/releases">
575577 <span class="lbl">releases</span>
576578 <span class="k">section</span>
577579 </a>
578580 </li>
579581 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
580-<a href="/diffs/actions" tabindex="-1">
582+<a role="presentation" data-href="/diffs/actions">
581583 <span class="lbl">actions</span>
582584 <span class="k">section</span>
583585 </a>
584586 </li>
585587 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
586-<a href="/diffs/tree/main" tabindex="-1">
588+<a role="presentation" data-href="/diffs/tree/main">
587589 <span class="lbl">main</span>
588590 <span class="k">branch</span>
589591 </a>
590592 </li>
591593 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
592-<a href="/diffs/tree/side" tabindex="-1">
594+<a role="presentation" data-href="/diffs/tree/side">
593595 <span class="lbl">side</span>
594596 <span class="k">branch</span>
595597 </a>
596598 </li>
597599 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
598-<a href="/diffs/tree/v1.0.0" tabindex="-1">
600+<a role="presentation" data-href="/diffs/tree/v1.0.0">
599601 <span class="lbl">v1.0.0</span>
600602 <span class="k">tag</span>
601603 </a>
602604 </li>
603605 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
604-<a href="#" data-fire="keys" tabindex="-1">
606+<a role="presentation" data-fire="keys">
605607 <span class="lbl">turn single-key shortcuts off</span>
606608 <span class="k">action</span>
607609 </a>
M internal/app/testdata/web/commit-modify.html +19 -17
@@ -349,18 +349,20 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
349349 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
350350 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
351351 </footer>
352-<div class="hint" role="status" aria-live="polite">
353-<span class="hint-nav" :text="document.body.dataset.nav || ''">
352+<div class="hint">
353+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
354354 </span>
355+<span role="status" aria-live="polite">
355356 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
356357 </span>
357358 <span class="hint-flash" :text="document.body.dataset.flash || ''">
358359 </span>
360+</span>
359361 </div>
360362 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
361363 <div class="panel-head">
362364 <h2>Keyboard shortcuts</h2>
363-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
365+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
364366 <kbd>esc</kbd> close</button>
365367 </div>
366368 <div class="keys-pref">
@@ -494,7 +496,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
494496 <div class="panel-head">
495497 <label for="refs-filter" class="sr">filter branches and tags</label>
496498 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
497-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
499+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
498500 <kbd>esc</kbd>
499501 </button>
500502 </div>
@@ -529,79 +531,79 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
529531 <div class="panel-head">
530532 <label for="cmd-q" class="sr">go to</label>
531533 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
532-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
534+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
533535 <kbd>esc</kbd>
534536 </button>
535537 </div>
536538 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
537539 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
538-<a href="/" tabindex="-1">
540+<a role="presentation" data-href="/">
539541 <span class="lbl">home</span>
540542 <span class="k">go</span>
541543 </a>
542544 </li>
543545 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
544-<a href="/diffs" tabindex="-1">
546+<a role="presentation" data-href="/diffs">
545547 <span class="lbl">summary</span>
546548 <span class="k">section</span>
547549 </a>
548550 </li>
549551 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
550-<a href="/diffs/tree/main" tabindex="-1">
552+<a role="presentation" data-href="/diffs/tree/main">
551553 <span class="lbl">files</span>
552554 <span class="k">section</span>
553555 </a>
554556 </li>
555557 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
556-<a href="/diffs/commits/main" tabindex="-1">
558+<a role="presentation" data-href="/diffs/commits/main">
557559 <span class="lbl">commits</span>
558560 <span class="k">section</span>
559561 </a>
560562 </li>
561563 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
562-<a href="/diffs/refs" tabindex="-1">
564+<a role="presentation" data-href="/diffs/refs">
563565 <span class="lbl">branches &amp; tags</span>
564566 <span class="k">section</span>
565567 </a>
566568 </li>
567569 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
568-<a href="/diffs/merge-requests" tabindex="-1">
570+<a role="presentation" data-href="/diffs/merge-requests">
569571 <span class="lbl">merge requests</span>
570572 <span class="k">section</span>
571573 </a>
572574 </li>
573575 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
574-<a href="/diffs/releases" tabindex="-1">
576+<a role="presentation" data-href="/diffs/releases">
575577 <span class="lbl">releases</span>
576578 <span class="k">section</span>
577579 </a>
578580 </li>
579581 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
580-<a href="/diffs/actions" tabindex="-1">
582+<a role="presentation" data-href="/diffs/actions">
581583 <span class="lbl">actions</span>
582584 <span class="k">section</span>
583585 </a>
584586 </li>
585587 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
586-<a href="/diffs/tree/main" tabindex="-1">
588+<a role="presentation" data-href="/diffs/tree/main">
587589 <span class="lbl">main</span>
588590 <span class="k">branch</span>
589591 </a>
590592 </li>
591593 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
592-<a href="/diffs/tree/side" tabindex="-1">
594+<a role="presentation" data-href="/diffs/tree/side">
593595 <span class="lbl">side</span>
594596 <span class="k">branch</span>
595597 </a>
596598 </li>
597599 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
598-<a href="/diffs/tree/v1.0.0" tabindex="-1">
600+<a role="presentation" data-href="/diffs/tree/v1.0.0">
599601 <span class="lbl">v1.0.0</span>
600602 <span class="k">tag</span>
601603 </a>
602604 </li>
603605 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
604-<a href="#" data-fire="keys" tabindex="-1">
606+<a role="presentation" data-fire="keys">
605607 <span class="lbl">turn single-key shortcuts off</span>
606608 <span class="k">action</span>
607609 </a>
M internal/app/testdata/web/commit-rename.html +19 -17
@@ -253,18 +253,20 @@
253253 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
254254 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
255255 </footer>
256-<div class="hint" role="status" aria-live="polite">
257-<span class="hint-nav" :text="document.body.dataset.nav || ''">
256+<div class="hint">
257+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
258258 </span>
259+<span role="status" aria-live="polite">
259260 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
260261 </span>
261262 <span class="hint-flash" :text="document.body.dataset.flash || ''">
262263 </span>
264+</span>
263265 </div>
264266 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
265267 <div class="panel-head">
266268 <h2>Keyboard shortcuts</h2>
267-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
269+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
268270 <kbd>esc</kbd> close</button>
269271 </div>
270272 <div class="keys-pref">
@@ -398,7 +400,7 @@
398400 <div class="panel-head">
399401 <label for="refs-filter" class="sr">filter branches and tags</label>
400402 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
401-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
403+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
402404 <kbd>esc</kbd>
403405 </button>
404406 </div>
@@ -433,79 +435,79 @@
433435 <div class="panel-head">
434436 <label for="cmd-q" class="sr">go to</label>
435437 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
436-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
438+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
437439 <kbd>esc</kbd>
438440 </button>
439441 </div>
440442 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
441443 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
442-<a href="/" tabindex="-1">
444+<a role="presentation" data-href="/">
443445 <span class="lbl">home</span>
444446 <span class="k">go</span>
445447 </a>
446448 </li>
447449 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
448-<a href="/diffs" tabindex="-1">
450+<a role="presentation" data-href="/diffs">
449451 <span class="lbl">summary</span>
450452 <span class="k">section</span>
451453 </a>
452454 </li>
453455 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
454-<a href="/diffs/tree/main" tabindex="-1">
456+<a role="presentation" data-href="/diffs/tree/main">
455457 <span class="lbl">files</span>
456458 <span class="k">section</span>
457459 </a>
458460 </li>
459461 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
460-<a href="/diffs/commits/main" tabindex="-1">
462+<a role="presentation" data-href="/diffs/commits/main">
461463 <span class="lbl">commits</span>
462464 <span class="k">section</span>
463465 </a>
464466 </li>
465467 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
466-<a href="/diffs/refs" tabindex="-1">
468+<a role="presentation" data-href="/diffs/refs">
467469 <span class="lbl">branches &amp; tags</span>
468470 <span class="k">section</span>
469471 </a>
470472 </li>
471473 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
472-<a href="/diffs/merge-requests" tabindex="-1">
474+<a role="presentation" data-href="/diffs/merge-requests">
473475 <span class="lbl">merge requests</span>
474476 <span class="k">section</span>
475477 </a>
476478 </li>
477479 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
478-<a href="/diffs/releases" tabindex="-1">
480+<a role="presentation" data-href="/diffs/releases">
479481 <span class="lbl">releases</span>
480482 <span class="k">section</span>
481483 </a>
482484 </li>
483485 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
484-<a href="/diffs/actions" tabindex="-1">
486+<a role="presentation" data-href="/diffs/actions">
485487 <span class="lbl">actions</span>
486488 <span class="k">section</span>
487489 </a>
488490 </li>
489491 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
490-<a href="/diffs/tree/main" tabindex="-1">
492+<a role="presentation" data-href="/diffs/tree/main">
491493 <span class="lbl">main</span>
492494 <span class="k">branch</span>
493495 </a>
494496 </li>
495497 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
496-<a href="/diffs/tree/side" tabindex="-1">
498+<a role="presentation" data-href="/diffs/tree/side">
497499 <span class="lbl">side</span>
498500 <span class="k">branch</span>
499501 </a>
500502 </li>
501503 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
502-<a href="/diffs/tree/v1.0.0" tabindex="-1">
504+<a role="presentation" data-href="/diffs/tree/v1.0.0">
503505 <span class="lbl">v1.0.0</span>
504506 <span class="k">tag</span>
505507 </a>
506508 </li>
507509 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
508-<a href="#" data-fire="keys" tabindex="-1">
510+<a role="presentation" data-fire="keys">
509511 <span class="lbl">turn single-key shortcuts off</span>
510512 <span class="k">action</span>
511513 </a>
M internal/app/testdata/web/commit-root.html +19 -17
@@ -572,18 +572,20 @@
572572 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
573573 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
574574 </footer>
575-<div class="hint" role="status" aria-live="polite">
576-<span class="hint-nav" :text="document.body.dataset.nav || ''">
575+<div class="hint">
576+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
577577 </span>
578+<span role="status" aria-live="polite">
578579 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
579580 </span>
580581 <span class="hint-flash" :text="document.body.dataset.flash || ''">
581582 </span>
583+</span>
582584 </div>
583585 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
584586 <div class="panel-head">
585587 <h2>Keyboard shortcuts</h2>
586-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
588+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
587589 <kbd>esc</kbd> close</button>
588590 </div>
589591 <div class="keys-pref">
@@ -717,7 +719,7 @@
717719 <div class="panel-head">
718720 <label for="refs-filter" class="sr">filter branches and tags</label>
719721 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
720-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
722+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
721723 <kbd>esc</kbd>
722724 </button>
723725 </div>
@@ -752,79 +754,79 @@
752754 <div class="panel-head">
753755 <label for="cmd-q" class="sr">go to</label>
754756 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
755-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
757+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
756758 <kbd>esc</kbd>
757759 </button>
758760 </div>
759761 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
760762 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
761-<a href="/" tabindex="-1">
763+<a role="presentation" data-href="/">
762764 <span class="lbl">home</span>
763765 <span class="k">go</span>
764766 </a>
765767 </li>
766768 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
767-<a href="/diffs" tabindex="-1">
769+<a role="presentation" data-href="/diffs">
768770 <span class="lbl">summary</span>
769771 <span class="k">section</span>
770772 </a>
771773 </li>
772774 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
773-<a href="/diffs/tree/main" tabindex="-1">
775+<a role="presentation" data-href="/diffs/tree/main">
774776 <span class="lbl">files</span>
775777 <span class="k">section</span>
776778 </a>
777779 </li>
778780 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
779-<a href="/diffs/commits/main" tabindex="-1">
781+<a role="presentation" data-href="/diffs/commits/main">
780782 <span class="lbl">commits</span>
781783 <span class="k">section</span>
782784 </a>
783785 </li>
784786 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
785-<a href="/diffs/refs" tabindex="-1">
787+<a role="presentation" data-href="/diffs/refs">
786788 <span class="lbl">branches &amp; tags</span>
787789 <span class="k">section</span>
788790 </a>
789791 </li>
790792 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
791-<a href="/diffs/merge-requests" tabindex="-1">
793+<a role="presentation" data-href="/diffs/merge-requests">
792794 <span class="lbl">merge requests</span>
793795 <span class="k">section</span>
794796 </a>
795797 </li>
796798 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
797-<a href="/diffs/releases" tabindex="-1">
799+<a role="presentation" data-href="/diffs/releases">
798800 <span class="lbl">releases</span>
799801 <span class="k">section</span>
800802 </a>
801803 </li>
802804 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
803-<a href="/diffs/actions" tabindex="-1">
805+<a role="presentation" data-href="/diffs/actions">
804806 <span class="lbl">actions</span>
805807 <span class="k">section</span>
806808 </a>
807809 </li>
808810 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
809-<a href="/diffs/tree/main" tabindex="-1">
811+<a role="presentation" data-href="/diffs/tree/main">
810812 <span class="lbl">main</span>
811813 <span class="k">branch</span>
812814 </a>
813815 </li>
814816 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
815-<a href="/diffs/tree/side" tabindex="-1">
817+<a role="presentation" data-href="/diffs/tree/side">
816818 <span class="lbl">side</span>
817819 <span class="k">branch</span>
818820 </a>
819821 </li>
820822 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
821-<a href="/diffs/tree/v1.0.0" tabindex="-1">
823+<a role="presentation" data-href="/diffs/tree/v1.0.0">
822824 <span class="lbl">v1.0.0</span>
823825 <span class="k">tag</span>
824826 </a>
825827 </li>
826828 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
827-<a href="#" data-fire="keys" tabindex="-1">
829+<a role="presentation" data-fire="keys">
828830 <span class="lbl">turn single-key shortcuts off</span>
829831 <span class="k">action</span>
830832 </a>
M internal/app/testdata/web/commit-trojan.html +19 -17
@@ -401,18 +401,20 @@
401401 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
402402 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
403403 </footer>
404-<div class="hint" role="status" aria-live="polite">
405-<span class="hint-nav" :text="document.body.dataset.nav || ''">
404+<div class="hint">
405+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
406406 </span>
407+<span role="status" aria-live="polite">
407408 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
408409 </span>
409410 <span class="hint-flash" :text="document.body.dataset.flash || ''">
410411 </span>
412+</span>
411413 </div>
412414 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
413415 <div class="panel-head">
414416 <h2>Keyboard shortcuts</h2>
415-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
417+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
416418 <kbd>esc</kbd> close</button>
417419 </div>
418420 <div class="keys-pref">
@@ -546,7 +548,7 @@
546548 <div class="panel-head">
547549 <label for="refs-filter" class="sr">filter branches and tags</label>
548550 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
549-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
551+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
550552 <kbd>esc</kbd>
551553 </button>
552554 </div>
@@ -581,79 +583,79 @@
581583 <div class="panel-head">
582584 <label for="cmd-q" class="sr">go to</label>
583585 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
584-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
586+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
585587 <kbd>esc</kbd>
586588 </button>
587589 </div>
588590 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
589591 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
590-<a href="/" tabindex="-1">
592+<a role="presentation" data-href="/">
591593 <span class="lbl">home</span>
592594 <span class="k">go</span>
593595 </a>
594596 </li>
595597 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
596-<a href="/diffs" tabindex="-1">
598+<a role="presentation" data-href="/diffs">
597599 <span class="lbl">summary</span>
598600 <span class="k">section</span>
599601 </a>
600602 </li>
601603 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
602-<a href="/diffs/tree/main" tabindex="-1">
604+<a role="presentation" data-href="/diffs/tree/main">
603605 <span class="lbl">files</span>
604606 <span class="k">section</span>
605607 </a>
606608 </li>
607609 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
608-<a href="/diffs/commits/main" tabindex="-1">
610+<a role="presentation" data-href="/diffs/commits/main">
609611 <span class="lbl">commits</span>
610612 <span class="k">section</span>
611613 </a>
612614 </li>
613615 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
614-<a href="/diffs/refs" tabindex="-1">
616+<a role="presentation" data-href="/diffs/refs">
615617 <span class="lbl">branches &amp; tags</span>
616618 <span class="k">section</span>
617619 </a>
618620 </li>
619621 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
620-<a href="/diffs/merge-requests" tabindex="-1">
622+<a role="presentation" data-href="/diffs/merge-requests">
621623 <span class="lbl">merge requests</span>
622624 <span class="k">section</span>
623625 </a>
624626 </li>
625627 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
626-<a href="/diffs/releases" tabindex="-1">
628+<a role="presentation" data-href="/diffs/releases">
627629 <span class="lbl">releases</span>
628630 <span class="k">section</span>
629631 </a>
630632 </li>
631633 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
632-<a href="/diffs/actions" tabindex="-1">
634+<a role="presentation" data-href="/diffs/actions">
633635 <span class="lbl">actions</span>
634636 <span class="k">section</span>
635637 </a>
636638 </li>
637639 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
638-<a href="/diffs/tree/main" tabindex="-1">
640+<a role="presentation" data-href="/diffs/tree/main">
639641 <span class="lbl">main</span>
640642 <span class="k">branch</span>
641643 </a>
642644 </li>
643645 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
644-<a href="/diffs/tree/side" tabindex="-1">
646+<a role="presentation" data-href="/diffs/tree/side">
645647 <span class="lbl">side</span>
646648 <span class="k">branch</span>
647649 </a>
648650 </li>
649651 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
650-<a href="/diffs/tree/v1.0.0" tabindex="-1">
652+<a role="presentation" data-href="/diffs/tree/v1.0.0">
651653 <span class="lbl">v1.0.0</span>
652654 <span class="k">tag</span>
653655 </a>
654656 </li>
655657 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
656-<a href="#" data-fire="keys" tabindex="-1">
658+<a role="presentation" data-fire="keys">
657659 <span class="lbl">turn single-key shortcuts off</span>
658660 <span class="k">action</span>
659661 </a>
M internal/app/testdata/web/commits-page2.html +18 -16
@@ -168,18 +168,20 @@
168168 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
169169 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
170170 </footer>
171-<div class="hint" role="status" aria-live="polite">
172-<span class="hint-nav" :text="document.body.dataset.nav || ''">
171+<div class="hint">
172+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
173173 </span>
174+<span role="status" aria-live="polite">
174175 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
175176 </span>
176177 <span class="hint-flash" :text="document.body.dataset.flash || ''">
177178 </span>
179+</span>
178180 </div>
179181 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
180182 <div class="panel-head">
181183 <h2>Keyboard shortcuts</h2>
182-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
184+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
183185 <kbd>esc</kbd> close</button>
184186 </div>
185187 <div class="keys-pref">
@@ -313,7 +315,7 @@
313315 <div class="panel-head">
314316 <label for="refs-filter" class="sr">filter branches and tags</label>
315317 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
316-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
318+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
317319 <kbd>esc</kbd>
318320 </button>
319321 </div>
@@ -340,73 +342,73 @@
340342 <div class="panel-head">
341343 <label for="cmd-q" class="sr">go to</label>
342344 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
343-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
345+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
344346 <kbd>esc</kbd>
345347 </button>
346348 </div>
347349 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
348350 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
349-<a href="/" tabindex="-1">
351+<a role="presentation" data-href="/">
350352 <span class="lbl">home</span>
351353 <span class="k">go</span>
352354 </a>
353355 </li>
354356 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
355-<a href="/long" tabindex="-1">
357+<a role="presentation" data-href="/long">
356358 <span class="lbl">summary</span>
357359 <span class="k">section</span>
358360 </a>
359361 </li>
360362 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
361-<a href="/long/tree/main" tabindex="-1">
363+<a role="presentation" data-href="/long/tree/main">
362364 <span class="lbl">files</span>
363365 <span class="k">section</span>
364366 </a>
365367 </li>
366368 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
367-<a href="/long/commits/main" tabindex="-1">
369+<a role="presentation" data-href="/long/commits/main">
368370 <span class="lbl">commits</span>
369371 <span class="k">section</span>
370372 </a>
371373 </li>
372374 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
373-<a href="/long/refs" tabindex="-1">
375+<a role="presentation" data-href="/long/refs">
374376 <span class="lbl">branches &amp; tags</span>
375377 <span class="k">section</span>
376378 </a>
377379 </li>
378380 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
379-<a href="/long/merge-requests" tabindex="-1">
381+<a role="presentation" data-href="/long/merge-requests">
380382 <span class="lbl">merge requests</span>
381383 <span class="k">section</span>
382384 </a>
383385 </li>
384386 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
385-<a href="/long/releases" tabindex="-1">
387+<a role="presentation" data-href="/long/releases">
386388 <span class="lbl">releases</span>
387389 <span class="k">section</span>
388390 </a>
389391 </li>
390392 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
391-<a href="/long/actions" tabindex="-1">
393+<a role="presentation" data-href="/long/actions">
392394 <span class="lbl">actions</span>
393395 <span class="k">section</span>
394396 </a>
395397 </li>
396398 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
397-<a href="/long/tree/main" tabindex="-1">
399+<a role="presentation" data-href="/long/tree/main">
398400 <span class="lbl">main</span>
399401 <span class="k">branch</span>
400402 </a>
401403 </li>
402404 <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs">
403-<a href="/diffs" tabindex="-1">
405+<a role="presentation" data-href="/diffs">
404406 <span class="lbl">diffs</span>
405407 <span class="k">repo</span>
406408 </a>
407409 </li>
408410 <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
409-<a href="#" data-fire="keys" tabindex="-1">
411+<a role="presentation" data-fire="keys">
410412 <span class="lbl">turn single-key shortcuts off</span>
411413 <span class="k">action</span>
412414 </a>
M internal/app/testdata/web/commits-path.html +18 -16
@@ -183,18 +183,20 @@
183183 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
184184 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
185185 </footer>
186-<div class="hint" role="status" aria-live="polite">
187-<span class="hint-nav" :text="document.body.dataset.nav || ''">
186+<div class="hint">
187+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
188188 </span>
189+<span role="status" aria-live="polite">
189190 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
190191 </span>
191192 <span class="hint-flash" :text="document.body.dataset.flash || ''">
192193 </span>
194+</span>
193195 </div>
194196 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
195197 <div class="panel-head">
196198 <h2>Keyboard shortcuts</h2>
197-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
199+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
198200 <kbd>esc</kbd> close</button>
199201 </div>
200202 <div class="keys-pref">
@@ -328,7 +330,7 @@
328330 <div class="panel-head">
329331 <label for="refs-filter" class="sr">filter branches and tags</label>
330332 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
331-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
333+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
332334 <kbd>esc</kbd>
333335 </button>
334336 </div>
@@ -355,73 +357,73 @@
355357 <div class="panel-head">
356358 <label for="cmd-q" class="sr">go to</label>
357359 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
358-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
360+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
359361 <kbd>esc</kbd>
360362 </button>
361363 </div>
362364 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
363365 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
364-<a href="/" tabindex="-1">
366+<a role="presentation" data-href="/">
365367 <span class="lbl">home</span>
366368 <span class="k">go</span>
367369 </a>
368370 </li>
369371 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
370-<a href="/long" tabindex="-1">
372+<a role="presentation" data-href="/long">
371373 <span class="lbl">summary</span>
372374 <span class="k">section</span>
373375 </a>
374376 </li>
375377 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
376-<a href="/long/tree/main" tabindex="-1">
378+<a role="presentation" data-href="/long/tree/main">
377379 <span class="lbl">files</span>
378380 <span class="k">section</span>
379381 </a>
380382 </li>
381383 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
382-<a href="/long/commits/main" tabindex="-1">
384+<a role="presentation" data-href="/long/commits/main">
383385 <span class="lbl">commits</span>
384386 <span class="k">section</span>
385387 </a>
386388 </li>
387389 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
388-<a href="/long/refs" tabindex="-1">
390+<a role="presentation" data-href="/long/refs">
389391 <span class="lbl">branches &amp; tags</span>
390392 <span class="k">section</span>
391393 </a>
392394 </li>
393395 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
394-<a href="/long/merge-requests" tabindex="-1">
396+<a role="presentation" data-href="/long/merge-requests">
395397 <span class="lbl">merge requests</span>
396398 <span class="k">section</span>
397399 </a>
398400 </li>
399401 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
400-<a href="/long/releases" tabindex="-1">
402+<a role="presentation" data-href="/long/releases">
401403 <span class="lbl">releases</span>
402404 <span class="k">section</span>
403405 </a>
404406 </li>
405407 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
406-<a href="/long/actions" tabindex="-1">
408+<a role="presentation" data-href="/long/actions">
407409 <span class="lbl">actions</span>
408410 <span class="k">section</span>
409411 </a>
410412 </li>
411413 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
412-<a href="/long/tree/main" tabindex="-1">
414+<a role="presentation" data-href="/long/tree/main">
413415 <span class="lbl">main</span>
414416 <span class="k">branch</span>
415417 </a>
416418 </li>
417419 <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs">
418-<a href="/diffs" tabindex="-1">
420+<a role="presentation" data-href="/diffs">
419421 <span class="lbl">diffs</span>
420422 <span class="k">repo</span>
421423 </a>
422424 </li>
423425 <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
424-<a href="#" data-fire="keys" tabindex="-1">
426+<a role="presentation" data-fire="keys">
425427 <span class="lbl">turn single-key shortcuts off</span>
426428 <span class="k">action</span>
427429 </a>
M internal/app/testdata/web/commits-root.html +19 -17
@@ -965,14 +965,16 @@
965965 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
966966 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
967967 </footer>
968-<div class="hint" role="status" aria-live="polite">
969-<span class="hint-nav" :text="document.body.dataset.nav || ''">
968+<div class="hint">
969+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
970970 </span>
971+<span role="status" aria-live="polite">
971972 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
972973 </span>
973974 <span class="hint-flash" :text="document.body.dataset.flash || ''">
974975 </span>
975-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
976+</span>
977+<span class="hint-live" role="status" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
976978 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
977979 htmx:sse:error -> ermiteLive.error(event.detail.status);
978980 htmx:sse:before:connection -> ermiteLive.gate(event);
@@ -984,7 +986,7 @@
984986 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
985987 <div class="panel-head">
986988 <h2>Keyboard shortcuts</h2>
987-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
989+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
988990 <kbd>esc</kbd> close</button>
989991 </div>
990992 <div class="keys-pref">
@@ -1118,7 +1120,7 @@
11181120 <div class="panel-head">
11191121 <label for="refs-filter" class="sr">filter branches and tags</label>
11201122 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
1121-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
1123+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
11221124 <kbd>esc</kbd>
11231125 </button>
11241126 </div>
@@ -1145,73 +1147,73 @@
11451147 <div class="panel-head">
11461148 <label for="cmd-q" class="sr">go to</label>
11471149 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
1148-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
1150+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
11491151 <kbd>esc</kbd>
11501152 </button>
11511153 </div>
11521154 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
11531155 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
1154-<a href="/" tabindex="-1">
1156+<a role="presentation" data-href="/">
11551157 <span class="lbl">home</span>
11561158 <span class="k">go</span>
11571159 </a>
11581160 </li>
11591161 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
1160-<a href="/long" tabindex="-1">
1162+<a role="presentation" data-href="/long">
11611163 <span class="lbl">summary</span>
11621164 <span class="k">section</span>
11631165 </a>
11641166 </li>
11651167 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
1166-<a href="/long/tree/main" tabindex="-1">
1168+<a role="presentation" data-href="/long/tree/main">
11671169 <span class="lbl">files</span>
11681170 <span class="k">section</span>
11691171 </a>
11701172 </li>
11711173 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
1172-<a href="/long/commits/main" tabindex="-1">
1174+<a role="presentation" data-href="/long/commits/main">
11731175 <span class="lbl">commits</span>
11741176 <span class="k">section</span>
11751177 </a>
11761178 </li>
11771179 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
1178-<a href="/long/refs" tabindex="-1">
1180+<a role="presentation" data-href="/long/refs">
11791181 <span class="lbl">branches &amp; tags</span>
11801182 <span class="k">section</span>
11811183 </a>
11821184 </li>
11831185 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
1184-<a href="/long/merge-requests" tabindex="-1">
1186+<a role="presentation" data-href="/long/merge-requests">
11851187 <span class="lbl">merge requests</span>
11861188 <span class="k">section</span>
11871189 </a>
11881190 </li>
11891191 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
1190-<a href="/long/releases" tabindex="-1">
1192+<a role="presentation" data-href="/long/releases">
11911193 <span class="lbl">releases</span>
11921194 <span class="k">section</span>
11931195 </a>
11941196 </li>
11951197 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
1196-<a href="/long/actions" tabindex="-1">
1198+<a role="presentation" data-href="/long/actions">
11971199 <span class="lbl">actions</span>
11981200 <span class="k">section</span>
11991201 </a>
12001202 </li>
12011203 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
1202-<a href="/long/tree/main" tabindex="-1">
1204+<a role="presentation" data-href="/long/tree/main">
12031205 <span class="lbl">main</span>
12041206 <span class="k">branch</span>
12051207 </a>
12061208 </li>
12071209 <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs">
1208-<a href="/diffs" tabindex="-1">
1210+<a role="presentation" data-href="/diffs">
12091211 <span class="lbl">diffs</span>
12101212 <span class="k">repo</span>
12111213 </a>
12121214 </li>
12131215 <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
1214-<a href="#" data-fire="keys" tabindex="-1">
1216+<a role="presentation" data-fire="keys">
12151217 <span class="lbl">turn single-key shortcuts off</span>
12161218 <span class="k">action</span>
12171219 </a>
M internal/app/testdata/web/commits-small.html +21 -19
@@ -310,14 +310,16 @@
310310 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
311311 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
312312 </footer>
313-<div class="hint" role="status" aria-live="polite">
314-<span class="hint-nav" :text="document.body.dataset.nav || ''">
313+<div class="hint">
314+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
315315 </span>
316+<span role="status" aria-live="polite">
316317 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
317318 </span>
318319 <span class="hint-flash" :text="document.body.dataset.flash || ''">
319320 </span>
320-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
321+</span>
322+<span class="hint-live" role="status" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
321323 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
322324 htmx:sse:error -> ermiteLive.error(event.detail.status);
323325 htmx:sse:before:connection -> ermiteLive.gate(event);
@@ -329,7 +331,7 @@
329331 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
330332 <div class="panel-head">
331333 <h2>Keyboard shortcuts</h2>
332-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
334+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
333335 <kbd>esc</kbd> close</button>
334336 </div>
335337 <div class="keys-pref">
@@ -463,7 +465,7 @@
463465 <div class="panel-head">
464466 <label for="refs-filter" class="sr">filter branches and tags</label>
465467 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
466-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
468+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
467469 <kbd>esc</kbd>
468470 </button>
469471 </div>
@@ -498,85 +500,85 @@
498500 <div class="panel-head">
499501 <label for="cmd-q" class="sr">go to</label>
500502 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
501-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
503+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
502504 <kbd>esc</kbd>
503505 </button>
504506 </div>
505507 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
506508 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
507-<a href="/" tabindex="-1">
509+<a role="presentation" data-href="/">
508510 <span class="lbl">home</span>
509511 <span class="k">go</span>
510512 </a>
511513 </li>
512514 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
513-<a href="/diffs" tabindex="-1">
515+<a role="presentation" data-href="/diffs">
514516 <span class="lbl">summary</span>
515517 <span class="k">section</span>
516518 </a>
517519 </li>
518520 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
519-<a href="/diffs/tree/main" tabindex="-1">
521+<a role="presentation" data-href="/diffs/tree/main">
520522 <span class="lbl">files</span>
521523 <span class="k">section</span>
522524 </a>
523525 </li>
524526 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
525-<a href="/diffs/commits/main" tabindex="-1">
527+<a role="presentation" data-href="/diffs/commits/main">
526528 <span class="lbl">commits</span>
527529 <span class="k">section</span>
528530 </a>
529531 </li>
530532 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
531-<a href="/diffs/refs" tabindex="-1">
533+<a role="presentation" data-href="/diffs/refs">
532534 <span class="lbl">branches &amp; tags</span>
533535 <span class="k">section</span>
534536 </a>
535537 </li>
536538 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
537-<a href="/diffs/merge-requests" tabindex="-1">
539+<a role="presentation" data-href="/diffs/merge-requests">
538540 <span class="lbl">merge requests</span>
539541 <span class="k">section</span>
540542 </a>
541543 </li>
542544 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
543-<a href="/diffs/releases" tabindex="-1">
545+<a role="presentation" data-href="/diffs/releases">
544546 <span class="lbl">releases</span>
545547 <span class="k">section</span>
546548 </a>
547549 </li>
548550 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
549-<a href="/diffs/actions" tabindex="-1">
551+<a role="presentation" data-href="/diffs/actions">
550552 <span class="lbl">actions</span>
551553 <span class="k">section</span>
552554 </a>
553555 </li>
554556 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
555-<a href="/diffs/tree/main" tabindex="-1">
557+<a role="presentation" data-href="/diffs/tree/main">
556558 <span class="lbl">main</span>
557559 <span class="k">branch</span>
558560 </a>
559561 </li>
560562 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side">
561-<a href="/diffs/tree/side" tabindex="-1">
563+<a role="presentation" data-href="/diffs/tree/side">
562564 <span class="lbl">side</span>
563565 <span class="k">branch</span>
564566 </a>
565567 </li>
566568 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0">
567-<a href="/diffs/tree/v1.0.0" tabindex="-1">
569+<a role="presentation" data-href="/diffs/tree/v1.0.0">
568570 <span class="lbl">v1.0.0</span>
569571 <span class="k">tag</span>
570572 </a>
571573 </li>
572574 <li id="cmd-o11" role="option" aria-selected="false" data-k="repo long">
573-<a href="/long" tabindex="-1">
575+<a role="presentation" data-href="/long">
574576 <span class="lbl">long</span>
575577 <span class="k">repo</span>
576578 </a>
577579 </li>
578580 <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
579-<a href="#" data-fire="keys" tabindex="-1">
581+<a role="presentation" data-fire="keys">
580582 <span class="lbl">turn single-key shortcuts off</span>
581583 <span class="k">action</span>
582584 </a>
M internal/app/testdata/web/empty.html +9 -7
@@ -86,18 +86,20 @@
8686 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
8787 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
8888 </footer>
89-<div class="hint" role="status" aria-live="polite">
90-<span class="hint-nav" :text="document.body.dataset.nav || ''">
89+<div class="hint">
90+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
9191 </span>
92+<span role="status" aria-live="polite">
9293 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
9394 </span>
9495 <span class="hint-flash" :text="document.body.dataset.flash || ''">
9596 </span>
97+</span>
9698 </div>
9799 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
98100 <div class="panel-head">
99101 <h2>Keyboard shortcuts</h2>
100-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
102+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
101103 <kbd>esc</kbd> close</button>
102104 </div>
103105 <div class="keys-pref">
@@ -231,25 +233,25 @@
231233 <div class="panel-head">
232234 <label for="cmd-q" class="sr">go to</label>
233235 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
234-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
236+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
235237 <kbd>esc</kbd>
236238 </button>
237239 </div>
238240 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
239241 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
240-<a href="/" tabindex="-1">
242+<a role="presentation" data-href="/">
241243 <span class="lbl">home</span>
242244 <span class="k">go</span>
243245 </a>
244246 </li>
245247 <li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo">
246-<a href="/demo" tabindex="-1">
248+<a role="presentation" data-href="/demo">
247249 <span class="lbl">demo</span>
248250 <span class="k">repo</span>
249251 </a>
250252 </li>
251253 <li id="cmd-o2" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
252-<a href="#" data-fire="keys" tabindex="-1">
254+<a role="presentation" data-fire="keys">
253255 <span class="lbl">turn single-key shortcuts off</span>
254256 <span class="k">action</span>
255257 </a>
M internal/app/testdata/web/home.html +11 -9
@@ -182,14 +182,16 @@
182182 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
183183 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
184184 </footer>
185-<div class="hint" role="status" aria-live="polite">
186-<span class="hint-nav" :text="document.body.dataset.nav || ''">
185+<div class="hint">
186+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
187187 </span>
188+<span role="status" aria-live="polite">
188189 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
189190 </span>
190191 <span class="hint-flash" :text="document.body.dataset.flash || ''">
191192 </span>
192-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
193+</span>
194+<span class="hint-live" role="status" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
193195 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
194196 htmx:sse:error -> ermiteLive.error(event.detail.status);
195197 htmx:sse:before:connection -> ermiteLive.gate(event);
@@ -201,7 +203,7 @@
201203 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
202204 <div class="panel-head">
203205 <h2>Keyboard shortcuts</h2>
204-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
206+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
205207 <kbd>esc</kbd> close</button>
206208 </div>
207209 <div class="keys-pref">
@@ -335,31 +337,31 @@
335337 <div class="panel-head">
336338 <label for="cmd-q" class="sr">go to</label>
337339 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
338-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
340+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
339341 <kbd>esc</kbd>
340342 </button>
341343 </div>
342344 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
343345 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
344-<a href="/" tabindex="-1">
346+<a role="presentation" data-href="/">
345347 <span class="lbl">home</span>
346348 <span class="k">go</span>
347349 </a>
348350 </li>
349351 <li id="cmd-o1" role="option" aria-selected="false" data-k="repo quiet">
350-<a href="/quiet" tabindex="-1">
352+<a role="presentation" data-href="/quiet">
351353 <span class="lbl">quiet</span>
352354 <span class="k">repo</span>
353355 </a>
354356 </li>
355357 <li id="cmd-o2" role="option" aria-selected="false" data-k="repo demo">
356-<a href="/demo" tabindex="-1">
358+<a role="presentation" data-href="/demo">
357359 <span class="lbl">demo</span>
358360 <span class="k">repo</span>
359361 </a>
360362 </li>
361363 <li id="cmd-o3" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
362-<a href="#" data-fire="keys" tabindex="-1">
364+<a role="presentation" data-fire="keys">
363365 <span class="lbl">turn single-key shortcuts off</span>
364366 <span class="k">action</span>
365367 </a>
M internal/app/testdata/web/not-found-page.html +8 -6
@@ -75,18 +75,20 @@
7575 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
7676 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
7777 </footer>
78-<div class="hint" role="status" aria-live="polite">
79-<span class="hint-nav" :text="document.body.dataset.nav || ''">
78+<div class="hint">
79+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
8080 </span>
81+<span role="status" aria-live="polite">
8182 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
8283 </span>
8384 <span class="hint-flash" :text="document.body.dataset.flash || ''">
8485 </span>
86+</span>
8587 </div>
8688 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
8789 <div class="panel-head">
8890 <h2>Keyboard shortcuts</h2>
89-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
91+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
9092 <kbd>esc</kbd> close</button>
9193 </div>
9294 <div class="keys-pref">
@@ -220,19 +222,19 @@
220222 <div class="panel-head">
221223 <label for="cmd-q" class="sr">go to</label>
222224 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
223-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
225+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
224226 <kbd>esc</kbd>
225227 </button>
226228 </div>
227229 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
228230 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
229-<a href="/" tabindex="-1">
231+<a role="presentation" data-href="/">
230232 <span class="lbl">home</span>
231233 <span class="k">go</span>
232234 </a>
233235 </li>
234236 <li id="cmd-o1" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
235-<a href="#" data-fire="keys" tabindex="-1">
237+<a role="presentation" data-fire="keys">
236238 <span class="lbl">turn single-key shortcuts off</span>
237239 <span class="k">action</span>
238240 </a>
M internal/app/testdata/web/not-found.html +8 -6
@@ -75,18 +75,20 @@
7575 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
7676 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
7777 </footer>
78-<div class="hint" role="status" aria-live="polite">
79-<span class="hint-nav" :text="document.body.dataset.nav || ''">
78+<div class="hint">
79+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
8080 </span>
81+<span role="status" aria-live="polite">
8182 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
8283 </span>
8384 <span class="hint-flash" :text="document.body.dataset.flash || ''">
8485 </span>
86+</span>
8587 </div>
8688 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
8789 <div class="panel-head">
8890 <h2>Keyboard shortcuts</h2>
89-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
91+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
9092 <kbd>esc</kbd> close</button>
9193 </div>
9294 <div class="keys-pref">
@@ -220,19 +222,19 @@
220222 <div class="panel-head">
221223 <label for="cmd-q" class="sr">go to</label>
222224 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
223-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
225+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
224226 <kbd>esc</kbd>
225227 </button>
226228 </div>
227229 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
228230 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
229-<a href="/" tabindex="-1">
231+<a role="presentation" data-href="/">
230232 <span class="lbl">home</span>
231233 <span class="k">go</span>
232234 </a>
233235 </li>
234236 <li id="cmd-o1" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
235-<a href="#" data-fire="keys" tabindex="-1">
237+<a role="presentation" data-fire="keys">
236238 <span class="lbl">turn single-key shortcuts off</span>
237239 <span class="k">action</span>
238240 </a>
M internal/app/testdata/web/refs.html +22 -20
@@ -192,14 +192,16 @@
192192 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
193193 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
194194 </footer>
195-<div class="hint" role="status" aria-live="polite">
196-<span class="hint-nav" :text="document.body.dataset.nav || ''">
195+<div class="hint">
196+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
197197 </span>
198+<span role="status" aria-live="polite">
198199 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
199200 </span>
200201 <span class="hint-flash" :text="document.body.dataset.flash || ''">
201202 </span>
202-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
203+</span>
204+<span class="hint-live" role="status" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
203205 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
204206 htmx:sse:error -> ermiteLive.error(event.detail.status);
205207 htmx:sse:before:connection -> ermiteLive.gate(event);
@@ -211,7 +213,7 @@
211213 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
212214 <div class="panel-head">
213215 <h2>Keyboard shortcuts</h2>
214-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
216+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
215217 <kbd>esc</kbd> close</button>
216218 </div>
217219 <div class="keys-pref">
@@ -345,7 +347,7 @@
345347 <div class="panel-head">
346348 <label for="refs-filter" class="sr">filter branches and tags</label>
347349 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
348-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
350+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
349351 <kbd>esc</kbd>
350352 </button>
351353 </div>
@@ -384,91 +386,91 @@
384386 <div class="panel-head">
385387 <label for="cmd-q" class="sr">go to</label>
386388 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
387-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
389+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
388390 <kbd>esc</kbd>
389391 </button>
390392 </div>
391393 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
392394 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
393-<a href="/" tabindex="-1">
395+<a role="presentation" data-href="/">
394396 <span class="lbl">home</span>
395397 <span class="k">go</span>
396398 </a>
397399 </li>
398400 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
399-<a href="/demo" tabindex="-1">
401+<a role="presentation" data-href="/demo">
400402 <span class="lbl">summary</span>
401403 <span class="k">section</span>
402404 </a>
403405 </li>
404406 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
405-<a href="/demo/tree/main" tabindex="-1">
407+<a role="presentation" data-href="/demo/tree/main">
406408 <span class="lbl">files</span>
407409 <span class="k">section</span>
408410 </a>
409411 </li>
410412 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
411-<a href="/demo/commits/main" tabindex="-1">
413+<a role="presentation" data-href="/demo/commits/main">
412414 <span class="lbl">commits</span>
413415 <span class="k">section</span>
414416 </a>
415417 </li>
416418 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
417-<a href="/demo/refs" tabindex="-1">
419+<a role="presentation" data-href="/demo/refs">
418420 <span class="lbl">branches &amp; tags</span>
419421 <span class="k">section</span>
420422 </a>
421423 </li>
422424 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
423-<a href="/demo/merge-requests" tabindex="-1">
425+<a role="presentation" data-href="/demo/merge-requests">
424426 <span class="lbl">merge requests</span>
425427 <span class="k">section</span>
426428 </a>
427429 </li>
428430 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
429-<a href="/demo/releases" tabindex="-1">
431+<a role="presentation" data-href="/demo/releases">
430432 <span class="lbl">releases</span>
431433 <span class="k">section</span>
432434 </a>
433435 </li>
434436 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
435-<a href="/demo/actions" tabindex="-1">
437+<a role="presentation" data-href="/demo/actions">
436438 <span class="lbl">actions</span>
437439 <span class="k">section</span>
438440 </a>
439441 </li>
440442 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
441-<a href="/demo/tree/feature/x" tabindex="-1">
443+<a role="presentation" data-href="/demo/tree/feature/x">
442444 <span class="lbl">feature/x</span>
443445 <span class="k">branch</span>
444446 </a>
445447 </li>
446448 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
447-<a href="/demo/tree/main" tabindex="-1">
449+<a role="presentation" data-href="/demo/tree/main">
448450 <span class="lbl">main</span>
449451 <span class="k">branch</span>
450452 </a>
451453 </li>
452454 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0">
453-<a href="/demo/tree/v0.1.0" tabindex="-1">
455+<a role="presentation" data-href="/demo/tree/v0.1.0">
454456 <span class="lbl">v0.1.0</span>
455457 <span class="k">tag</span>
456458 </a>
457459 </li>
458460 <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0">
459-<a href="/demo/tree/v0.2.0" tabindex="-1">
461+<a role="presentation" data-href="/demo/tree/v0.2.0">
460462 <span class="lbl">v0.2.0</span>
461463 <span class="k">tag</span>
462464 </a>
463465 </li>
464466 <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet">
465-<a href="/quiet" tabindex="-1">
467+<a role="presentation" data-href="/quiet">
466468 <span class="lbl">quiet</span>
467469 <span class="k">repo</span>
468470 </a>
469471 </li>
470472 <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
471-<a href="#" data-fire="keys" tabindex="-1">
473+<a role="presentation" data-fire="keys">
472474 <span class="lbl">turn single-key shortcuts off</span>
473475 <span class="k">action</span>
474476 </a>
M internal/app/testdata/web/summary.html +22 -20
@@ -218,14 +218,16 @@
218218 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
219219 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
220220 </footer>
221-<div class="hint" role="status" aria-live="polite">
222-<span class="hint-nav" :text="document.body.dataset.nav || ''">
221+<div class="hint">
222+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
223223 </span>
224+<span role="status" aria-live="polite">
224225 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
225226 </span>
226227 <span class="hint-flash" :text="document.body.dataset.flash || ''">
227228 </span>
228-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
229+</span>
230+<span class="hint-live" role="status" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
229231 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
230232 htmx:sse:error -> ermiteLive.error(event.detail.status);
231233 htmx:sse:before:connection -> ermiteLive.gate(event);
@@ -237,7 +239,7 @@
237239 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
238240 <div class="panel-head">
239241 <h2>Keyboard shortcuts</h2>
240-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
242+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
241243 <kbd>esc</kbd> close</button>
242244 </div>
243245 <div class="keys-pref">
@@ -371,7 +373,7 @@
371373 <div class="panel-head">
372374 <label for="refs-filter" class="sr">filter branches and tags</label>
373375 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
374-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
376+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
375377 <kbd>esc</kbd>
376378 </button>
377379 </div>
@@ -410,91 +412,91 @@
410412 <div class="panel-head">
411413 <label for="cmd-q" class="sr">go to</label>
412414 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
413-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
415+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
414416 <kbd>esc</kbd>
415417 </button>
416418 </div>
417419 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
418420 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
419-<a href="/" tabindex="-1">
421+<a role="presentation" data-href="/">
420422 <span class="lbl">home</span>
421423 <span class="k">go</span>
422424 </a>
423425 </li>
424426 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
425-<a href="/demo" tabindex="-1">
427+<a role="presentation" data-href="/demo">
426428 <span class="lbl">summary</span>
427429 <span class="k">section</span>
428430 </a>
429431 </li>
430432 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
431-<a href="/demo/tree/main" tabindex="-1">
433+<a role="presentation" data-href="/demo/tree/main">
432434 <span class="lbl">files</span>
433435 <span class="k">section</span>
434436 </a>
435437 </li>
436438 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
437-<a href="/demo/commits/main" tabindex="-1">
439+<a role="presentation" data-href="/demo/commits/main">
438440 <span class="lbl">commits</span>
439441 <span class="k">section</span>
440442 </a>
441443 </li>
442444 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
443-<a href="/demo/refs" tabindex="-1">
445+<a role="presentation" data-href="/demo/refs">
444446 <span class="lbl">branches &amp; tags</span>
445447 <span class="k">section</span>
446448 </a>
447449 </li>
448450 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
449-<a href="/demo/merge-requests" tabindex="-1">
451+<a role="presentation" data-href="/demo/merge-requests">
450452 <span class="lbl">merge requests</span>
451453 <span class="k">section</span>
452454 </a>
453455 </li>
454456 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
455-<a href="/demo/releases" tabindex="-1">
457+<a role="presentation" data-href="/demo/releases">
456458 <span class="lbl">releases</span>
457459 <span class="k">section</span>
458460 </a>
459461 </li>
460462 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
461-<a href="/demo/actions" tabindex="-1">
463+<a role="presentation" data-href="/demo/actions">
462464 <span class="lbl">actions</span>
463465 <span class="k">section</span>
464466 </a>
465467 </li>
466468 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
467-<a href="/demo/tree/feature/x" tabindex="-1">
469+<a role="presentation" data-href="/demo/tree/feature/x">
468470 <span class="lbl">feature/x</span>
469471 <span class="k">branch</span>
470472 </a>
471473 </li>
472474 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
473-<a href="/demo/tree/main" tabindex="-1">
475+<a role="presentation" data-href="/demo/tree/main">
474476 <span class="lbl">main</span>
475477 <span class="k">branch</span>
476478 </a>
477479 </li>
478480 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0">
479-<a href="/demo/tree/v0.1.0" tabindex="-1">
481+<a role="presentation" data-href="/demo/tree/v0.1.0">
480482 <span class="lbl">v0.1.0</span>
481483 <span class="k">tag</span>
482484 </a>
483485 </li>
484486 <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0">
485-<a href="/demo/tree/v0.2.0" tabindex="-1">
487+<a role="presentation" data-href="/demo/tree/v0.2.0">
486488 <span class="lbl">v0.2.0</span>
487489 <span class="k">tag</span>
488490 </a>
489491 </li>
490492 <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet">
491-<a href="/quiet" tabindex="-1">
493+<a role="presentation" data-href="/quiet">
492494 <span class="lbl">quiet</span>
493495 <span class="k">repo</span>
494496 </a>
495497 </li>
496498 <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
497-<a href="#" data-fire="keys" tabindex="-1">
499+<a role="presentation" data-fire="keys">
498500 <span class="lbl">turn single-key shortcuts off</span>
499501 <span class="k">action</span>
500502 </a>
M internal/app/testdata/web/tree-nested.html +21 -19
@@ -117,9 +117,9 @@
117117 <kbd>j</kbd>
118118 <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p>
119119 </div>
120-<div id="finder" class="tree-card" hidden>
121-<p id="finder-status" class="dim" aria-live="polite">
120+<p id="finder-status" class="dim" role="status">
122121 </p>
122+<div id="finder" class="tree-card" hidden>
123123 <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files">
124124 </ul>
125125 </div>
@@ -156,18 +156,20 @@
156156 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
157157 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
158158 </footer>
159-<div class="hint" role="status" aria-live="polite">
160-<span class="hint-nav" :text="document.body.dataset.nav || ''">
159+<div class="hint">
160+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
161161 </span>
162+<span role="status" aria-live="polite">
162163 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
163164 </span>
164165 <span class="hint-flash" :text="document.body.dataset.flash || ''">
165166 </span>
167+</span>
166168 </div>
167169 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
168170 <div class="panel-head">
169171 <h2>Keyboard shortcuts</h2>
170-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
172+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
171173 <kbd>esc</kbd> close</button>
172174 </div>
173175 <div class="keys-pref">
@@ -301,7 +303,7 @@
301303 <div class="panel-head">
302304 <label for="refs-filter" class="sr">filter branches and tags</label>
303305 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
304-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
306+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
305307 <kbd>esc</kbd>
306308 </button>
307309 </div>
@@ -336,79 +338,79 @@
336338 <div class="panel-head">
337339 <label for="cmd-q" class="sr">go to</label>
338340 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
339-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
341+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
340342 <kbd>esc</kbd>
341343 </button>
342344 </div>
343345 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
344346 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
345-<a href="/" tabindex="-1">
347+<a role="presentation" data-href="/">
346348 <span class="lbl">home</span>
347349 <span class="k">go</span>
348350 </a>
349351 </li>
350352 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
351-<a href="/files" tabindex="-1">
353+<a role="presentation" data-href="/files">
352354 <span class="lbl">summary</span>
353355 <span class="k">section</span>
354356 </a>
355357 </li>
356358 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
357-<a href="/files/tree/main" tabindex="-1">
359+<a role="presentation" data-href="/files/tree/main">
358360 <span class="lbl">files</span>
359361 <span class="k">section</span>
360362 </a>
361363 </li>
362364 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
363-<a href="/files/commits/main" tabindex="-1">
365+<a role="presentation" data-href="/files/commits/main">
364366 <span class="lbl">commits</span>
365367 <span class="k">section</span>
366368 </a>
367369 </li>
368370 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
369-<a href="/files/refs" tabindex="-1">
371+<a role="presentation" data-href="/files/refs">
370372 <span class="lbl">branches &amp; tags</span>
371373 <span class="k">section</span>
372374 </a>
373375 </li>
374376 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
375-<a href="/files/merge-requests" tabindex="-1">
377+<a role="presentation" data-href="/files/merge-requests">
376378 <span class="lbl">merge requests</span>
377379 <span class="k">section</span>
378380 </a>
379381 </li>
380382 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
381-<a href="/files/releases" tabindex="-1">
383+<a role="presentation" data-href="/files/releases">
382384 <span class="lbl">releases</span>
383385 <span class="k">section</span>
384386 </a>
385387 </li>
386388 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
387-<a href="/files/actions" tabindex="-1">
389+<a role="presentation" data-href="/files/actions">
388390 <span class="lbl">actions</span>
389391 <span class="k">section</span>
390392 </a>
391393 </li>
392394 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
393-<a href="/files/tree/feature/x" tabindex="-1">
395+<a role="presentation" data-href="/files/tree/feature/x">
394396 <span class="lbl">feature/x</span>
395397 <span class="k">branch</span>
396398 </a>
397399 </li>
398400 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
399-<a href="/files/tree/main" tabindex="-1">
401+<a role="presentation" data-href="/files/tree/main">
400402 <span class="lbl">main</span>
401403 <span class="k">branch</span>
402404 </a>
403405 </li>
404406 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
405-<a href="/files/tree/v1.0" tabindex="-1">
407+<a role="presentation" data-href="/files/tree/v1.0">
406408 <span class="lbl">v1.0</span>
407409 <span class="k">tag</span>
408410 </a>
409411 </li>
410412 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
411-<a href="#" data-fire="keys" tabindex="-1">
413+<a role="presentation" data-fire="keys">
412414 <span class="lbl">turn single-key shortcuts off</span>
413415 <span class="k">action</span>
414416 </a>
M internal/app/testdata/web/tree-root.html +21 -19
@@ -111,9 +111,9 @@
111111 <kbd>j</kbd>
112112 <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p>
113113 </div>
114-<div id="finder" class="tree-card" hidden>
115-<p id="finder-status" class="dim" aria-live="polite">
114+<p id="finder-status" class="dim" role="status">
116115 </p>
116+<div id="finder" class="tree-card" hidden>
117117 <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files">
118118 </ul>
119119 </div>
@@ -377,18 +377,20 @@
377377 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
378378 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
379379 </footer>
380-<div class="hint" role="status" aria-live="polite">
381-<span class="hint-nav" :text="document.body.dataset.nav || ''">
380+<div class="hint">
381+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
382382 </span>
383+<span role="status" aria-live="polite">
383384 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
384385 </span>
385386 <span class="hint-flash" :text="document.body.dataset.flash || ''">
386387 </span>
388+</span>
387389 </div>
388390 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
389391 <div class="panel-head">
390392 <h2>Keyboard shortcuts</h2>
391-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
393+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
392394 <kbd>esc</kbd> close</button>
393395 </div>
394396 <div class="keys-pref">
@@ -522,7 +524,7 @@
522524 <div class="panel-head">
523525 <label for="refs-filter" class="sr">filter branches and tags</label>
524526 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
525-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
527+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
526528 <kbd>esc</kbd>
527529 </button>
528530 </div>
@@ -557,79 +559,79 @@
557559 <div class="panel-head">
558560 <label for="cmd-q" class="sr">go to</label>
559561 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
560-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
562+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
561563 <kbd>esc</kbd>
562564 </button>
563565 </div>
564566 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
565567 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
566-<a href="/" tabindex="-1">
568+<a role="presentation" data-href="/">
567569 <span class="lbl">home</span>
568570 <span class="k">go</span>
569571 </a>
570572 </li>
571573 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
572-<a href="/files" tabindex="-1">
574+<a role="presentation" data-href="/files">
573575 <span class="lbl">summary</span>
574576 <span class="k">section</span>
575577 </a>
576578 </li>
577579 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
578-<a href="/files/tree/main" tabindex="-1">
580+<a role="presentation" data-href="/files/tree/main">
579581 <span class="lbl">files</span>
580582 <span class="k">section</span>
581583 </a>
582584 </li>
583585 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
584-<a href="/files/commits/main" tabindex="-1">
586+<a role="presentation" data-href="/files/commits/main">
585587 <span class="lbl">commits</span>
586588 <span class="k">section</span>
587589 </a>
588590 </li>
589591 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
590-<a href="/files/refs" tabindex="-1">
592+<a role="presentation" data-href="/files/refs">
591593 <span class="lbl">branches &amp; tags</span>
592594 <span class="k">section</span>
593595 </a>
594596 </li>
595597 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
596-<a href="/files/merge-requests" tabindex="-1">
598+<a role="presentation" data-href="/files/merge-requests">
597599 <span class="lbl">merge requests</span>
598600 <span class="k">section</span>
599601 </a>
600602 </li>
601603 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
602-<a href="/files/releases" tabindex="-1">
604+<a role="presentation" data-href="/files/releases">
603605 <span class="lbl">releases</span>
604606 <span class="k">section</span>
605607 </a>
606608 </li>
607609 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
608-<a href="/files/actions" tabindex="-1">
610+<a role="presentation" data-href="/files/actions">
609611 <span class="lbl">actions</span>
610612 <span class="k">section</span>
611613 </a>
612614 </li>
613615 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
614-<a href="/files/tree/feature/x" tabindex="-1">
616+<a role="presentation" data-href="/files/tree/feature/x">
615617 <span class="lbl">feature/x</span>
616618 <span class="k">branch</span>
617619 </a>
618620 </li>
619621 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
620-<a href="/files/tree/main" tabindex="-1">
622+<a role="presentation" data-href="/files/tree/main">
621623 <span class="lbl">main</span>
622624 <span class="k">branch</span>
623625 </a>
624626 </li>
625627 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
626-<a href="/files/tree/v1.0" tabindex="-1">
628+<a role="presentation" data-href="/files/tree/v1.0">
627629 <span class="lbl">v1.0</span>
628630 <span class="k">tag</span>
629631 </a>
630632 </li>
631633 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
632-<a href="#" data-fire="keys" tabindex="-1">
634+<a role="presentation" data-fire="keys">
633635 <span class="lbl">turn single-key shortcuts off</span>
634636 <span class="k">action</span>
635637 </a>
M internal/app/testdata/web/tree-slash-ref.html +21 -19
@@ -114,9 +114,9 @@
114114 <kbd>j</kbd>
115115 <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p>
116116 </div>
117-<div id="finder" class="tree-card" hidden>
118-<p id="finder-status" class="dim" aria-live="polite">
117+<p id="finder-status" class="dim" role="status">
119118 </p>
119+<div id="finder" class="tree-card" hidden>
120120 <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files">
121121 </ul>
122122 </div>
@@ -153,18 +153,20 @@
153153 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
154154 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
155155 </footer>
156-<div class="hint" role="status" aria-live="polite">
157-<span class="hint-nav" :text="document.body.dataset.nav || ''">
156+<div class="hint">
157+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
158158 </span>
159+<span role="status" aria-live="polite">
159160 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
160161 </span>
161162 <span class="hint-flash" :text="document.body.dataset.flash || ''">
162163 </span>
164+</span>
163165 </div>
164166 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
165167 <div class="panel-head">
166168 <h2>Keyboard shortcuts</h2>
167-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
169+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
168170 <kbd>esc</kbd> close</button>
169171 </div>
170172 <div class="keys-pref">
@@ -298,7 +300,7 @@
298300 <div class="panel-head">
299301 <label for="refs-filter" class="sr">filter branches and tags</label>
300302 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
301-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
303+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
302304 <kbd>esc</kbd>
303305 </button>
304306 </div>
@@ -333,79 +335,79 @@
333335 <div class="panel-head">
334336 <label for="cmd-q" class="sr">go to</label>
335337 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
336-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
338+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
337339 <kbd>esc</kbd>
338340 </button>
339341 </div>
340342 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
341343 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
342-<a href="/" tabindex="-1">
344+<a role="presentation" data-href="/">
343345 <span class="lbl">home</span>
344346 <span class="k">go</span>
345347 </a>
346348 </li>
347349 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
348-<a href="/files" tabindex="-1">
350+<a role="presentation" data-href="/files">
349351 <span class="lbl">summary</span>
350352 <span class="k">section</span>
351353 </a>
352354 </li>
353355 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
354-<a href="/files/tree/main" tabindex="-1">
356+<a role="presentation" data-href="/files/tree/main">
355357 <span class="lbl">files</span>
356358 <span class="k">section</span>
357359 </a>
358360 </li>
359361 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
360-<a href="/files/commits/main" tabindex="-1">
362+<a role="presentation" data-href="/files/commits/main">
361363 <span class="lbl">commits</span>
362364 <span class="k">section</span>
363365 </a>
364366 </li>
365367 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
366-<a href="/files/refs" tabindex="-1">
368+<a role="presentation" data-href="/files/refs">
367369 <span class="lbl">branches &amp; tags</span>
368370 <span class="k">section</span>
369371 </a>
370372 </li>
371373 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
372-<a href="/files/merge-requests" tabindex="-1">
374+<a role="presentation" data-href="/files/merge-requests">
373375 <span class="lbl">merge requests</span>
374376 <span class="k">section</span>
375377 </a>
376378 </li>
377379 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
378-<a href="/files/releases" tabindex="-1">
380+<a role="presentation" data-href="/files/releases">
379381 <span class="lbl">releases</span>
380382 <span class="k">section</span>
381383 </a>
382384 </li>
383385 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
384-<a href="/files/actions" tabindex="-1">
386+<a role="presentation" data-href="/files/actions">
385387 <span class="lbl">actions</span>
386388 <span class="k">section</span>
387389 </a>
388390 </li>
389391 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
390-<a href="/files/tree/feature/x" tabindex="-1">
392+<a role="presentation" data-href="/files/tree/feature/x">
391393 <span class="lbl">feature/x</span>
392394 <span class="k">branch</span>
393395 </a>
394396 </li>
395397 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
396-<a href="/files/tree/main" tabindex="-1">
398+<a role="presentation" data-href="/files/tree/main">
397399 <span class="lbl">main</span>
398400 <span class="k">branch</span>
399401 </a>
400402 </li>
401403 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
402-<a href="/files/tree/v1.0" tabindex="-1">
404+<a role="presentation" data-href="/files/tree/v1.0">
403405 <span class="lbl">v1.0</span>
404406 <span class="k">tag</span>
405407 </a>
406408 </li>
407409 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
408-<a href="#" data-fire="keys" tabindex="-1">
410+<a role="presentation" data-fire="keys">
409411 <span class="lbl">turn single-key shortcuts off</span>
410412 <span class="k">action</span>
411413 </a>
M internal/app/testdata/web/tree-subdir.html +21 -19
@@ -114,9 +114,9 @@
114114 <kbd>j</kbd>
115115 <kbd>k</kbd> move · <kbd>l</kbd> open · <kbd>h</kbd> back</p>
116116 </div>
117-<div id="finder" class="tree-card" hidden>
118-<p id="finder-status" class="dim" aria-live="polite">
117+<p id="finder-status" class="dim" role="status">
119118 </p>
119+<div id="finder" class="tree-card" hidden>
120120 <ul id="finder-list" class="cmd-list" role="listbox" aria-label="Files">
121121 </ul>
122122 </div>
@@ -169,18 +169,20 @@
169169 <button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
170170 <kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
171171 </footer>
172-<div class="hint" role="status" aria-live="polite">
173-<span class="hint-nav" :text="document.body.dataset.nav || ''">
172+<div class="hint">
173+<span aria-hidden="true" class="hint-nav" :text="document.body.dataset.nav || ''">
174174 </span>
175+<span role="status" aria-live="polite">
175176 <span class="hint-sel" :text="document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''">
176177 </span>
177178 <span class="hint-flash" :text="document.body.dataset.flash || ''">
178179 </span>
180+</span>
179181 </div>
180182 <dialog id="panel-help" class="panel modal" aria-label="Keyboard shortcuts" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
181183 <div class="panel-head">
182184 <h2>Keyboard shortcuts</h2>
183-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
185+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
184186 <kbd>esc</kbd> close</button>
185187 </div>
186188 <div class="keys-pref">
@@ -314,7 +316,7 @@
314316 <div class="panel-head">
315317 <label for="refs-filter" class="sr">filter branches and tags</label>
316318 <input id="refs-filter" type="search" placeholder="Switch branch or tag…" autocomplete="off" data-pick="#panel-refs li" hx-on:input="ermite.filter(this)">
317-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
319+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
318320 <kbd>esc</kbd>
319321 </button>
320322 </div>
@@ -349,79 +351,79 @@
349351 <div class="panel-head">
350352 <label for="cmd-q" class="sr">go to</label>
351353 <input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="cmd-list" aria-activedescendant="cmd-o0" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">
352-<button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
354+<button type="button" class="panel-x" aria-label="Close (esc)" hx-on:click="closest('dialog').close()">
353355 <kbd>esc</kbd>
354356 </button>
355357 </div>
356358 <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
357359 <li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
358-<a href="/" tabindex="-1">
360+<a role="presentation" data-href="/">
359361 <span class="lbl">home</span>
360362 <span class="k">go</span>
361363 </a>
362364 </li>
363365 <li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
364-<a href="/files" tabindex="-1">
366+<a role="presentation" data-href="/files">
365367 <span class="lbl">summary</span>
366368 <span class="k">section</span>
367369 </a>
368370 </li>
369371 <li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
370-<a href="/files/tree/main" tabindex="-1">
372+<a role="presentation" data-href="/files/tree/main">
371373 <span class="lbl">files</span>
372374 <span class="k">section</span>
373375 </a>
374376 </li>
375377 <li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
376-<a href="/files/commits/main" tabindex="-1">
378+<a role="presentation" data-href="/files/commits/main">
377379 <span class="lbl">commits</span>
378380 <span class="k">section</span>
379381 </a>
380382 </li>
381383 <li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
382-<a href="/files/refs" tabindex="-1">
384+<a role="presentation" data-href="/files/refs">
383385 <span class="lbl">branches &amp; tags</span>
384386 <span class="k">section</span>
385387 </a>
386388 </li>
387389 <li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
388-<a href="/files/merge-requests" tabindex="-1">
390+<a role="presentation" data-href="/files/merge-requests">
389391 <span class="lbl">merge requests</span>
390392 <span class="k">section</span>
391393 </a>
392394 </li>
393395 <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases">
394-<a href="/files/releases" tabindex="-1">
396+<a role="presentation" data-href="/files/releases">
395397 <span class="lbl">releases</span>
396398 <span class="k">section</span>
397399 </a>
398400 </li>
399401 <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions">
400-<a href="/files/actions" tabindex="-1">
402+<a role="presentation" data-href="/files/actions">
401403 <span class="lbl">actions</span>
402404 <span class="k">section</span>
403405 </a>
404406 </li>
405407 <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x">
406-<a href="/files/tree/feature/x" tabindex="-1">
408+<a role="presentation" data-href="/files/tree/feature/x">
407409 <span class="lbl">feature/x</span>
408410 <span class="k">branch</span>
409411 </a>
410412 </li>
411413 <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main">
412-<a href="/files/tree/main" tabindex="-1">
414+<a role="presentation" data-href="/files/tree/main">
413415 <span class="lbl">main</span>
414416 <span class="k">branch</span>
415417 </a>
416418 </li>
417419 <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0">
418-<a href="/files/tree/v1.0" tabindex="-1">
420+<a role="presentation" data-href="/files/tree/v1.0">
419421 <span class="lbl">v1.0</span>
420422 <span class="k">tag</span>
421423 </a>
422424 </li>
423425 <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
424-<a href="#" data-fire="keys" tabindex="-1">
426+<a role="presentation" data-fire="keys">
425427 <span class="lbl">turn single-key shortcuts off</span>
426428 <span class="k">action</span>
427429 </a>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close