detached ef38ad0

fix(web): accessibility of the command palette, live swaps and shortcuts (#59)

commit
ef38ad003e0ebee3afecd42d576ac4228405050e
author
brice · · 2026-10-18 06:39 UTC
committer
bnema · · 2026-10-18 06:39 UTC
parent
79d6b04
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.21.0
45 files · +1393 -451
M internal/adapters/web/diff.templ +1 -1
@@ -54,7 +54,7 @@ templ Commit(l Layout, p CommitPage) {
54 }54 }
55 </dl>55 </dl>
56 </header>56 </header>
57- <div class="diff-tools" role="toolbar" aria-label="Diff view">57+ <div class="diff-tools" role="group" aria-label="Diff view">
58 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>58 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
59 <span class="grow"></span>59 <span class="grow"></span>
60 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)60 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
M internal/adapters/web/diff_templ.go +1 -1 generated file: collapsed
A internal/adapters/web/jstest/a11y.test.mjs +319 -0
@@ -0,0 +1,319 @@
1+// Tests of the keyboard accessibility of static/js/ermite.js and files.js on
2+// the small DOM of dom.mjs: the "go to" palette as a combobox, Escape in the
3+// file finder, the focus kept through a live swap, and the preference that
4+// turns the single-key shortcuts off. Run with
5+// node --test 'internal/adapters/web/jstest/*.test.mjs'
6+import assert from 'node:assert/strict';
7+import { test } from 'node:test';
8+import { dom, El, el, flushFrames, installDom, settle } from './dom.mjs';
9+
10+// The few things ermite.js uses that dom.mjs does not have.
11+El.prototype.replaceWith = function (n) {
12+ const p = this.parent;
13+ const i = p.nodes.indexOf(this);
14+ const [added] = p.adopt([n]);
15+ p.nodes.splice(i, 1, added);
16+ this.parent = null;
17+};
18+El.prototype.contains = function (e) { for (let n = e; n; n = n.parent) if (n === this) return true; return false; };
19+Object.defineProperty(El.prototype, 'isConnected', { get() { let n = this; while (n.parent) n = n.parent; return n === dom.body; } });
20+El.prototype.toggleAttribute = function (k, on) { if (on) this.setAttribute(k, ''); else this.removeAttribute(k); return on; };
21+El.prototype.dispatchEvent = function (ev) { (this.events ||= []).push(ev.type); };
22+// The focusable controls of a fragment, as the selector of ermiteLive says.
23+const nativeAll = El.prototype.querySelectorAll;
24+El.prototype.querySelectorAll = function (sel) {
25+ if (sel === '[data-rows] > [data-cursor]') return this.querySelectorAll('li[data-cursor]'); // no cursor in these tests
26+ if (sel === '[data-rows] > *') return [];
27+ if (!sel.startsWith('a[href], button')) return nativeAll.call(this, sel);
28+ const out = [];
29+ const walk = (e) => e.children.forEach((c) => {
30+ if ((c.tagName === 'A' && c.hasAttribute('href')) || ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY'].includes(c.tagName) || c.hasAttribute('tabindex')) out.push(c);
31+ walk(c);
32+ });
33+ walk(this);
34+ return out;
35+};
36+
37+let active = null;
38+// As in a browser, an element inside a hidden one does not take the focus.
39+El.prototype.focus = function (opts) {
40+ dom.focused.push({ el: this, opts });
41+ if (!this.closest('[hidden]')) active = this;
42+};
43+const doc = installDom({ parseHTML: (text) => parsed[text] });
44+const parsed = {};
45+Object.defineProperty(doc, 'activeElement', { get: () => active ?? dom.body });
46+doc.documentElement = { classList: { add() {} } };
47+const query = { all: doc.querySelectorAll, one: doc.querySelector };
48+doc.querySelectorAll = () => []; // the page-wide queries of the top of ermite.js
49+doc.querySelector = () => null;
50+
51+const store = new Map();
52+globalThis.localStorage = { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => store.set(k, String(v)) };
53+const listeners = {};
54+globalThis.addEventListener = (type, f) => (listeners[type] ||= []).push(f);
55+globalThis.scrollTo = () => {};
56+globalThis.fetch = async () => ({ ok: true, json: async () => ({ paths: ['cmd/main.go', 'README.md'] }) });
57+
58+await import('../static/js/files.js');
59+await import('../static/js/ermite.js');
60+doc.querySelectorAll = query.all;
61+doc.querySelector = query.one;
62+const F = window.ermiteFinder;
63+const K = window.ermiteKeys;
64+
65+// keydown runs an element handler, then the window ones unless stopped, as a
66+// browser does.
67+function keydown(target, key, elementHandler, extra = {}) {
68+ const e = {
69+ key, target, isComposing: false, prevented: false, stopped: false, blurred: false,
70+ preventDefault() { this.prevented = true; },
71+ stopPropagation() { this.stopped = true; },
72+ ...extra,
73+ };
74+ target.closest ||= () => null;
75+ elementHandler?.(e);
76+ if (!e.stopped) for (const f of listeners.keydown) f(e);
77+ return e;
78+}
79+
80+// ---- the palette --------------------------------------------------------
81+
82+function palette() {
83+ const li = (k, label) => el('li', { attrs: { 'data-k': k } }, el('a', { attrs: { href: '/' + label } }));
84+ const items = [li('go home', 'home'), li('section files', 'files'), li('branch main', 'main')];
85+ items[0].setAttribute('data-cursor', '');
86+ const input = el('input', { id: 'cmd-q', attrs: { role: 'combobox', 'aria-controls': 'cmd-list' } });
87+ const list = el('ul', { id: 'cmd-list' }, ...items);
88+ dom.body.replaceChildren(input, list);
89+ return { input, list, items };
90+}
91+
92+test('the palette: options, selected option and aria-activedescendant follow the cursor', () => {
93+ const p = palette();
94+ window.ermiteCmd.sync();
95+ assert.deepEqual(p.items.map((i) => i.getAttribute('role')), ['option', 'option', 'option']);
96+ assert.equal(new Set(p.items.map((i) => i.id)).size, 3, 'unique ids');
97+ assert.equal(p.input.getAttribute('aria-expanded'), 'true');
98+ assert.equal(p.input.getAttribute('aria-activedescendant'), p.items[0].id);
99+ assert.deepEqual(p.items.map((i) => i.getAttribute('aria-selected')), ['true', 'false', 'false']);
100+
101+ window.ermite.cmdKey({ key: 'ArrowDown', preventDefault() {} });
102+ assert.equal(p.input.getAttribute('aria-activedescendant'), p.items[1].id);
103+ assert.deepEqual(p.items.map((i) => i.getAttribute('aria-selected')), ['false', 'true', 'false']);
104+ assert.ok(p.items[1].hasAttribute('data-cursor') && !p.items[0].hasAttribute('data-cursor'));
105+
106+ window.ermite.cmdKey({ key: 'ArrowUp', preventDefault() {} });
107+ assert.equal(p.input.getAttribute('aria-activedescendant'), p.items[0].id);
108+});
109+
110+test('the palette: a filter moves the active option, no match empties it and collapses the popup', () => {
111+ const p = palette();
112+ p.input.value = 'branch';
113+ window.ermite.cmdFilter(p.input);
114+ assert.equal(p.input.getAttribute('aria-activedescendant'), p.items[2].id);
115+ assert.equal(p.items[2].getAttribute('aria-selected'), 'true');
116+ assert.equal(p.items[0].getAttribute('aria-selected'), 'false');
117+ p.input.value = 'zzz';
118+ window.ermite.cmdFilter(p.input);
119+ assert.equal(p.input.getAttribute('aria-expanded'), 'false');
120+ assert.equal(p.input.getAttribute('aria-activedescendant'), null);
121+});
122+
123+test('the palette: the file hits that arrive later get ids, roles and the active option', async () => {
124+ const p = palette();
125+ dom.body.setAttribute('data-paths', '/paths.json');
126+ dom.body.setAttribute('data-blob', '/blob/main');
127+ p.input.value = 'main';
128+ window.ermite.cmdFilter(p.input); // the branch "main" is the active option
129+ assert.equal(p.input.getAttribute('aria-activedescendant'), p.items[2].id);
130+ F.palette(p.input);
131+ flushFrames();
132+ await settle();
133+ const hits = p.list.children.filter((li) => li.className === 'finder-hit');
134+ assert.ok(hits.length > 0, 'a file hit came');
135+ for (const h of hits) {
136+ assert.equal(h.getAttribute('role'), 'option');
137+ assert.ok(h.id);
138+ }
139+ assert.equal(p.list.children.length, new Set(p.list.children.map((i) => i.id)).size, 'ids stay unique');
140+ // down to the hit: the field points at it
141+ window.ermite.cmdKey({ key: 'ArrowDown', preventDefault() {} });
142+ assert.equal(p.input.getAttribute('aria-activedescendant'), hits[0].id);
143+ assert.equal(hits[0].getAttribute('aria-selected'), 'true');
144+ dom.body.removeAttribute('data-paths');
145+});
146+
147+// ---- the file finder ----------------------------------------------------
148+
149+function finder() {
150+ const q = el('input', { id: 'finder-q', attrs: { role: 'combobox' } });
151+ const rows = ['a', 'b'].map((n) => el('tr', { attrs: { 'data-nm': n } }));
152+ rows[0].setAttribute('data-cursor', '');
153+ const treeRows = el('tbody', { id: 'tree-rows', attrs: { 'data-rows': '' } }, ...rows);
154+ const list = el('ul', { id: 'finder-list' });
155+ dom.body.replaceChildren(q, el('div', { id: 'tree-view' }, treeRows), el('div', { id: 'finder', hidden: true }, el('p', { id: 'finder-status' }), list));
156+ dom.body.setAttribute('data-paths', '/paths.json');
157+ dom.body.setAttribute('data-blob', '/blob/main');
158+ return { q, list };
159+}
160+
161+test('Escape in the finder clears the query, keeps the focus in the field and does not reach the global handler', async () => {
162+ const f = finder();
163+ f.q.value = 'main';
164+ F.input(f.q);
165+ flushFrames();
166+ await settle();
167+ active = f.q;
168+ let blurred = false;
169+ f.q.blur = () => { blurred = true; };
170+ f.q.closest = () => f.q; // the global handler sees a field being typed in
171+ const e = keydown(f.q, 'Escape', (ev) => F.key(f.q, ev));
172+ assert.equal(f.q.value, '');
173+ assert.equal(e.stopped, true);
174+ assert.equal(e.prevented, true);
175+ assert.equal(blurred, false, 'the field keeps the focus');
176+ assert.equal(active, f.q);
177+ // Escape again, with nothing typed: the field is left (global handler)
178+ const e2 = keydown(f.q, 'Escape', (ev) => F.key(f.q, ev));
179+ assert.equal(e2.stopped, false);
180+ assert.equal(blurred, true);
181+});
182+
183+test('the finder follows the cursor moved from outside (j / k)', async () => {
184+ const f = finder();
185+ f.q.value = 'm';
186+ F.input(f.q);
187+ flushFrames();
188+ await settle();
189+ const [first, second] = f.list.children;
190+ assert.equal(f.q.getAttribute('aria-activedescendant'), first.id);
191+ // ermite.move: the cursor goes to the next option
192+ first.removeAttribute('data-cursor');
193+ second.setAttribute('data-cursor', '');
194+ F.sync();
195+ assert.equal(second.getAttribute('aria-selected'), 'true');
196+ assert.equal(first.getAttribute('aria-selected'), 'false');
197+ assert.equal(f.q.getAttribute('aria-activedescendant'), second.id);
198+});
199+
200+// ---- the focus through a live swap --------------------------------------
201+
202+function rows() {
203+ const mk = (id, href) => el('li', { id }, el('a', { attrs: { href }, id: id + '-a' }), el('button', { id: id + '-b' }));
204+ const main = el('main', { id: 'main', attrs: { tabindex: '-1' } });
205+ const list = el('ul', { id: 'repo-rows' }, mk('repo-a', '/a'), mk('repo-b', '/b'), mk('repo-c', '/c'));
206+ dom.body.replaceChildren(main, list);
207+ return { main, list, mk };
208+}
209+const swapWith = (html, ...nodes) => { parsed[html] = nodes; window.ermiteLive.swap(html); };
210+
211+test('a swap that replaces the focused row puts the focus on the same control of the new one', () => {
212+ const r = rows();
213+ const old = r.list.children[1];
214+ active = old.children[0];
215+ dom.focused = [];
216+ const fresh = r.mk('repo-b', '/b');
217+ swapWith('b1', fresh);
218+ assert.equal(active, fresh.children[0], 'the link with the same href');
219+ assert.equal(dom.focused.at(-1).opts.preventScroll, true);
220+ // the second control
221+ active = fresh.children[1];
222+ const fresh2 = r.mk('repo-b', '/b');
223+ swapWith('b2', fresh2);
224+ assert.equal(active, fresh2.children[1], 'the button with the same id');
225+});
226+
227+test('a swap with the focus elsewhere does not touch it', () => {
228+ const r = rows();
229+ active = r.list.children[0].children[0];
230+ dom.focused = [];
231+ swapWith('b3', r.mk('repo-b', '/b'));
232+ assert.equal(dom.focused.length, 0);
233+ assert.equal(active, r.list.children[0].children[0]);
234+});
235+
236+test('a removed row hands the focus to the next row, else the previous, else the content', () => {
237+ const r = rows();
238+ active = r.list.children[1].children[0];
239+ const gone = (id) => { const g = el('li', { id, attrs: { 'data-gone': '' } }); return g; };
240+ swapWith('g1', gone('repo-b'));
241+ assert.equal(active, r.list.children[1].children[0], 'repo-c, which took its place');
242+ assert.equal(r.list.children[1].id, 'repo-c');
243+ active = r.list.children[1].children[0];
244+ swapWith('g2', gone('repo-c'));
245+ assert.equal(active, r.list.children[0].children[0], 'repo-a, the previous row');
246+ swapWith('g3', gone('repo-a'));
247+ // focus was on repo-a's link? it was handed there: now it is removed too
248+ assert.equal(active, r.main);
249+});
250+
251+test('a removed row skips the rows a filter hides', () => {
252+ const r = rows();
253+ const [a, b, c] = r.list.children;
254+ b.hidden = true; // filtered out
255+ active = a.children[0];
256+ swapWith('g4', el('li', { id: 'repo-a', attrs: { 'data-gone': '' } }));
257+ assert.equal(active, c.children[0], 'repo-c, the next shown row, not the hidden repo-b');
258+ c.hidden = true;
259+ swapWith('g5', el('li', { id: 'repo-c', attrs: { 'data-gone': '' } }));
260+ assert.equal(active, r.main, 'no shown row left: the content');
261+});
262+
263+// ---- single-character shortcuts -----------------------------------------
264+
265+test('shortcuts off: single characters do nothing, ctrl k, escape and enter still work', () => {
266+ const events = () => dom.body.events.splice(0);
267+ dom.body.events = [];
268+ doc.querySelector = () => null;
269+ const press = (key, extra) => keydown({ closest: () => null }, key, null, extra);
270+ press('j');
271+ assert.deepEqual(events(), ['k-j'], 'on by default');
272+
273+ K.set(true);
274+ assert.equal(store.get('keys-off'), '1', 'persisted');
275+ for (const k of ['j', 'k', '?', ':', 'G', 's', 'y']) press(k);
276+ press('g'); press('h');
277+ assert.deepEqual(events(), [], 'nothing fires');
278+ press('k', { ctrlKey: true });
279+ press('Escape');
280+ assert.deepEqual(events(), ['k-cmd', 'k-esc']);
281+
282+ K.set(false);
283+ assert.equal(store.get('keys-off'), '', 'persisted off');
284+ press('j');
285+ assert.deepEqual(events(), ['k-j']);
286+});
287+
288+test('shortcuts off: the checkbox, the note and the palette action show the state', () => {
289+ const box = el('input', { id: 'keys-off', attrs: { type: 'checkbox' } });
290+ const note = el('p', { id: 'keys-note' });
291+ const lbl = el('span', { cls: 'lbl' });
292+ const list = el('ul', { id: 'cmd-list' }, el('li', {}, el('a', { attrs: { 'data-fire': 'keys' } }, lbl)));
293+ dom.body.replaceChildren(box, note, list);
294+ const find = doc.querySelector;
295+ doc.querySelector = (sel) => (sel === '#cmd-list [data-fire="keys"] .lbl' ? lbl : null);
296+ K.set(true);
297+ assert.equal(box.checked, true);
298+ assert.match(note.textContent, /are off/);
299+ assert.match(lbl.textContent, /on$/);
300+ K.set(false);
301+ assert.equal(box.checked, false);
302+ assert.match(note.textContent, /are on/);
303+ assert.match(lbl.textContent, /off$/);
304+ doc.querySelector = find;
305+});
306+
307+test('the preference is read from storage when the page loads', async () => {
308+ store.set('keys-off', '1');
309+ listeners.keydown = [];
310+ const src = await import('node:fs').then((m) => m.readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8'));
311+ doc.querySelectorAll = () => [];
312+ doc.querySelector = () => null;
313+ new Function(src)(); // a second page load
314+ assert.equal(window.ermiteKeys.off(), true);
315+ dom.body.events = [];
316+ keydown({ closest: () => null }, 'j');
317+ assert.deepEqual(dom.body.events, []);
318+ store.set('keys-off', '');
319+});
M internal/adapters/web/jstest/finder.test.mjs +1 -1
@@ -49,7 +49,7 @@ function page() {
49 }49 }
50 50
51 const key = (k, extra = {}) => {51 const key = (k, extra = {}) => {
52- const e = { key: k, isComposing: false, prevented: false, preventDefault() { this.prevented = true; }, ...extra };52+ const e = { key: k, isComposing: false, prevented: false, preventDefault() { this.prevented = true; }, stopPropagation() {}, ...extra };
53 return e;53 return e;
54 };54 };
55 const status = () => document.getElementById('finder-status').textContent;55 const status = () => document.getElementById('finder-status').textContent;
M internal/adapters/web/jstest/tree.test.mjs +12 -0
@@ -276,6 +276,18 @@ test('init: one tab stop, the row under the cursor; name links are out of the ta
276 assert.equal(body.children[1].getAttribute('tabindex'), '-1');276 assert.equal(body.children[1].getAttribute('tabindex'), '-1');
277 });277 });
278 278
279+test('l or h before any focus in the tree brings the focus to the cursor row', async () => {
280+ pending.clear();
281+ const body = table('P/', 'f');
282+ assert.equal(dom.active, null, 'the page has the focus after a load');
283+ T.key(true); // opens P/, the cursor stays on it
284+ assert.equal(dom.active, body.children[0]);
285+ await answer('/rows/P/', 'a');
286+ dom.active = null;
287+ T.key(false); // closes P/
288+ assert.equal(dom.active, body.children[0]);
289+});
290+
279 test('l and h move the focus with the cursor, and the tab stop with them', async () => {291 test('l and h move the focus with the cursor, and the tab stop with them', async () => {
280 pending.clear();292 pending.clear();
281 const body = table('P/', 'f');293 const body = table('P/', 'f');
M internal/adapters/web/keys.templ +2 -1
@@ -36,6 +36,7 @@ templ Keys() {
36 k-permalink from:body -> ermite.permalink();36 k-permalink from:body -> ermite.permalink();
37 k-merge from:body -> window.ermiteMR?.openMerge();37 k-merge from:body -> window.ermiteMR?.openMerge();
38 k-read from:body -> window.ermiteMR?.markRead();38 k-read from:body -> window.ermiteMR?.markRead();
39- k-gf from:body -> window.ermiteMR?.focusFiles()"39+ k-gf from:body -> window.ermiteMR?.focusFiles();
40+ k-keys from:body -> ermite.toggleKeys()"
40 ></div>41 ></div>
41 }42 }
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.templ +2 -2
@@ -358,7 +358,7 @@ templ mrRail(p MRPage) {
358 // request, the merge menu and the other actions. Each menu is a <details>: it358 // request, the merge menu and the other actions. Each menu is a <details>: it
359 // works without scripts; with them, M and the keys of the menu drive it.359 // works without scripts; with them, M and the keys of the menu drive it.
360 templ mrBar(p MRPage) {360 templ mrBar(p MRPage) {
361- <div class="diff-tools mr-bar" role="toolbar" aria-label="Diff view and merge">361+ <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
362 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>362 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
363 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)363 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
364 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)364 @viewLink(p.DiffView, "split", "Split", "d", p.SplitHref)
@@ -488,7 +488,7 @@ templ Compare(l Layout, p ComparePage) {
488 </form>488 </form>
489 }489 }
490 if p.HasDiff {490 if p.HasDiff {
491- <div class="diff-tools" role="toolbar" aria-label="Diff view">491+ <div class="diff-tools" role="group" aria-label="Diff view">
492 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>492 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
493 <span class="grow"></span>493 <span class="grow"></span>
494 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)494 @viewLink(p.DiffView, "unified", "Unified", "u", p.UnifiedHref)
M internal/adapters/web/mr_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/overlays.templ +15 -3
@@ -2,13 +2,20 @@
2 2
3 package web3 package web
4 4
5-import "strings"5+import (
6+ "strconv"
7+ "strings"
8+)
6 9
7 // OverlayHelp is the keyboard map panel; the keys of the merge request page10 // OverlayHelp is the keyboard map panel; the keys of the merge request page
8 // show on that page only.11 // show on that page only.
9 templ OverlayHelp(mr bool) {12 templ OverlayHelp(mr bool) {
10 <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()">13 <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()">
11 <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" hx-on:click="closest('dialog').close()"><kbd>esc</kbd> close</button></div>
15+ <div class="keys-pref">
16+ <label><input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"/> Turn off single-key shortcuts</label>
17+ <p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
18+ </div>
12 <dl class="keys">19 <dl class="keys">
13 <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div>20 <div><dt><kbd>ctrl k</kbd> <kbd>:</kbd></dt><dd>go to a section, branch, tag, file or repository</dd></div>
14 <div><dt><kbd>?</kbd></dt><dd>this list</dd></div>21 <div><dt><kbd>?</kbd></dt><dd>this list</dd></div>
@@ -104,16 +111,21 @@ templ OverlayCmd(items []CmdItem) {
104 placeholder="Go to a section, branch, tag, file or repository…"111 placeholder="Go to a section, branch, tag, file or repository…"
105 autocomplete="off"112 autocomplete="off"
106 spellcheck="false"113 spellcheck="false"
114+ role="combobox"
115+ aria-autocomplete="list"
116+ aria-expanded="true"
107 aria-controls="cmd-list"117 aria-controls="cmd-list"
118+ aria-activedescendant="cmd-o0"
108 hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)"119 hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)"
109 hx-on:keydown="ermite.cmdKey(event)"120 hx-on:keydown="ermite.cmdKey(event)"
110 />121 />
111 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>122 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()"><kbd>esc</kbd></button>
112 </div>123 </div>
113- <ul id="cmd-list" class="cmd-list" aria-label="Targets">124+ <ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
114 for i, c := range items {125 for i, c := range items {
115- <li 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>126+ <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>
116 }127 }
128+ <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>
117 </ul>129 </ul>
118 <p class="panel-foot"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p>130 <p class="panel-foot"><span><kbd>↑</kbd><kbd>↓</kbd> move</span><span><kbd>enter</kbd> open</span><span><kbd>esc</kbd> close</span></p>
119 </dialog>131 </dialog>
M internal/adapters/web/overlays_templ.go +72 -30 generated file: collapsed
M internal/adapters/web/static/css/25-panel.css +11 -4
@@ -21,7 +21,6 @@
21 & h2 { font-size: var(--fs); flex: 1; }21 & h2 { font-size: var(--fs); flex: 1; }
22 & input { flex: 1; min-width: 0; height: 32px; font-size: 16px; }22 & input { flex: 1; min-width: 0; height: 32px; font-size: 16px; }
23 & input::placeholder { color: var(--ink-mute); opacity: 1; }23 & input::placeholder { color: var(--ink-mute); opacity: 1; }
24- & input:focus-visible { outline: none; }
25 }24 }
26 .panel-x { color: var(--ink-mute); font-size: var(--fs-s); display: flex; gap: var(--s2); align-items: center; }25 .panel-x { color: var(--ink-mute); font-size: var(--fs-s); display: flex; gap: var(--s2); align-items: center; }
27 .panel-x:hover { color: var(--ink-hi); }26 .panel-x:hover { color: var(--ink-hi); }
@@ -60,10 +59,18 @@
60 }59 }
61 60
62 .keys {61 .keys {
63- display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); column-gap: var(--s5); row-gap: var(--s1);62+ display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); column-gap: var(--s5); row-gap: var(--s1);
64 padding: var(--s3) var(--s4) var(--s4) !important;63 padding: var(--s3) var(--s4) var(--s4) !important;
65 & > div { display: grid; grid-template-columns: 130px 1fr; gap: var(--s3); align-items: baseline; padding: var(--s1) 0; border-bottom: 1px solid var(--border); }64 & > div { display: grid; grid-template-columns: 130px 1fr; gap: var(--s3); align-items: baseline; padding: var(--s1) 0; border-bottom: 1px solid var(--border); }
66- & dt { white-space: nowrap; display: flex; gap: var(--s1); }65+ & dt { display: flex; flex-wrap: wrap; gap: var(--s1); min-width: 0; }
67- & dd { color: var(--ink-dim); font-size: var(--fs-s); }66+ & dd { color: var(--ink-dim); font-size: var(--fs-s); min-width: 0; overflow-wrap: anywhere; }
67+ @media (max-width: 480px) { & > div { grid-template-columns: minmax(0, 1fr); gap: var(--s1); } }
68+ }
69+ /* the preference "single-key shortcuts" of the help dialog */
70+ .keys-pref {
71+ flex: none; padding: var(--s3) var(--s4) 0;
72+ & label { display: flex; align-items: center; gap: var(--s2); cursor: pointer; }
73+ & input[type=checkbox] { width: 16px; height: 16px; flex: none; accent-color: var(--teal); }
74+ & p { font-size: var(--fs-s); margin-top: var(--s1); }
68 }75 }
69 }76 }
M internal/adapters/web/static/js/ermite.js +184 -24
@@ -64,6 +64,7 @@
64 setCursor(rows[j], $$('[data-rows] > [data-cursor]'));64 setCursor(rows[j], $$('[data-rows] > [data-cursor]'));
65 rows[j].scrollIntoView({ block: 'nearest' });65 rows[j].scrollIntoView({ block: 'nearest' });
66 window.ermiteTree?.sync(rows[j]);66 window.ermiteTree?.sync(rows[j]);
67+ window.ermiteFinder?.sync(); // the rows may be the file finder's options
67 const mark = j + 1 + '/' + rows.length;68 const mark = j + 1 + '/' + rows.length;
68 body().dataset.nav = mark;69 body().dataset.nav = mark;
69 setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500);70 setTimeout(() => body().dataset.nav === mark && (body().dataset.nav = ''), 1500);
@@ -76,6 +77,7 @@
76 setCursor(row, $$('[data-rows] > [data-cursor]'));77 setCursor(row, $$('[data-rows] > [data-cursor]'));
77 row.scrollIntoView({ block: 'nearest' });78 row.scrollIntoView({ block: 'nearest' });
78 window.ermiteTree?.sync(row);79 window.ermiteTree?.sync(row);
80+ window.ermiteFinder?.sync();
79 },81 },
80 open() {82 open() {
81 if ($('dialog[open]') || window.ermiteTree?.open()) return;83 if ($('dialog[open]') || window.ermiteTree?.open()) return;
@@ -108,6 +110,18 @@
108 $(`.tabs [data-key=${key}]`)?.click();110 $(`.tabs [data-key=${key}]`)?.click();
109 },111 },
110 112
113+ // The preference "single-key shortcuts" (ermiteKeys): the checkbox of the
114+ // help dialog and the action of the palette.
115+ setKeysOff(off) {
116+ window.ermiteKeys.set(off);
117+ },
118+ toggleKeys() {
119+ const off = !window.ermiteKeys.off();
120+ window.ermiteKeys.set(off);
121+ ermite.closeDialogs();
122+ flash(off ? 'single-key shortcuts off · ctrl k and esc still work' : 'single-key shortcuts on');
123+ },
124+
111 // Dialogs.125 // Dialogs.
112 closeDialogs() {126 closeDialogs() {
113 for (const d of $$('dialog[open]')) d.close();127 for (const d of $$('dialog[open]')) d.close();
@@ -117,6 +131,7 @@
117 if (!d) return;131 if (!d) return;
118 ermite.closeDialogs();132 ermite.closeDialogs();
119 d.showModal();133 d.showModal();
134+ if (id === 'panel-cmd') window.ermiteCmd?.sync();
120 if (focusId) document.getElementById(focusId)?.focus();135 if (focusId) document.getElementById(focusId)?.focus();
121 },136 },
122 137
@@ -146,31 +161,12 @@
146 }161 }
147 return rows;162 return rows;
148 },163 },
149- // The "go to" palette.164+ // The "go to" palette (ermiteCmd below).
150 cmdFilter(input) {165 cmdFilter(input) {
151- const t = input.value.toLowerCase().trim();166+ window.ermiteCmd.filter(input);
152- // The file hits the finder adds (li.finder-hit, files.js) have no data-k:
153- // the finder owns them.
154- const items = $$('#cmd-list > li[data-k]');
155- for (const li of items) li.hidden = !li.dataset.k.includes(t);
156- const first = items.find((li) => !li.hidden);
157- if (first) setCursor(first, $$('#cmd-list > li'));
158 },167 },
159 cmdKey(e) {168 cmdKey(e) {
160- const vis = $$('#cmd-list > li:not([hidden])');169+ window.ermiteCmd.key(e);
161- const i = vis.findIndex((li) => li.hasAttribute('data-cursor'));
162- const step = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : 0;
163- if (step) {
164- e.preventDefault();
165- const n = vis[Math.max(0, Math.min(vis.length - 1, i + step))];
166- if (n) {
167- setCursor(n, $$('#cmd-list > li'));
168- n.scrollIntoView({ block: 'nearest' });
169- }
170- } else if (e.key === 'Enter') {
171- e.preventDefault();
172- vis[Math.max(i, 0)]?.querySelector('a')?.click();
173- }
174 },170 },
175 171
176 // Diff.172 // Diff.
@@ -253,6 +249,69 @@
253 };249 };
254 window.ermite = ermite;250 window.ermite = ermite;
255 251
252+ // The "go to" palette is an editable combobox with a listbox popup: the
253+ // focus stays in #cmd-q, the options are the <li> of #cmd-list, and the one
254+ // with data-cursor is the active one (aria-selected on it, aria-activedescendant
255+ // on the field). sync keeps the two in step; it runs on every change of the
256+ // cursor or of the list, including the file hits files.js adds later.
257+ // Self-contained: jstest/palette.test.mjs evaluates this block alone.
258+ window.ermiteCmd = (() => {
259+ const list = () => document.getElementById('cmd-list');
260+ const field = () => document.getElementById('cmd-q');
261+ const options = () => [...list().querySelectorAll(':scope > li')];
262+ let seq = 0;
263+ const moveCursor = (li) => {
264+ for (const o of options()) if (o !== li) o.removeAttribute('data-cursor');
265+ li.setAttribute('data-cursor', '');
266+ };
267+ function sync() {
268+ const input = field();
269+ if (!input || !list()) return;
270+ const all = options();
271+ const shown = all.filter((li) => !li.hidden);
272+ const cur = shown.find((li) => li.hasAttribute('data-cursor'));
273+ for (const li of all) {
274+ li.id ||= 'cmd-h' + seq++;
275+ li.setAttribute('role', 'option');
276+ li.setAttribute('aria-selected', li === cur ? 'true' : 'false');
277+ }
278+ input.setAttribute('aria-expanded', shown.length ? 'true' : 'false');
279+ if (cur) input.setAttribute('aria-activedescendant', cur.id);
280+ else input.removeAttribute('aria-activedescendant');
281+ }
282+ return {
283+ sync,
284+ // filter hides the entries that do not contain the text of the field.
285+ // The file hits the finder adds (li.finder-hit, files.js) have no data-k:
286+ // the finder owns them.
287+ filter(input) {
288+ const t = input.value.toLowerCase().trim();
289+ const items = options().filter((li) => li.hasAttribute('data-k'));
290+ for (const li of items) li.hidden = !li.getAttribute('data-k').includes(t);
291+ const first = items.find((li) => !li.hidden);
292+ if (first) moveCursor(first);
293+ sync();
294+ },
295+ key(e) {
296+ const vis = options().filter((li) => !li.hidden);
297+ const i = vis.findIndex((li) => li.hasAttribute('data-cursor'));
298+ const step = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : 0;
299+ if (step) {
300+ e.preventDefault();
301+ const n = vis[Math.max(0, Math.min(vis.length - 1, i + step))];
302+ if (n) {
303+ moveCursor(n);
304+ n.scrollIntoView({ block: 'nearest' });
305+ }
306+ sync();
307+ } else if (e.key === 'Enter') {
308+ e.preventDefault();
309+ vis[Math.max(i, 0)]?.querySelector('a')?.click();
310+ }
311+ },
312+ };
313+ })();
314+
256 // The side panel (aside.rail) by keyboard: l enters it on its main action315 // The side panel (aside.rail) by keyboard: l enters it on its main action
257 // (the primary button) or its first control, j / k step between its shown316 // (the primary button) or its first control, j / k step between its shown
258 // controls (not hidden, not in a closed <details>), h leaves it for <main>.317 // controls (not hidden, not in a closed <details>), h leaves it for <main>.
@@ -458,6 +517,50 @@
458 return { load, file, current };517 return { load, file, current };
459 })();518 })();
460 519
520+ // The preference "single-character shortcuts" (WCAG 2.1.4): off, the keys
521+ // that are one printable character (j, k, s, ?, :, G, 1-9 in the merge menu,
522+ // …) do nothing. Shortcuts with ctrl or cmd, escape, enter and the arrows
523+ // keep working. The choice lives in localStorage ("keys-off") and is made in
524+ // the help dialog (a checkbox) or the palette (an action). Self-contained:
525+ // jstest/keys.test.mjs evaluates this block alone.
526+ window.ermiteKeys = (() => {
527+ const KEY = 'keys-off';
528+ let off = false;
529+ try { off = localStorage.getItem(KEY) === '1'; } catch { /* no storage: on, and the choice holds for this page */ }
530+ // apply shows the state: the checkbox and the note of the help dialog,
531+ // and the label of the palette action.
532+ function apply() {
533+ const box = document.getElementById('keys-off');
534+ if (box) box.checked = off;
535+ const note = document.getElementById('keys-note');
536+ if (note) {
537+ note.textContent = off
538+ ? 'Single-key shortcuts are off: only ctrl k and esc, enter and the arrows work. Turn them on again here.'
539+ : 'Single-key shortcuts are on. ctrl k and esc work whatever this says.';
540+ }
541+ const act = document.querySelector('#cmd-list [data-fire="keys"] .lbl');
542+ if (act) act.textContent = off ? 'turn single-key shortcuts on' : 'turn single-key shortcuts off';
543+ }
544+ function set(v) {
545+ off = !!v;
546+ try { localStorage.setItem(KEY, off ? '1' : ''); } catch { /* storage refused: holds for this page */ }
547+ apply();
548+ }
549+ addEventListener('storage', (e) => {
550+ if (e.key !== KEY) return;
551+ off = e.newValue === '1';
552+ apply();
553+ });
554+ apply();
555+ return {
556+ off: () => off,
557+ set,
558+ apply,
559+ // blocks says whether the preference turns this key off.
560+ blocks: (e) => off && e.key.length === 1,
561+ };
562+ })();
563+
461 // 1. Keys.564 // 1. Keys.
462 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',565 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
463 s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',566 s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
@@ -480,6 +583,8 @@
480 if (typing) e.target.blur();583 if (typing) e.target.blur();
481 return ermite.fire('esc');584 return ermite.fire('esc');
482 }585 }
586+ // Single-character shortcuts turned off: only the keys above work.
587+ if (window.ermiteKeys.blocks(e)) { pending = 0; return; }
483 // An open merge menu takes its keys (1-9, D) before the field check: its588 // An open merge menu takes its keys (1-9, D) before the field check: its
484 // radios and its checkbox hold the focus.589 // radios and its checkbox hold the focus.
485 if (mr?.menuKey(e)) return;590 if (mr?.menuKey(e)) return;
@@ -607,6 +712,50 @@ window.ermiteLive = (() => {
607 // server closing it, a network hiccup) is not worth a message.712 // server closing it, a network hiccup) is not worth a message.
608 const LOST_AFTER = 5000;713 const LOST_AFTER = 5000;
609 let lostTimer = 0;714 let lostTimer = 0;
715+ // The focus survives a swap. focusables are the controls of a fragment in
716+ // document order; where(root) says which one has the focus (its id, else its
717+ // tag and href or name, and its index among the controls) and refocus finds
718+ // it again in the element that replaced root.
719+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([type=hidden]):not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]';
720+ // shown: not inside a hidden element (a filtered row, a folded block); the
721+ // browser refuses the focus there.
722+ const shown = (e) => !e.closest('[hidden]') && (!e.checkVisibility || e.checkVisibility());
723+ const focusables = (root) => [...root.querySelectorAll(FOCUSABLE)].filter(shown);
724+ // take focuses e and says whether the focus is there now.
725+ const take = (e) => { e.focus({ preventScroll: true }); return document.activeElement === e; };
726+ const where = (root) => {
727+ const a = document.activeElement;
728+ if (!a || a === document.body || !root.contains(a)) return null;
729+ if (a === root) return { self: true };
730+ const all = focusables(root);
731+ return { id: a.id, tag: a.tagName, href: a.getAttribute('href'), name: a.getAttribute('name'), index: all.indexOf(a) };
732+ };
733+ const refocus = (root, w) => {
734+ if (w.self) return root.focus({ preventScroll: true });
735+ const all = focusables(root);
736+ const same = (e) => e.tagName === w.tag;
737+ const target =
738+ (w.id && all.find((e) => e.id === w.id)) ||
739+ (w.href != null && all.find((e) => same(e) && e.getAttribute('href') === w.href)) ||
740+ (w.name != null && all.find((e) => same(e) && e.getAttribute('name') === w.name)) ||
741+ (all[w.index] && same(all[w.index]) && all[w.index]) ||
742+ all[0];
743+ if (target) target.focus({ preventScroll: true });
744+ else if (root.hasAttribute('tabindex')) root.focus({ preventScroll: true });
745+ };
746+ // lostFocus: the element that had the focus inside is gone. The focus goes to
747+ // the first control of the next shown row, else of the nearest shown row
748+ // before, else to the content.
749+ const lostFocus = (after, before) => {
750+ const tryRow = (e) => {
751+ if (!shown(e)) return false;
752+ const c = focusables(e)[0] || (e.hasAttribute('tabindex') ? e : null);
753+ return !!c && take(c);
754+ };
755+ for (let e = after; e; e = e.nextElementSibling) if (tryRow(e)) return;
756+ for (let e = before; e; e = e.previousElementSibling) if (tryRow(e)) return;
757+ document.getElementById('main')?.focus({ preventScroll: true });
758+ };
610 const setState = (s) => { const el = document.querySelector('.hint-live'); if (el) el.dataset.state = s; };759 const setState = (s) => { const el = document.querySelector('.hint-live'); if (el) el.dataset.state = s; };
611 return {760 return {
612 // lost is htmx:sse:close and htmx:sse:error: "live updates paused" shows761 // lost is htmx:sse:close and htmx:sse:error: "live updates paused" shows
@@ -628,16 +777,27 @@ window.ermiteLive = (() => {
628 const cursorKey = cursor?.id || cursor?.querySelector('a')?.getAttribute('href');777 const cursorKey = cursor?.id || cursor?.querySelector('a')?.getAttribute('href');
629 for (const el of [...t.content.children]) {778 for (const el of [...t.content.children]) {
630 const old = el.id && document.getElementById(el.id);779 const old = el.id && document.getElementById(el.id);
631- if (el.hasAttribute('data-gone')) { old?.remove(); continue; }780+ const had = old ? where(old) : null; // the focus is inside what is replaced
781+ if (el.hasAttribute('data-gone')) {
782+ const after = old?.nextElementSibling, before = old?.previousElementSibling;
783+ old?.remove();
784+ if (had) lostFocus(after, before);
785+ continue;
786+ }
632 if (!old) {787 if (!old) {
633 const rows = document.getElementById('repo-rows');788 const rows = document.getElementById('repo-rows');
634 if (rows && el.id.startsWith('repo-')) { rows.prepend(el); window.htmx?.process(el); }789 if (rows && el.id.startsWith('repo-')) { rows.prepend(el); window.htmx?.process(el); }
635 continue;790 continue;
636 }791 }
637- if (old.hasAttribute('data-slot')) { old.replaceChildren(...el.childNodes); continue; }792+ if (old.hasAttribute('data-slot')) {
793+ old.replaceChildren(...el.childNodes);
794+ if (had) refocus(old, had);
795+ continue;
796+ }
638 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');797 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
639 old.replaceWith(el);798 old.replaceWith(el);
640 window.htmx?.process(el);799 window.htmx?.process(el);
800+ if (had) refocus(el, had);
641 }801 }
642 // The rows may have changed under an active filter: apply it again and802 // The rows may have changed under an active filter: apply it again and
643 // keep the cursor on its row while that row is still shown.803 // keep the cursor on its row while that row is still shown.
M internal/adapters/web/static/js/files.js +18 -1
@@ -232,6 +232,9 @@ window.ermiteTree = (() => {
232 if (!this.active()) return false;232 if (!this.active()) return false;
233 const li = cursorRow();233 const li = cursorRow();
234 if (!li) return false;234 if (!li) return false;
235+ // The focus joins the cursor row first (it may still be on the page, as
236+ // after a load): Enter then acts on the row the key acted on.
237+ if (document.activeElement !== li && !li.contains(document.activeElement)) mark(li, true);
235 if (into) {238 if (into) {
236 if (isDir(li)) {239 if (isDir(li)) {
237 if (!isOpen(li)) return toggle(li), true;240 if (!isOpen(li)) return toggle(li), true;
@@ -672,6 +675,13 @@ window.ermiteFinder = (() => {
672 }675 }
673 }676 }
674 677
678+ // sync is syncActive for whoever moves the cursor of the results from
679+ // outside (j / k and gg / G, ermite.js): the options and the field follow it.
680+ function sync() {
681+ const el = $('finder-q');
682+ if (el && $('finder') && !$('finder').hidden) syncActive(el);
683+ }
684+
675 // showTree: no query, the directory again, with j/k and Enter on its rows.685 // showTree: no query, the directory again, with j/k and Enter on its rows.
676 function showTree(el) {686 function showTree(el) {
677 rendered = null;687 rendered = null;
@@ -755,6 +765,7 @@ window.ermiteFinder = (() => {
755 function palette(el) {765 function palette(el) {
756 if (!document.body.dataset.paths) return;766 if (!document.body.dataset.paths) return;
757 clearHits();767 clearHits();
768+ window.ermiteCmd?.sync(); // the cursor may have been on a hit
758 if (pFrame) cancelAnimationFrame(pFrame);769 if (pFrame) cancelAnimationFrame(pFrame);
759 pFrame = 0;770 pFrame = 0;
760 if (!el.value.trim()) {771 if (!el.value.trim()) {
@@ -802,6 +813,7 @@ window.ermiteFinder = (() => {
802 list.querySelectorAll(':scope > li[data-cursor]').forEach((li) => li.removeAttribute('data-cursor'));813 list.querySelectorAll(':scope > li[data-cursor]').forEach((li) => li.removeAttribute('data-cursor'));
803 shown[0].setAttribute('data-cursor', '');814 shown[0].setAttribute('data-cursor', '');
804 }815 }
816+ window.ermiteCmd?.sync(); // roles, ids and the active option of the new hits
805 }817 }
806 818
807 // input runs the search of the field, once per frame.819 // input runs the search of the field, once per frame.
@@ -816,12 +828,17 @@ window.ermiteFinder = (() => {
816 }828 }
817 829
818 return {830 return {
819- index, search, positions, visible, label, load, palette, paletteClose, input,831+ index, search, positions, visible, label, load, palette, paletteClose, input, sync,
820 // key moves the cursor of the results, opens one, or clears the field.832 // key moves the cursor of the results, opens one, or clears the field.
821 key(el, e) {833 key(el, e) {
822 if (e.isComposing) return; // the Enter or Escape of an input method is its own834 if (e.isComposing) return; // the Enter or Escape of an input method is its own
823 const results = !$('finder').hidden;835 const results = !$('finder').hidden;
824 if (e.key === 'Escape') {836 if (e.key === 'Escape') {
837+ // With a query, Escape only clears it: the field keeps the focus (the
838+ // global handler of ermite.js would blur it). Without one, it leaves the field.
839+ if (!el.value) return;
840+ e.preventDefault();
841+ e.stopPropagation();
825 el.value = '';842 el.value = '';
826 input(el);843 input(el);
827 return;844 return;
M internal/app/testdata/web/blob-binary.html +25 -13
@@ -153,6 +153,11 @@
153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
154 <kbd>esc</kbd> close</button>154 <kbd>esc</kbd> close</button>
155 </div>155 </div>
156+<div class="keys-pref">
157+<label>
158+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
159+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
160+</div>
156 <dl class="keys">161 <dl class="keys">
157 <div>162 <div>
158 <dt>163 <dt>
@@ -306,72 +311,78 @@
306 <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()">311 <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()">
307 <div class="panel-head">312 <div class="panel-head">
308 <label for="cmd-q" class="sr">go to</label>313 <label for="cmd-q" class="sr">go to</label>
309-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">314+<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)">
310 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">315 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
311 <kbd>esc</kbd>316 <kbd>esc</kbd>
312 </button>317 </button>
313 </div>318 </div>
314-<ul id="cmd-list" class="cmd-list" aria-label="Targets">319+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
315-<li data-k="go home" data-cursor>320+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
316 <a href="/" tabindex="-1">321 <a href="/" tabindex="-1">
317 <span class="lbl">home</span>322 <span class="lbl">home</span>
318 <span class="k">go</span>323 <span class="k">go</span>
319 </a>324 </a>
320 </li>325 </li>
321-<li data-k="section summary">326+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
322 <a href="/files" tabindex="-1">327 <a href="/files" tabindex="-1">
323 <span class="lbl">summary</span>328 <span class="lbl">summary</span>
324 <span class="k">section</span>329 <span class="k">section</span>
325 </a>330 </a>
326 </li>331 </li>
327-<li data-k="section files">332+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
328 <a href="/files/tree/main" tabindex="-1">333 <a href="/files/tree/main" tabindex="-1">
329 <span class="lbl">files</span>334 <span class="lbl">files</span>
330 <span class="k">section</span>335 <span class="k">section</span>
331 </a>336 </a>
332 </li>337 </li>
333-<li data-k="section commits">338+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
334 <a href="/files/commits/main" tabindex="-1">339 <a href="/files/commits/main" tabindex="-1">
335 <span class="lbl">commits</span>340 <span class="lbl">commits</span>
336 <span class="k">section</span>341 <span class="k">section</span>
337 </a>342 </a>
338 </li>343 </li>
339-<li data-k="section branches &amp; tags">344+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
340 <a href="/files/refs" tabindex="-1">345 <a href="/files/refs" tabindex="-1">
341 <span class="lbl">branches &amp; tags</span>346 <span class="lbl">branches &amp; tags</span>
342 <span class="k">section</span>347 <span class="k">section</span>
343 </a>348 </a>
344 </li>349 </li>
345-<li data-k="section merge requests">350+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
346 <a href="/files/merge-requests" tabindex="-1">351 <a href="/files/merge-requests" tabindex="-1">
347 <span class="lbl">merge requests</span>352 <span class="lbl">merge requests</span>
348 <span class="k">section</span>353 <span class="k">section</span>
349 </a>354 </a>
350 </li>355 </li>
351-<li data-k="section actions">356+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
352 <a href="/files/actions" tabindex="-1">357 <a href="/files/actions" tabindex="-1">
353 <span class="lbl">actions</span>358 <span class="lbl">actions</span>
354 <span class="k">section</span>359 <span class="k">section</span>
355 </a>360 </a>
356 </li>361 </li>
357-<li data-k="branch feature/x">362+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
358 <a href="/files/tree/feature/x" tabindex="-1">363 <a href="/files/tree/feature/x" tabindex="-1">
359 <span class="lbl">feature/x</span>364 <span class="lbl">feature/x</span>
360 <span class="k">branch</span>365 <span class="k">branch</span>
361 </a>366 </a>
362 </li>367 </li>
363-<li data-k="branch main">368+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
364 <a href="/files/tree/main" tabindex="-1">369 <a href="/files/tree/main" tabindex="-1">
365 <span class="lbl">main</span>370 <span class="lbl">main</span>
366 <span class="k">branch</span>371 <span class="k">branch</span>
367 </a>372 </a>
368 </li>373 </li>
369-<li data-k="tag v1.0">374+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
370 <a href="/files/tree/v1.0" tabindex="-1">375 <a href="/files/tree/v1.0" tabindex="-1">
371 <span class="lbl">v1.0</span>376 <span class="lbl">v1.0</span>
372 <span class="k">tag</span>377 <span class="k">tag</span>
373 </a>378 </a>
374 </li>379 </li>
380+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
381+<a href="#" data-fire="keys" tabindex="-1">
382+<span class="lbl">turn single-key shortcuts off</span>
383+<span class="k">action</span>
384+</a>
385+</li>
375 </ul>386 </ul>
376 <p class="panel-foot">387 <p class="panel-foot">
377 <span>388 <span>
@@ -410,7 +421,8 @@
410 k-permalink from:body -> ermite.permalink();421 k-permalink from:body -> ermite.permalink();
411 k-merge from:body -> window.ermiteMR?.openMerge();422 k-merge from:body -> window.ermiteMR?.openMerge();
412 k-read from:body -> window.ermiteMR?.markRead();423 k-read from:body -> window.ermiteMR?.markRead();
413- k-gf from:body -> window.ermiteMR?.focusFiles()">424+ k-gf from:body -> window.ermiteMR?.focusFiles();
425+ k-keys from:body -> ermite.toggleKeys()">
414 </div>426 </div>
415 </body>427 </body>
416 </html>428 </html>
M internal/app/testdata/web/blob-code.html +25 -13
@@ -223,6 +223,11 @@
223 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">223 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
224 <kbd>esc</kbd> close</button>224 <kbd>esc</kbd> close</button>
225 </div>225 </div>
226+<div class="keys-pref">
227+<label>
228+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
229+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
230+</div>
226 <dl class="keys">231 <dl class="keys">
227 <div>232 <div>
228 <dt>233 <dt>
@@ -376,72 +381,78 @@
376 <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()">381 <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()">
377 <div class="panel-head">382 <div class="panel-head">
378 <label for="cmd-q" class="sr">go to</label>383 <label for="cmd-q" class="sr">go to</label>
379-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">384+<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)">
380 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">385 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
381 <kbd>esc</kbd>386 <kbd>esc</kbd>
382 </button>387 </button>
383 </div>388 </div>
384-<ul id="cmd-list" class="cmd-list" aria-label="Targets">389+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
385-<li data-k="go home" data-cursor>390+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
386 <a href="/" tabindex="-1">391 <a href="/" tabindex="-1">
387 <span class="lbl">home</span>392 <span class="lbl">home</span>
388 <span class="k">go</span>393 <span class="k">go</span>
389 </a>394 </a>
390 </li>395 </li>
391-<li data-k="section summary">396+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
392 <a href="/files" tabindex="-1">397 <a href="/files" tabindex="-1">
393 <span class="lbl">summary</span>398 <span class="lbl">summary</span>
394 <span class="k">section</span>399 <span class="k">section</span>
395 </a>400 </a>
396 </li>401 </li>
397-<li data-k="section files">402+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
398 <a href="/files/tree/main" tabindex="-1">403 <a href="/files/tree/main" tabindex="-1">
399 <span class="lbl">files</span>404 <span class="lbl">files</span>
400 <span class="k">section</span>405 <span class="k">section</span>
401 </a>406 </a>
402 </li>407 </li>
403-<li data-k="section commits">408+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
404 <a href="/files/commits/main" tabindex="-1">409 <a href="/files/commits/main" tabindex="-1">
405 <span class="lbl">commits</span>410 <span class="lbl">commits</span>
406 <span class="k">section</span>411 <span class="k">section</span>
407 </a>412 </a>
408 </li>413 </li>
409-<li data-k="section branches &amp; tags">414+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
410 <a href="/files/refs" tabindex="-1">415 <a href="/files/refs" tabindex="-1">
411 <span class="lbl">branches &amp; tags</span>416 <span class="lbl">branches &amp; tags</span>
412 <span class="k">section</span>417 <span class="k">section</span>
413 </a>418 </a>
414 </li>419 </li>
415-<li data-k="section merge requests">420+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
416 <a href="/files/merge-requests" tabindex="-1">421 <a href="/files/merge-requests" tabindex="-1">
417 <span class="lbl">merge requests</span>422 <span class="lbl">merge requests</span>
418 <span class="k">section</span>423 <span class="k">section</span>
419 </a>424 </a>
420 </li>425 </li>
421-<li data-k="section actions">426+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
422 <a href="/files/actions" tabindex="-1">427 <a href="/files/actions" tabindex="-1">
423 <span class="lbl">actions</span>428 <span class="lbl">actions</span>
424 <span class="k">section</span>429 <span class="k">section</span>
425 </a>430 </a>
426 </li>431 </li>
427-<li data-k="branch feature/x">432+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
428 <a href="/files/tree/feature/x" tabindex="-1">433 <a href="/files/tree/feature/x" tabindex="-1">
429 <span class="lbl">feature/x</span>434 <span class="lbl">feature/x</span>
430 <span class="k">branch</span>435 <span class="k">branch</span>
431 </a>436 </a>
432 </li>437 </li>
433-<li data-k="branch main">438+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
434 <a href="/files/tree/main" tabindex="-1">439 <a href="/files/tree/main" tabindex="-1">
435 <span class="lbl">main</span>440 <span class="lbl">main</span>
436 <span class="k">branch</span>441 <span class="k">branch</span>
437 </a>442 </a>
438 </li>443 </li>
439-<li data-k="tag v1.0">444+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
440 <a href="/files/tree/v1.0" tabindex="-1">445 <a href="/files/tree/v1.0" tabindex="-1">
441 <span class="lbl">v1.0</span>446 <span class="lbl">v1.0</span>
442 <span class="k">tag</span>447 <span class="k">tag</span>
443 </a>448 </a>
444 </li>449 </li>
450+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
451+<a href="#" data-fire="keys" tabindex="-1">
452+<span class="lbl">turn single-key shortcuts off</span>
453+<span class="k">action</span>
454+</a>
455+</li>
445 </ul>456 </ul>
446 <p class="panel-foot">457 <p class="panel-foot">
447 <span>458 <span>
@@ -480,7 +491,8 @@
480 k-permalink from:body -> ermite.permalink();491 k-permalink from:body -> ermite.permalink();
481 k-merge from:body -> window.ermiteMR?.openMerge();492 k-merge from:body -> window.ermiteMR?.openMerge();
482 k-read from:body -> window.ermiteMR?.markRead();493 k-read from:body -> window.ermiteMR?.markRead();
483- k-gf from:body -> window.ermiteMR?.focusFiles()">494+ k-gf from:body -> window.ermiteMR?.focusFiles();
495+ k-keys from:body -> ermite.toggleKeys()">
484 </div>496 </div>
485 </body>497 </body>
486 </html>498 </html>
M internal/app/testdata/web/blob-empty.html +25 -13
@@ -153,6 +153,11 @@
153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
154 <kbd>esc</kbd> close</button>154 <kbd>esc</kbd> close</button>
155 </div>155 </div>
156+<div class="keys-pref">
157+<label>
158+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
159+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
160+</div>
156 <dl class="keys">161 <dl class="keys">
157 <div>162 <div>
158 <dt>163 <dt>
@@ -306,72 +311,78 @@
306 <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()">311 <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()">
307 <div class="panel-head">312 <div class="panel-head">
308 <label for="cmd-q" class="sr">go to</label>313 <label for="cmd-q" class="sr">go to</label>
309-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">314+<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)">
310 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">315 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
311 <kbd>esc</kbd>316 <kbd>esc</kbd>
312 </button>317 </button>
313 </div>318 </div>
314-<ul id="cmd-list" class="cmd-list" aria-label="Targets">319+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
315-<li data-k="go home" data-cursor>320+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
316 <a href="/" tabindex="-1">321 <a href="/" tabindex="-1">
317 <span class="lbl">home</span>322 <span class="lbl">home</span>
318 <span class="k">go</span>323 <span class="k">go</span>
319 </a>324 </a>
320 </li>325 </li>
321-<li data-k="section summary">326+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
322 <a href="/files" tabindex="-1">327 <a href="/files" tabindex="-1">
323 <span class="lbl">summary</span>328 <span class="lbl">summary</span>
324 <span class="k">section</span>329 <span class="k">section</span>
325 </a>330 </a>
326 </li>331 </li>
327-<li data-k="section files">332+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
328 <a href="/files/tree/main" tabindex="-1">333 <a href="/files/tree/main" tabindex="-1">
329 <span class="lbl">files</span>334 <span class="lbl">files</span>
330 <span class="k">section</span>335 <span class="k">section</span>
331 </a>336 </a>
332 </li>337 </li>
333-<li data-k="section commits">338+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
334 <a href="/files/commits/main" tabindex="-1">339 <a href="/files/commits/main" tabindex="-1">
335 <span class="lbl">commits</span>340 <span class="lbl">commits</span>
336 <span class="k">section</span>341 <span class="k">section</span>
337 </a>342 </a>
338 </li>343 </li>
339-<li data-k="section branches &amp; tags">344+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
340 <a href="/files/refs" tabindex="-1">345 <a href="/files/refs" tabindex="-1">
341 <span class="lbl">branches &amp; tags</span>346 <span class="lbl">branches &amp; tags</span>
342 <span class="k">section</span>347 <span class="k">section</span>
343 </a>348 </a>
344 </li>349 </li>
345-<li data-k="section merge requests">350+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
346 <a href="/files/merge-requests" tabindex="-1">351 <a href="/files/merge-requests" tabindex="-1">
347 <span class="lbl">merge requests</span>352 <span class="lbl">merge requests</span>
348 <span class="k">section</span>353 <span class="k">section</span>
349 </a>354 </a>
350 </li>355 </li>
351-<li data-k="section actions">356+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
352 <a href="/files/actions" tabindex="-1">357 <a href="/files/actions" tabindex="-1">
353 <span class="lbl">actions</span>358 <span class="lbl">actions</span>
354 <span class="k">section</span>359 <span class="k">section</span>
355 </a>360 </a>
356 </li>361 </li>
357-<li data-k="branch feature/x">362+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
358 <a href="/files/tree/feature/x" tabindex="-1">363 <a href="/files/tree/feature/x" tabindex="-1">
359 <span class="lbl">feature/x</span>364 <span class="lbl">feature/x</span>
360 <span class="k">branch</span>365 <span class="k">branch</span>
361 </a>366 </a>
362 </li>367 </li>
363-<li data-k="branch main">368+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
364 <a href="/files/tree/main" tabindex="-1">369 <a href="/files/tree/main" tabindex="-1">
365 <span class="lbl">main</span>370 <span class="lbl">main</span>
366 <span class="k">branch</span>371 <span class="k">branch</span>
367 </a>372 </a>
368 </li>373 </li>
369-<li data-k="tag v1.0">374+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
370 <a href="/files/tree/v1.0" tabindex="-1">375 <a href="/files/tree/v1.0" tabindex="-1">
371 <span class="lbl">v1.0</span>376 <span class="lbl">v1.0</span>
372 <span class="k">tag</span>377 <span class="k">tag</span>
373 </a>378 </a>
374 </li>379 </li>
380+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
381+<a href="#" data-fire="keys" tabindex="-1">
382+<span class="lbl">turn single-key shortcuts off</span>
383+<span class="k">action</span>
384+</a>
385+</li>
375 </ul>386 </ul>
376 <p class="panel-foot">387 <p class="panel-foot">
377 <span>388 <span>
@@ -410,7 +421,8 @@
410 k-permalink from:body -> ermite.permalink();421 k-permalink from:body -> ermite.permalink();
411 k-merge from:body -> window.ermiteMR?.openMerge();422 k-merge from:body -> window.ermiteMR?.openMerge();
412 k-read from:body -> window.ermiteMR?.markRead();423 k-read from:body -> window.ermiteMR?.markRead();
413- k-gf from:body -> window.ermiteMR?.focusFiles()">424+ k-gf from:body -> window.ermiteMR?.focusFiles();
425+ k-keys from:body -> ermite.toggleKeys()">
414 </div>426 </div>
415 </body>427 </body>
416 </html>428 </html>
M internal/app/testdata/web/blob-image.html +25 -13
@@ -155,6 +155,11 @@
155 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">155 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
156 <kbd>esc</kbd> close</button>156 <kbd>esc</kbd> close</button>
157 </div>157 </div>
158+<div class="keys-pref">
159+<label>
160+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
161+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
162+</div>
158 <dl class="keys">163 <dl class="keys">
159 <div>164 <div>
160 <dt>165 <dt>
@@ -308,72 +313,78 @@
308 <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()">313 <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()">
309 <div class="panel-head">314 <div class="panel-head">
310 <label for="cmd-q" class="sr">go to</label>315 <label for="cmd-q" class="sr">go to</label>
311-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">316+<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)">
312 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">317 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
313 <kbd>esc</kbd>318 <kbd>esc</kbd>
314 </button>319 </button>
315 </div>320 </div>
316-<ul id="cmd-list" class="cmd-list" aria-label="Targets">321+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
317-<li data-k="go home" data-cursor>322+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
318 <a href="/" tabindex="-1">323 <a href="/" tabindex="-1">
319 <span class="lbl">home</span>324 <span class="lbl">home</span>
320 <span class="k">go</span>325 <span class="k">go</span>
321 </a>326 </a>
322 </li>327 </li>
323-<li data-k="section summary">328+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
324 <a href="/files" tabindex="-1">329 <a href="/files" tabindex="-1">
325 <span class="lbl">summary</span>330 <span class="lbl">summary</span>
326 <span class="k">section</span>331 <span class="k">section</span>
327 </a>332 </a>
328 </li>333 </li>
329-<li data-k="section files">334+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
330 <a href="/files/tree/main" tabindex="-1">335 <a href="/files/tree/main" tabindex="-1">
331 <span class="lbl">files</span>336 <span class="lbl">files</span>
332 <span class="k">section</span>337 <span class="k">section</span>
333 </a>338 </a>
334 </li>339 </li>
335-<li data-k="section commits">340+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
336 <a href="/files/commits/main" tabindex="-1">341 <a href="/files/commits/main" tabindex="-1">
337 <span class="lbl">commits</span>342 <span class="lbl">commits</span>
338 <span class="k">section</span>343 <span class="k">section</span>
339 </a>344 </a>
340 </li>345 </li>
341-<li data-k="section branches &amp; tags">346+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
342 <a href="/files/refs" tabindex="-1">347 <a href="/files/refs" tabindex="-1">
343 <span class="lbl">branches &amp; tags</span>348 <span class="lbl">branches &amp; tags</span>
344 <span class="k">section</span>349 <span class="k">section</span>
345 </a>350 </a>
346 </li>351 </li>
347-<li data-k="section merge requests">352+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
348 <a href="/files/merge-requests" tabindex="-1">353 <a href="/files/merge-requests" tabindex="-1">
349 <span class="lbl">merge requests</span>354 <span class="lbl">merge requests</span>
350 <span class="k">section</span>355 <span class="k">section</span>
351 </a>356 </a>
352 </li>357 </li>
353-<li data-k="section actions">358+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
354 <a href="/files/actions" tabindex="-1">359 <a href="/files/actions" tabindex="-1">
355 <span class="lbl">actions</span>360 <span class="lbl">actions</span>
356 <span class="k">section</span>361 <span class="k">section</span>
357 </a>362 </a>
358 </li>363 </li>
359-<li data-k="branch feature/x">364+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
360 <a href="/files/tree/feature/x" tabindex="-1">365 <a href="/files/tree/feature/x" tabindex="-1">
361 <span class="lbl">feature/x</span>366 <span class="lbl">feature/x</span>
362 <span class="k">branch</span>367 <span class="k">branch</span>
363 </a>368 </a>
364 </li>369 </li>
365-<li data-k="branch main">370+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
366 <a href="/files/tree/main" tabindex="-1">371 <a href="/files/tree/main" tabindex="-1">
367 <span class="lbl">main</span>372 <span class="lbl">main</span>
368 <span class="k">branch</span>373 <span class="k">branch</span>
369 </a>374 </a>
370 </li>375 </li>
371-<li data-k="tag v1.0">376+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
372 <a href="/files/tree/v1.0" tabindex="-1">377 <a href="/files/tree/v1.0" tabindex="-1">
373 <span class="lbl">v1.0</span>378 <span class="lbl">v1.0</span>
374 <span class="k">tag</span>379 <span class="k">tag</span>
375 </a>380 </a>
376 </li>381 </li>
382+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
383+<a href="#" data-fire="keys" tabindex="-1">
384+<span class="lbl">turn single-key shortcuts off</span>
385+<span class="k">action</span>
386+</a>
387+</li>
377 </ul>388 </ul>
378 <p class="panel-foot">389 <p class="panel-foot">
379 <span>390 <span>
@@ -412,7 +423,8 @@
412 k-permalink from:body -> ermite.permalink();423 k-permalink from:body -> ermite.permalink();
413 k-merge from:body -> window.ermiteMR?.openMerge();424 k-merge from:body -> window.ermiteMR?.openMerge();
414 k-read from:body -> window.ermiteMR?.markRead();425 k-read from:body -> window.ermiteMR?.markRead();
415- k-gf from:body -> window.ermiteMR?.focusFiles()">426+ k-gf from:body -> window.ermiteMR?.focusFiles();
427+ k-keys from:body -> ermite.toggleKeys()">
416 </div>428 </div>
417 </body>429 </body>
418 </html>430 </html>
M internal/app/testdata/web/blob-markdown.html +25 -13
@@ -220,6 +220,11 @@
220 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">220 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
221 <kbd>esc</kbd> close</button>221 <kbd>esc</kbd> close</button>
222 </div>222 </div>
223+<div class="keys-pref">
224+<label>
225+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
226+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
227+</div>
223 <dl class="keys">228 <dl class="keys">
224 <div>229 <div>
225 <dt>230 <dt>
@@ -373,72 +378,78 @@
373 <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()">378 <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()">
374 <div class="panel-head">379 <div class="panel-head">
375 <label for="cmd-q" class="sr">go to</label>380 <label for="cmd-q" class="sr">go to</label>
376-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">381+<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)">
377 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">382 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
378 <kbd>esc</kbd>383 <kbd>esc</kbd>
379 </button>384 </button>
380 </div>385 </div>
381-<ul id="cmd-list" class="cmd-list" aria-label="Targets">386+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
382-<li data-k="go home" data-cursor>387+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
383 <a href="/" tabindex="-1">388 <a href="/" tabindex="-1">
384 <span class="lbl">home</span>389 <span class="lbl">home</span>
385 <span class="k">go</span>390 <span class="k">go</span>
386 </a>391 </a>
387 </li>392 </li>
388-<li data-k="section summary">393+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
389 <a href="/files" tabindex="-1">394 <a href="/files" tabindex="-1">
390 <span class="lbl">summary</span>395 <span class="lbl">summary</span>
391 <span class="k">section</span>396 <span class="k">section</span>
392 </a>397 </a>
393 </li>398 </li>
394-<li data-k="section files">399+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
395 <a href="/files/tree/main" tabindex="-1">400 <a href="/files/tree/main" tabindex="-1">
396 <span class="lbl">files</span>401 <span class="lbl">files</span>
397 <span class="k">section</span>402 <span class="k">section</span>
398 </a>403 </a>
399 </li>404 </li>
400-<li data-k="section commits">405+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
401 <a href="/files/commits/main" tabindex="-1">406 <a href="/files/commits/main" tabindex="-1">
402 <span class="lbl">commits</span>407 <span class="lbl">commits</span>
403 <span class="k">section</span>408 <span class="k">section</span>
404 </a>409 </a>
405 </li>410 </li>
406-<li data-k="section branches &amp; tags">411+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
407 <a href="/files/refs" tabindex="-1">412 <a href="/files/refs" tabindex="-1">
408 <span class="lbl">branches &amp; tags</span>413 <span class="lbl">branches &amp; tags</span>
409 <span class="k">section</span>414 <span class="k">section</span>
410 </a>415 </a>
411 </li>416 </li>
412-<li data-k="section merge requests">417+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
413 <a href="/files/merge-requests" tabindex="-1">418 <a href="/files/merge-requests" tabindex="-1">
414 <span class="lbl">merge requests</span>419 <span class="lbl">merge requests</span>
415 <span class="k">section</span>420 <span class="k">section</span>
416 </a>421 </a>
417 </li>422 </li>
418-<li data-k="section actions">423+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
419 <a href="/files/actions" tabindex="-1">424 <a href="/files/actions" tabindex="-1">
420 <span class="lbl">actions</span>425 <span class="lbl">actions</span>
421 <span class="k">section</span>426 <span class="k">section</span>
422 </a>427 </a>
423 </li>428 </li>
424-<li data-k="branch feature/x">429+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
425 <a href="/files/tree/feature/x" tabindex="-1">430 <a href="/files/tree/feature/x" tabindex="-1">
426 <span class="lbl">feature/x</span>431 <span class="lbl">feature/x</span>
427 <span class="k">branch</span>432 <span class="k">branch</span>
428 </a>433 </a>
429 </li>434 </li>
430-<li data-k="branch main">435+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
431 <a href="/files/tree/main" tabindex="-1">436 <a href="/files/tree/main" tabindex="-1">
432 <span class="lbl">main</span>437 <span class="lbl">main</span>
433 <span class="k">branch</span>438 <span class="k">branch</span>
434 </a>439 </a>
435 </li>440 </li>
436-<li data-k="tag v1.0">441+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
437 <a href="/files/tree/v1.0" tabindex="-1">442 <a href="/files/tree/v1.0" tabindex="-1">
438 <span class="lbl">v1.0</span>443 <span class="lbl">v1.0</span>
439 <span class="k">tag</span>444 <span class="k">tag</span>
440 </a>445 </a>
441 </li>446 </li>
447+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
448+<a href="#" data-fire="keys" tabindex="-1">
449+<span class="lbl">turn single-key shortcuts off</span>
450+<span class="k">action</span>
451+</a>
452+</li>
442 </ul>453 </ul>
443 <p class="panel-foot">454 <p class="panel-foot">
444 <span>455 <span>
@@ -477,7 +488,8 @@
477 k-permalink from:body -> ermite.permalink();488 k-permalink from:body -> ermite.permalink();
478 k-merge from:body -> window.ermiteMR?.openMerge();489 k-merge from:body -> window.ermiteMR?.openMerge();
479 k-read from:body -> window.ermiteMR?.markRead();490 k-read from:body -> window.ermiteMR?.markRead();
480- k-gf from:body -> window.ermiteMR?.focusFiles()">491+ k-gf from:body -> window.ermiteMR?.focusFiles();
492+ k-keys from:body -> ermite.toggleKeys()">
481 </div>493 </div>
482 </body>494 </body>
483 </html>495 </html>
M internal/app/testdata/web/blob-svg.html +25 -13
@@ -178,6 +178,11 @@
178 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">178 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
179 <kbd>esc</kbd> close</button>179 <kbd>esc</kbd> close</button>
180 </div>180 </div>
181+<div class="keys-pref">
182+<label>
183+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
184+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
185+</div>
181 <dl class="keys">186 <dl class="keys">
182 <div>187 <div>
183 <dt>188 <dt>
@@ -331,72 +336,78 @@
331 <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()">336 <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()">
332 <div class="panel-head">337 <div class="panel-head">
333 <label for="cmd-q" class="sr">go to</label>338 <label for="cmd-q" class="sr">go to</label>
334-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">339+<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)">
335 <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" hx-on:click="closest('dialog').close()">
336 <kbd>esc</kbd>341 <kbd>esc</kbd>
337 </button>342 </button>
338 </div>343 </div>
339-<ul id="cmd-list" class="cmd-list" aria-label="Targets">344+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
340-<li data-k="go home" data-cursor>345+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
341 <a href="/" tabindex="-1">346 <a href="/" tabindex="-1">
342 <span class="lbl">home</span>347 <span class="lbl">home</span>
343 <span class="k">go</span>348 <span class="k">go</span>
344 </a>349 </a>
345 </li>350 </li>
346-<li data-k="section summary">351+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
347 <a href="/files" tabindex="-1">352 <a href="/files" tabindex="-1">
348 <span class="lbl">summary</span>353 <span class="lbl">summary</span>
349 <span class="k">section</span>354 <span class="k">section</span>
350 </a>355 </a>
351 </li>356 </li>
352-<li data-k="section files">357+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
353 <a href="/files/tree/main" tabindex="-1">358 <a href="/files/tree/main" tabindex="-1">
354 <span class="lbl">files</span>359 <span class="lbl">files</span>
355 <span class="k">section</span>360 <span class="k">section</span>
356 </a>361 </a>
357 </li>362 </li>
358-<li data-k="section commits">363+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
359 <a href="/files/commits/main" tabindex="-1">364 <a href="/files/commits/main" tabindex="-1">
360 <span class="lbl">commits</span>365 <span class="lbl">commits</span>
361 <span class="k">section</span>366 <span class="k">section</span>
362 </a>367 </a>
363 </li>368 </li>
364-<li data-k="section branches &amp; tags">369+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
365 <a href="/files/refs" tabindex="-1">370 <a href="/files/refs" tabindex="-1">
366 <span class="lbl">branches &amp; tags</span>371 <span class="lbl">branches &amp; tags</span>
367 <span class="k">section</span>372 <span class="k">section</span>
368 </a>373 </a>
369 </li>374 </li>
370-<li data-k="section merge requests">375+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
371 <a href="/files/merge-requests" tabindex="-1">376 <a href="/files/merge-requests" tabindex="-1">
372 <span class="lbl">merge requests</span>377 <span class="lbl">merge requests</span>
373 <span class="k">section</span>378 <span class="k">section</span>
374 </a>379 </a>
375 </li>380 </li>
376-<li data-k="section actions">381+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
377 <a href="/files/actions" tabindex="-1">382 <a href="/files/actions" tabindex="-1">
378 <span class="lbl">actions</span>383 <span class="lbl">actions</span>
379 <span class="k">section</span>384 <span class="k">section</span>
380 </a>385 </a>
381 </li>386 </li>
382-<li data-k="branch feature/x">387+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
383 <a href="/files/tree/feature/x" tabindex="-1">388 <a href="/files/tree/feature/x" tabindex="-1">
384 <span class="lbl">feature/x</span>389 <span class="lbl">feature/x</span>
385 <span class="k">branch</span>390 <span class="k">branch</span>
386 </a>391 </a>
387 </li>392 </li>
388-<li data-k="branch main">393+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
389 <a href="/files/tree/main" tabindex="-1">394 <a href="/files/tree/main" tabindex="-1">
390 <span class="lbl">main</span>395 <span class="lbl">main</span>
391 <span class="k">branch</span>396 <span class="k">branch</span>
392 </a>397 </a>
393 </li>398 </li>
394-<li data-k="tag v1.0">399+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
395 <a href="/files/tree/v1.0" tabindex="-1">400 <a href="/files/tree/v1.0" tabindex="-1">
396 <span class="lbl">v1.0</span>401 <span class="lbl">v1.0</span>
397 <span class="k">tag</span>402 <span class="k">tag</span>
398 </a>403 </a>
399 </li>404 </li>
405+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
406+<a href="#" data-fire="keys" tabindex="-1">
407+<span class="lbl">turn single-key shortcuts off</span>
408+<span class="k">action</span>
409+</a>
410+</li>
400 </ul>411 </ul>
401 <p class="panel-foot">412 <p class="panel-foot">
402 <span>413 <span>
@@ -435,7 +446,8 @@
435 k-permalink from:body -> ermite.permalink();446 k-permalink from:body -> ermite.permalink();
436 k-merge from:body -> window.ermiteMR?.openMerge();447 k-merge from:body -> window.ermiteMR?.openMerge();
437 k-read from:body -> window.ermiteMR?.markRead();448 k-read from:body -> window.ermiteMR?.markRead();
438- k-gf from:body -> window.ermiteMR?.focusFiles()">449+ k-gf from:body -> window.ermiteMR?.focusFiles();
450+ k-keys from:body -> ermite.toggleKeys()">
439 </div>451 </div>
440 </body>452 </body>
441 </html>453 </html>
M internal/app/testdata/web/blob-text.html +25 -13
@@ -173,6 +173,11 @@
173 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">173 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
174 <kbd>esc</kbd> close</button>174 <kbd>esc</kbd> close</button>
175 </div>175 </div>
176+<div class="keys-pref">
177+<label>
178+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
179+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
180+</div>
176 <dl class="keys">181 <dl class="keys">
177 <div>182 <div>
178 <dt>183 <dt>
@@ -326,72 +331,78 @@
326 <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()">331 <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()">
327 <div class="panel-head">332 <div class="panel-head">
328 <label for="cmd-q" class="sr">go to</label>333 <label for="cmd-q" class="sr">go to</label>
329-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">334+<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)">
330 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">335 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
331 <kbd>esc</kbd>336 <kbd>esc</kbd>
332 </button>337 </button>
333 </div>338 </div>
334-<ul id="cmd-list" class="cmd-list" aria-label="Targets">339+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
335-<li data-k="go home" data-cursor>340+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
336 <a href="/" tabindex="-1">341 <a href="/" tabindex="-1">
337 <span class="lbl">home</span>342 <span class="lbl">home</span>
338 <span class="k">go</span>343 <span class="k">go</span>
339 </a>344 </a>
340 </li>345 </li>
341-<li data-k="section summary">346+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
342 <a href="/files" tabindex="-1">347 <a href="/files" tabindex="-1">
343 <span class="lbl">summary</span>348 <span class="lbl">summary</span>
344 <span class="k">section</span>349 <span class="k">section</span>
345 </a>350 </a>
346 </li>351 </li>
347-<li data-k="section files">352+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
348 <a href="/files/tree/main" tabindex="-1">353 <a href="/files/tree/main" tabindex="-1">
349 <span class="lbl">files</span>354 <span class="lbl">files</span>
350 <span class="k">section</span>355 <span class="k">section</span>
351 </a>356 </a>
352 </li>357 </li>
353-<li data-k="section commits">358+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
354 <a href="/files/commits/main" tabindex="-1">359 <a href="/files/commits/main" tabindex="-1">
355 <span class="lbl">commits</span>360 <span class="lbl">commits</span>
356 <span class="k">section</span>361 <span class="k">section</span>
357 </a>362 </a>
358 </li>363 </li>
359-<li data-k="section branches &amp; tags">364+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
360 <a href="/files/refs" tabindex="-1">365 <a href="/files/refs" tabindex="-1">
361 <span class="lbl">branches &amp; tags</span>366 <span class="lbl">branches &amp; tags</span>
362 <span class="k">section</span>367 <span class="k">section</span>
363 </a>368 </a>
364 </li>369 </li>
365-<li data-k="section merge requests">370+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
366 <a href="/files/merge-requests" tabindex="-1">371 <a href="/files/merge-requests" tabindex="-1">
367 <span class="lbl">merge requests</span>372 <span class="lbl">merge requests</span>
368 <span class="k">section</span>373 <span class="k">section</span>
369 </a>374 </a>
370 </li>375 </li>
371-<li data-k="section actions">376+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
372 <a href="/files/actions" tabindex="-1">377 <a href="/files/actions" tabindex="-1">
373 <span class="lbl">actions</span>378 <span class="lbl">actions</span>
374 <span class="k">section</span>379 <span class="k">section</span>
375 </a>380 </a>
376 </li>381 </li>
377-<li data-k="branch feature/x">382+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
378 <a href="/files/tree/feature/x" tabindex="-1">383 <a href="/files/tree/feature/x" tabindex="-1">
379 <span class="lbl">feature/x</span>384 <span class="lbl">feature/x</span>
380 <span class="k">branch</span>385 <span class="k">branch</span>
381 </a>386 </a>
382 </li>387 </li>
383-<li data-k="branch main">388+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
384 <a href="/files/tree/main" tabindex="-1">389 <a href="/files/tree/main" tabindex="-1">
385 <span class="lbl">main</span>390 <span class="lbl">main</span>
386 <span class="k">branch</span>391 <span class="k">branch</span>
387 </a>392 </a>
388 </li>393 </li>
389-<li data-k="tag v1.0">394+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
390 <a href="/files/tree/v1.0" tabindex="-1">395 <a href="/files/tree/v1.0" tabindex="-1">
391 <span class="lbl">v1.0</span>396 <span class="lbl">v1.0</span>
392 <span class="k">tag</span>397 <span class="k">tag</span>
393 </a>398 </a>
394 </li>399 </li>
400+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
401+<a href="#" data-fire="keys" tabindex="-1">
402+<span class="lbl">turn single-key shortcuts off</span>
403+<span class="k">action</span>
404+</a>
405+</li>
395 </ul>406 </ul>
396 <p class="panel-foot">407 <p class="panel-foot">
397 <span>408 <span>
@@ -430,7 +441,8 @@
430 k-permalink from:body -> ermite.permalink();441 k-permalink from:body -> ermite.permalink();
431 k-merge from:body -> window.ermiteMR?.openMerge();442 k-merge from:body -> window.ermiteMR?.openMerge();
432 k-read from:body -> window.ermiteMR?.markRead();443 k-read from:body -> window.ermiteMR?.markRead();
433- k-gf from:body -> window.ermiteMR?.focusFiles()">444+ k-gf from:body -> window.ermiteMR?.focusFiles();
445+ k-keys from:body -> ermite.toggleKeys()">
434 </div>446 </div>
435 </body>447 </body>
436 </html>448 </html>
M internal/app/testdata/web/blob-too-large.html +25 -13
@@ -153,6 +153,11 @@
153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">153 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
154 <kbd>esc</kbd> close</button>154 <kbd>esc</kbd> close</button>
155 </div>155 </div>
156+<div class="keys-pref">
157+<label>
158+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
159+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
160+</div>
156 <dl class="keys">161 <dl class="keys">
157 <div>162 <div>
158 <dt>163 <dt>
@@ -306,72 +311,78 @@
306 <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()">311 <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()">
307 <div class="panel-head">312 <div class="panel-head">
308 <label for="cmd-q" class="sr">go to</label>313 <label for="cmd-q" class="sr">go to</label>
309-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">314+<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)">
310 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">315 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
311 <kbd>esc</kbd>316 <kbd>esc</kbd>
312 </button>317 </button>
313 </div>318 </div>
314-<ul id="cmd-list" class="cmd-list" aria-label="Targets">319+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
315-<li data-k="go home" data-cursor>320+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
316 <a href="/" tabindex="-1">321 <a href="/" tabindex="-1">
317 <span class="lbl">home</span>322 <span class="lbl">home</span>
318 <span class="k">go</span>323 <span class="k">go</span>
319 </a>324 </a>
320 </li>325 </li>
321-<li data-k="section summary">326+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
322 <a href="/files" tabindex="-1">327 <a href="/files" tabindex="-1">
323 <span class="lbl">summary</span>328 <span class="lbl">summary</span>
324 <span class="k">section</span>329 <span class="k">section</span>
325 </a>330 </a>
326 </li>331 </li>
327-<li data-k="section files">332+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
328 <a href="/files/tree/main" tabindex="-1">333 <a href="/files/tree/main" tabindex="-1">
329 <span class="lbl">files</span>334 <span class="lbl">files</span>
330 <span class="k">section</span>335 <span class="k">section</span>
331 </a>336 </a>
332 </li>337 </li>
333-<li data-k="section commits">338+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
334 <a href="/files/commits/main" tabindex="-1">339 <a href="/files/commits/main" tabindex="-1">
335 <span class="lbl">commits</span>340 <span class="lbl">commits</span>
336 <span class="k">section</span>341 <span class="k">section</span>
337 </a>342 </a>
338 </li>343 </li>
339-<li data-k="section branches &amp; tags">344+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
340 <a href="/files/refs" tabindex="-1">345 <a href="/files/refs" tabindex="-1">
341 <span class="lbl">branches &amp; tags</span>346 <span class="lbl">branches &amp; tags</span>
342 <span class="k">section</span>347 <span class="k">section</span>
343 </a>348 </a>
344 </li>349 </li>
345-<li data-k="section merge requests">350+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
346 <a href="/files/merge-requests" tabindex="-1">351 <a href="/files/merge-requests" tabindex="-1">
347 <span class="lbl">merge requests</span>352 <span class="lbl">merge requests</span>
348 <span class="k">section</span>353 <span class="k">section</span>
349 </a>354 </a>
350 </li>355 </li>
351-<li data-k="section actions">356+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
352 <a href="/files/actions" tabindex="-1">357 <a href="/files/actions" tabindex="-1">
353 <span class="lbl">actions</span>358 <span class="lbl">actions</span>
354 <span class="k">section</span>359 <span class="k">section</span>
355 </a>360 </a>
356 </li>361 </li>
357-<li data-k="branch feature/x">362+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
358 <a href="/files/tree/feature/x" tabindex="-1">363 <a href="/files/tree/feature/x" tabindex="-1">
359 <span class="lbl">feature/x</span>364 <span class="lbl">feature/x</span>
360 <span class="k">branch</span>365 <span class="k">branch</span>
361 </a>366 </a>
362 </li>367 </li>
363-<li data-k="branch main">368+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
364 <a href="/files/tree/main" tabindex="-1">369 <a href="/files/tree/main" tabindex="-1">
365 <span class="lbl">main</span>370 <span class="lbl">main</span>
366 <span class="k">branch</span>371 <span class="k">branch</span>
367 </a>372 </a>
368 </li>373 </li>
369-<li data-k="tag v1.0">374+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
370 <a href="/files/tree/v1.0" tabindex="-1">375 <a href="/files/tree/v1.0" tabindex="-1">
371 <span class="lbl">v1.0</span>376 <span class="lbl">v1.0</span>
372 <span class="k">tag</span>377 <span class="k">tag</span>
373 </a>378 </a>
374 </li>379 </li>
380+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
381+<a href="#" data-fire="keys" tabindex="-1">
382+<span class="lbl">turn single-key shortcuts off</span>
383+<span class="k">action</span>
384+</a>
385+</li>
375 </ul>386 </ul>
376 <p class="panel-foot">387 <p class="panel-foot">
377 <span>388 <span>
@@ -410,7 +421,8 @@
410 k-permalink from:body -> ermite.permalink();421 k-permalink from:body -> ermite.permalink();
411 k-merge from:body -> window.ermiteMR?.openMerge();422 k-merge from:body -> window.ermiteMR?.openMerge();
412 k-read from:body -> window.ermiteMR?.markRead();423 k-read from:body -> window.ermiteMR?.markRead();
413- k-gf from:body -> window.ermiteMR?.focusFiles()">424+ k-gf from:body -> window.ermiteMR?.focusFiles();
425+ k-keys from:body -> ermite.toggleKeys()">
414 </div>426 </div>
415 </body>427 </body>
416 </html>428 </html>
M internal/app/testdata/web/blob-trojan.html +25 -13
@@ -220,6 +220,11 @@
220 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">220 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
221 <kbd>esc</kbd> close</button>221 <kbd>esc</kbd> close</button>
222 </div>222 </div>
223+<div class="keys-pref">
224+<label>
225+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
226+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
227+</div>
223 <dl class="keys">228 <dl class="keys">
224 <div>229 <div>
225 <dt>230 <dt>
@@ -373,72 +378,78 @@
373 <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()">378 <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()">
374 <div class="panel-head">379 <div class="panel-head">
375 <label for="cmd-q" class="sr">go to</label>380 <label for="cmd-q" class="sr">go to</label>
376-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">381+<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)">
377 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">382 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
378 <kbd>esc</kbd>383 <kbd>esc</kbd>
379 </button>384 </button>
380 </div>385 </div>
381-<ul id="cmd-list" class="cmd-list" aria-label="Targets">386+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
382-<li data-k="go home" data-cursor>387+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
383 <a href="/" tabindex="-1">388 <a href="/" tabindex="-1">
384 <span class="lbl">home</span>389 <span class="lbl">home</span>
385 <span class="k">go</span>390 <span class="k">go</span>
386 </a>391 </a>
387 </li>392 </li>
388-<li data-k="section summary">393+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
389 <a href="/files" tabindex="-1">394 <a href="/files" tabindex="-1">
390 <span class="lbl">summary</span>395 <span class="lbl">summary</span>
391 <span class="k">section</span>396 <span class="k">section</span>
392 </a>397 </a>
393 </li>398 </li>
394-<li data-k="section files">399+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
395 <a href="/files/tree/main" tabindex="-1">400 <a href="/files/tree/main" tabindex="-1">
396 <span class="lbl">files</span>401 <span class="lbl">files</span>
397 <span class="k">section</span>402 <span class="k">section</span>
398 </a>403 </a>
399 </li>404 </li>
400-<li data-k="section commits">405+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
401 <a href="/files/commits/main" tabindex="-1">406 <a href="/files/commits/main" tabindex="-1">
402 <span class="lbl">commits</span>407 <span class="lbl">commits</span>
403 <span class="k">section</span>408 <span class="k">section</span>
404 </a>409 </a>
405 </li>410 </li>
406-<li data-k="section branches &amp; tags">411+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
407 <a href="/files/refs" tabindex="-1">412 <a href="/files/refs" tabindex="-1">
408 <span class="lbl">branches &amp; tags</span>413 <span class="lbl">branches &amp; tags</span>
409 <span class="k">section</span>414 <span class="k">section</span>
410 </a>415 </a>
411 </li>416 </li>
412-<li data-k="section merge requests">417+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
413 <a href="/files/merge-requests" tabindex="-1">418 <a href="/files/merge-requests" tabindex="-1">
414 <span class="lbl">merge requests</span>419 <span class="lbl">merge requests</span>
415 <span class="k">section</span>420 <span class="k">section</span>
416 </a>421 </a>
417 </li>422 </li>
418-<li data-k="section actions">423+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
419 <a href="/files/actions" tabindex="-1">424 <a href="/files/actions" tabindex="-1">
420 <span class="lbl">actions</span>425 <span class="lbl">actions</span>
421 <span class="k">section</span>426 <span class="k">section</span>
422 </a>427 </a>
423 </li>428 </li>
424-<li data-k="branch feature/x">429+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
425 <a href="/files/tree/feature/x" tabindex="-1">430 <a href="/files/tree/feature/x" tabindex="-1">
426 <span class="lbl">feature/x</span>431 <span class="lbl">feature/x</span>
427 <span class="k">branch</span>432 <span class="k">branch</span>
428 </a>433 </a>
429 </li>434 </li>
430-<li data-k="branch main">435+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
431 <a href="/files/tree/main" tabindex="-1">436 <a href="/files/tree/main" tabindex="-1">
432 <span class="lbl">main</span>437 <span class="lbl">main</span>
433 <span class="k">branch</span>438 <span class="k">branch</span>
434 </a>439 </a>
435 </li>440 </li>
436-<li data-k="tag v1.0">441+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
437 <a href="/files/tree/v1.0" tabindex="-1">442 <a href="/files/tree/v1.0" tabindex="-1">
438 <span class="lbl">v1.0</span>443 <span class="lbl">v1.0</span>
439 <span class="k">tag</span>444 <span class="k">tag</span>
440 </a>445 </a>
441 </li>446 </li>
447+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
448+<a href="#" data-fire="keys" tabindex="-1">
449+<span class="lbl">turn single-key shortcuts off</span>
450+<span class="k">action</span>
451+</a>
452+</li>
442 </ul>453 </ul>
443 <p class="panel-foot">454 <p class="panel-foot">
444 <span>455 <span>
@@ -477,7 +488,8 @@
477 k-permalink from:body -> ermite.permalink();488 k-permalink from:body -> ermite.permalink();
478 k-merge from:body -> window.ermiteMR?.openMerge();489 k-merge from:body -> window.ermiteMR?.openMerge();
479 k-read from:body -> window.ermiteMR?.markRead();490 k-read from:body -> window.ermiteMR?.markRead();
480- k-gf from:body -> window.ermiteMR?.focusFiles()">491+ k-gf from:body -> window.ermiteMR?.focusFiles();
492+ k-keys from:body -> ermite.toggleKeys()">
481 </div>493 </div>
482 </body>494 </body>
483 </html>495 </html>
M internal/app/testdata/web/commit-big.html +26 -14
@@ -117,7 +117,7 @@
117 </dd>117 </dd>
118 </dl>118 </dl>
119 </header>119 </header>
120-<div class="diff-tools" role="toolbar" aria-label="Diff view">120+<div class="diff-tools" role="group" aria-label="Diff view">
121 <span class="dim">1 file · <span class="plus">+2600</span>121 <span class="dim">1 file · <span class="plus">+2600</span>
122 <span class="minus">-0</span>122 <span class="minus">-0</span>
123 </span>123 </span>
@@ -191,6 +191,11 @@
191 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">191 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
192 <kbd>esc</kbd> close</button>192 <kbd>esc</kbd> close</button>
193 </div>193 </div>
194+<div class="keys-pref">
195+<label>
196+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
197+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
198+</div>
194 <dl class="keys">199 <dl class="keys">
195 <div>200 <div>
196 <dt>201 <dt>
@@ -344,72 +349,78 @@
344 <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()">349 <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()">
345 <div class="panel-head">350 <div class="panel-head">
346 <label for="cmd-q" class="sr">go to</label>351 <label for="cmd-q" class="sr">go to</label>
347-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">352+<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)">
348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">353 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
349 <kbd>esc</kbd>354 <kbd>esc</kbd>
350 </button>355 </button>
351 </div>356 </div>
352-<ul id="cmd-list" class="cmd-list" aria-label="Targets">357+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
353-<li data-k="go home" data-cursor>358+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
354 <a href="/" tabindex="-1">359 <a href="/" tabindex="-1">
355 <span class="lbl">home</span>360 <span class="lbl">home</span>
356 <span class="k">go</span>361 <span class="k">go</span>
357 </a>362 </a>
358 </li>363 </li>
359-<li data-k="section summary">364+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
360 <a href="/diffs" tabindex="-1">365 <a href="/diffs" tabindex="-1">
361 <span class="lbl">summary</span>366 <span class="lbl">summary</span>
362 <span class="k">section</span>367 <span class="k">section</span>
363 </a>368 </a>
364 </li>369 </li>
365-<li data-k="section files">370+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
366 <a href="/diffs/tree/main" tabindex="-1">371 <a href="/diffs/tree/main" tabindex="-1">
367 <span class="lbl">files</span>372 <span class="lbl">files</span>
368 <span class="k">section</span>373 <span class="k">section</span>
369 </a>374 </a>
370 </li>375 </li>
371-<li data-k="section commits">376+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
372 <a href="/diffs/commits/main" tabindex="-1">377 <a href="/diffs/commits/main" tabindex="-1">
373 <span class="lbl">commits</span>378 <span class="lbl">commits</span>
374 <span class="k">section</span>379 <span class="k">section</span>
375 </a>380 </a>
376 </li>381 </li>
377-<li data-k="section branches &amp; tags">382+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
378 <a href="/diffs/refs" tabindex="-1">383 <a href="/diffs/refs" tabindex="-1">
379 <span class="lbl">branches &amp; tags</span>384 <span class="lbl">branches &amp; tags</span>
380 <span class="k">section</span>385 <span class="k">section</span>
381 </a>386 </a>
382 </li>387 </li>
383-<li data-k="section merge requests">388+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
384 <a href="/diffs/merge-requests" tabindex="-1">389 <a href="/diffs/merge-requests" tabindex="-1">
385 <span class="lbl">merge requests</span>390 <span class="lbl">merge requests</span>
386 <span class="k">section</span>391 <span class="k">section</span>
387 </a>392 </a>
388 </li>393 </li>
389-<li data-k="section actions">394+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
390 <a href="/diffs/actions" tabindex="-1">395 <a href="/diffs/actions" tabindex="-1">
391 <span class="lbl">actions</span>396 <span class="lbl">actions</span>
392 <span class="k">section</span>397 <span class="k">section</span>
393 </a>398 </a>
394 </li>399 </li>
395-<li data-k="branch main">400+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
396 <a href="/diffs/tree/main" tabindex="-1">401 <a href="/diffs/tree/main" tabindex="-1">
397 <span class="lbl">main</span>402 <span class="lbl">main</span>
398 <span class="k">branch</span>403 <span class="k">branch</span>
399 </a>404 </a>
400 </li>405 </li>
401-<li data-k="branch side">406+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
402 <a href="/diffs/tree/side" tabindex="-1">407 <a href="/diffs/tree/side" tabindex="-1">
403 <span class="lbl">side</span>408 <span class="lbl">side</span>
404 <span class="k">branch</span>409 <span class="k">branch</span>
405 </a>410 </a>
406 </li>411 </li>
407-<li data-k="tag v1.0.0">412+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
408 <a href="/diffs/tree/v1.0.0" tabindex="-1">413 <a href="/diffs/tree/v1.0.0" tabindex="-1">
409 <span class="lbl">v1.0.0</span>414 <span class="lbl">v1.0.0</span>
410 <span class="k">tag</span>415 <span class="k">tag</span>
411 </a>416 </a>
412 </li>417 </li>
418+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
419+<a href="#" data-fire="keys" tabindex="-1">
420+<span class="lbl">turn single-key shortcuts off</span>
421+<span class="k">action</span>
422+</a>
423+</li>
413 </ul>424 </ul>
414 <p class="panel-foot">425 <p class="panel-foot">
415 <span>426 <span>
@@ -448,7 +459,8 @@
448 k-permalink from:body -> ermite.permalink();459 k-permalink from:body -> ermite.permalink();
449 k-merge from:body -> window.ermiteMR?.openMerge();460 k-merge from:body -> window.ermiteMR?.openMerge();
450 k-read from:body -> window.ermiteMR?.markRead();461 k-read from:body -> window.ermiteMR?.markRead();
451- k-gf from:body -> window.ermiteMR?.focusFiles()">462+ k-gf from:body -> window.ermiteMR?.focusFiles();
463+ k-keys from:body -> ermite.toggleKeys()">
452 </div>464 </div>
453 </body>465 </body>
454 </html>466 </html>
M internal/app/testdata/web/commit-binary.html +26 -14
@@ -113,7 +113,7 @@
113 </dd>113 </dd>
114 </dl>114 </dl>
115 </header>115 </header>
116-<div class="diff-tools" role="toolbar" aria-label="Diff view">116+<div class="diff-tools" role="group" aria-label="Diff view">
117 <span class="dim">1 file · <span class="plus">+0</span>117 <span class="dim">1 file · <span class="plus">+0</span>
118 <span class="minus">-0</span>118 <span class="minus">-0</span>
119 </span>119 </span>
@@ -183,6 +183,11 @@
183 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">183 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
184 <kbd>esc</kbd> close</button>184 <kbd>esc</kbd> close</button>
185 </div>185 </div>
186+<div class="keys-pref">
187+<label>
188+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
189+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
190+</div>
186 <dl class="keys">191 <dl class="keys">
187 <div>192 <div>
188 <dt>193 <dt>
@@ -336,72 +341,78 @@
336 <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()">341 <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()">
337 <div class="panel-head">342 <div class="panel-head">
338 <label for="cmd-q" class="sr">go to</label>343 <label for="cmd-q" class="sr">go to</label>
339-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">344+<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)">
340 <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" hx-on:click="closest('dialog').close()">
341 <kbd>esc</kbd>346 <kbd>esc</kbd>
342 </button>347 </button>
343 </div>348 </div>
344-<ul id="cmd-list" class="cmd-list" aria-label="Targets">349+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
345-<li data-k="go home" data-cursor>350+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
346 <a href="/" tabindex="-1">351 <a href="/" tabindex="-1">
347 <span class="lbl">home</span>352 <span class="lbl">home</span>
348 <span class="k">go</span>353 <span class="k">go</span>
349 </a>354 </a>
350 </li>355 </li>
351-<li data-k="section summary">356+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
352 <a href="/diffs" tabindex="-1">357 <a href="/diffs" tabindex="-1">
353 <span class="lbl">summary</span>358 <span class="lbl">summary</span>
354 <span class="k">section</span>359 <span class="k">section</span>
355 </a>360 </a>
356 </li>361 </li>
357-<li data-k="section files">362+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
358 <a href="/diffs/tree/main" tabindex="-1">363 <a href="/diffs/tree/main" tabindex="-1">
359 <span class="lbl">files</span>364 <span class="lbl">files</span>
360 <span class="k">section</span>365 <span class="k">section</span>
361 </a>366 </a>
362 </li>367 </li>
363-<li data-k="section commits">368+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
364 <a href="/diffs/commits/main" tabindex="-1">369 <a href="/diffs/commits/main" tabindex="-1">
365 <span class="lbl">commits</span>370 <span class="lbl">commits</span>
366 <span class="k">section</span>371 <span class="k">section</span>
367 </a>372 </a>
368 </li>373 </li>
369-<li data-k="section branches &amp; tags">374+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
370 <a href="/diffs/refs" tabindex="-1">375 <a href="/diffs/refs" tabindex="-1">
371 <span class="lbl">branches &amp; tags</span>376 <span class="lbl">branches &amp; tags</span>
372 <span class="k">section</span>377 <span class="k">section</span>
373 </a>378 </a>
374 </li>379 </li>
375-<li data-k="section merge requests">380+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
376 <a href="/diffs/merge-requests" tabindex="-1">381 <a href="/diffs/merge-requests" tabindex="-1">
377 <span class="lbl">merge requests</span>382 <span class="lbl">merge requests</span>
378 <span class="k">section</span>383 <span class="k">section</span>
379 </a>384 </a>
380 </li>385 </li>
381-<li data-k="section actions">386+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
382 <a href="/diffs/actions" tabindex="-1">387 <a href="/diffs/actions" tabindex="-1">
383 <span class="lbl">actions</span>388 <span class="lbl">actions</span>
384 <span class="k">section</span>389 <span class="k">section</span>
385 </a>390 </a>
386 </li>391 </li>
387-<li data-k="branch main">392+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
388 <a href="/diffs/tree/main" tabindex="-1">393 <a href="/diffs/tree/main" tabindex="-1">
389 <span class="lbl">main</span>394 <span class="lbl">main</span>
390 <span class="k">branch</span>395 <span class="k">branch</span>
391 </a>396 </a>
392 </li>397 </li>
393-<li data-k="branch side">398+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
394 <a href="/diffs/tree/side" tabindex="-1">399 <a href="/diffs/tree/side" tabindex="-1">
395 <span class="lbl">side</span>400 <span class="lbl">side</span>
396 <span class="k">branch</span>401 <span class="k">branch</span>
397 </a>402 </a>
398 </li>403 </li>
399-<li data-k="tag v1.0.0">404+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
400 <a href="/diffs/tree/v1.0.0" tabindex="-1">405 <a href="/diffs/tree/v1.0.0" tabindex="-1">
401 <span class="lbl">v1.0.0</span>406 <span class="lbl">v1.0.0</span>
402 <span class="k">tag</span>407 <span class="k">tag</span>
403 </a>408 </a>
404 </li>409 </li>
410+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
411+<a href="#" data-fire="keys" tabindex="-1">
412+<span class="lbl">turn single-key shortcuts off</span>
413+<span class="k">action</span>
414+</a>
415+</li>
405 </ul>416 </ul>
406 <p class="panel-foot">417 <p class="panel-foot">
407 <span>418 <span>
@@ -440,7 +451,8 @@
440 k-permalink from:body -> ermite.permalink();451 k-permalink from:body -> ermite.permalink();
441 k-merge from:body -> window.ermiteMR?.openMerge();452 k-merge from:body -> window.ermiteMR?.openMerge();
442 k-read from:body -> window.ermiteMR?.markRead();453 k-read from:body -> window.ermiteMR?.markRead();
443- k-gf from:body -> window.ermiteMR?.focusFiles()">454+ k-gf from:body -> window.ermiteMR?.focusFiles();
455+ k-keys from:body -> ermite.toggleKeys()">
444 </div>456 </div>
445 </body>457 </body>
446 </html>458 </html>
M internal/app/testdata/web/commit-merge.html +26 -14
@@ -112,7 +112,7 @@
112 <a href="/diffs/commit/SHA40">SHA7</a> + <a href="/diffs/commit/SHA40">SHA7</a> · diff against the first parent</dd>112 <a href="/diffs/commit/SHA40">SHA7</a> + <a href="/diffs/commit/SHA40">SHA7</a> · diff against the first parent</dd>
113 </dl>113 </dl>
114 </header>114 </header>
115-<div class="diff-tools" role="toolbar" aria-label="Diff view">115+<div class="diff-tools" role="group" aria-label="Diff view">
116 <span class="dim">1 file · <span class="plus">+1</span>116 <span class="dim">1 file · <span class="plus">+1</span>
117 <span class="minus">-0</span>117 <span class="minus">-0</span>
118 </span>118 </span>
@@ -225,6 +225,11 @@
225 <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" hx-on:click="closest('dialog').close()">
226 <kbd>esc</kbd> close</button>226 <kbd>esc</kbd> close</button>
227 </div>227 </div>
228+<div class="keys-pref">
229+<label>
230+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
231+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
232+</div>
228 <dl class="keys">233 <dl class="keys">
229 <div>234 <div>
230 <dt>235 <dt>
@@ -378,72 +383,78 @@
378 <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()">383 <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()">
379 <div class="panel-head">384 <div class="panel-head">
380 <label for="cmd-q" class="sr">go to</label>385 <label for="cmd-q" class="sr">go to</label>
381-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">386+<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)">
382 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">387 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
383 <kbd>esc</kbd>388 <kbd>esc</kbd>
384 </button>389 </button>
385 </div>390 </div>
386-<ul id="cmd-list" class="cmd-list" aria-label="Targets">391+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
387-<li data-k="go home" data-cursor>392+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
388 <a href="/" tabindex="-1">393 <a href="/" tabindex="-1">
389 <span class="lbl">home</span>394 <span class="lbl">home</span>
390 <span class="k">go</span>395 <span class="k">go</span>
391 </a>396 </a>
392 </li>397 </li>
393-<li data-k="section summary">398+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
394 <a href="/diffs" tabindex="-1">399 <a href="/diffs" tabindex="-1">
395 <span class="lbl">summary</span>400 <span class="lbl">summary</span>
396 <span class="k">section</span>401 <span class="k">section</span>
397 </a>402 </a>
398 </li>403 </li>
399-<li data-k="section files">404+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
400 <a href="/diffs/tree/main" tabindex="-1">405 <a href="/diffs/tree/main" tabindex="-1">
401 <span class="lbl">files</span>406 <span class="lbl">files</span>
402 <span class="k">section</span>407 <span class="k">section</span>
403 </a>408 </a>
404 </li>409 </li>
405-<li data-k="section commits">410+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
406 <a href="/diffs/commits/main" tabindex="-1">411 <a href="/diffs/commits/main" tabindex="-1">
407 <span class="lbl">commits</span>412 <span class="lbl">commits</span>
408 <span class="k">section</span>413 <span class="k">section</span>
409 </a>414 </a>
410 </li>415 </li>
411-<li data-k="section branches &amp; tags">416+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
412 <a href="/diffs/refs" tabindex="-1">417 <a href="/diffs/refs" tabindex="-1">
413 <span class="lbl">branches &amp; tags</span>418 <span class="lbl">branches &amp; tags</span>
414 <span class="k">section</span>419 <span class="k">section</span>
415 </a>420 </a>
416 </li>421 </li>
417-<li data-k="section merge requests">422+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
418 <a href="/diffs/merge-requests" tabindex="-1">423 <a href="/diffs/merge-requests" tabindex="-1">
419 <span class="lbl">merge requests</span>424 <span class="lbl">merge requests</span>
420 <span class="k">section</span>425 <span class="k">section</span>
421 </a>426 </a>
422 </li>427 </li>
423-<li data-k="section actions">428+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
424 <a href="/diffs/actions" tabindex="-1">429 <a href="/diffs/actions" tabindex="-1">
425 <span class="lbl">actions</span>430 <span class="lbl">actions</span>
426 <span class="k">section</span>431 <span class="k">section</span>
427 </a>432 </a>
428 </li>433 </li>
429-<li data-k="branch main">434+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
430 <a href="/diffs/tree/main" tabindex="-1">435 <a href="/diffs/tree/main" tabindex="-1">
431 <span class="lbl">main</span>436 <span class="lbl">main</span>
432 <span class="k">branch</span>437 <span class="k">branch</span>
433 </a>438 </a>
434 </li>439 </li>
435-<li data-k="branch side">440+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
436 <a href="/diffs/tree/side" tabindex="-1">441 <a href="/diffs/tree/side" tabindex="-1">
437 <span class="lbl">side</span>442 <span class="lbl">side</span>
438 <span class="k">branch</span>443 <span class="k">branch</span>
439 </a>444 </a>
440 </li>445 </li>
441-<li data-k="tag v1.0.0">446+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
442 <a href="/diffs/tree/v1.0.0" tabindex="-1">447 <a href="/diffs/tree/v1.0.0" tabindex="-1">
443 <span class="lbl">v1.0.0</span>448 <span class="lbl">v1.0.0</span>
444 <span class="k">tag</span>449 <span class="k">tag</span>
445 </a>450 </a>
446 </li>451 </li>
452+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
453+<a href="#" data-fire="keys" tabindex="-1">
454+<span class="lbl">turn single-key shortcuts off</span>
455+<span class="k">action</span>
456+</a>
457+</li>
447 </ul>458 </ul>
448 <p class="panel-foot">459 <p class="panel-foot">
449 <span>460 <span>
@@ -482,7 +493,8 @@
482 k-permalink from:body -> ermite.permalink();493 k-permalink from:body -> ermite.permalink();
483 k-merge from:body -> window.ermiteMR?.openMerge();494 k-merge from:body -> window.ermiteMR?.openMerge();
484 k-read from:body -> window.ermiteMR?.markRead();495 k-read from:body -> window.ermiteMR?.markRead();
485- k-gf from:body -> window.ermiteMR?.focusFiles()">496+ k-gf from:body -> window.ermiteMR?.focusFiles();
497+ k-keys from:body -> ermite.toggleKeys()">
486 </div>498 </div>
487 </body>499 </body>
488 </html>500 </html>
M internal/app/testdata/web/commit-modify-split.html +26 -14
@@ -119,7 +119,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
119 </dd>119 </dd>
120 </dl>120 </dl>
121 </header>121 </header>
122-<div class="diff-tools" role="toolbar" aria-label="Diff view">122+<div class="diff-tools" role="group" aria-label="Diff view">
123 <span class="dim">2 files · <span class="plus">+2</span>123 <span class="dim">2 files · <span class="plus">+2</span>
124 <span class="minus">-1</span>124 <span class="minus">-1</span>
125 </span>125 </span>
@@ -348,6 +348,11 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
349 <kbd>esc</kbd> close</button>349 <kbd>esc</kbd> close</button>
350 </div>350 </div>
351+<div class="keys-pref">
352+<label>
353+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
354+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
355+</div>
351 <dl class="keys">356 <dl class="keys">
352 <div>357 <div>
353 <dt>358 <dt>
@@ -501,72 +506,78 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
501 <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()">506 <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()">
502 <div class="panel-head">507 <div class="panel-head">
503 <label for="cmd-q" class="sr">go to</label>508 <label for="cmd-q" class="sr">go to</label>
504-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">509+<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)">
505 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">510 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
506 <kbd>esc</kbd>511 <kbd>esc</kbd>
507 </button>512 </button>
508 </div>513 </div>
509-<ul id="cmd-list" class="cmd-list" aria-label="Targets">514+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
510-<li data-k="go home" data-cursor>515+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
511 <a href="/" tabindex="-1">516 <a href="/" tabindex="-1">
512 <span class="lbl">home</span>517 <span class="lbl">home</span>
513 <span class="k">go</span>518 <span class="k">go</span>
514 </a>519 </a>
515 </li>520 </li>
516-<li data-k="section summary">521+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
517 <a href="/diffs" tabindex="-1">522 <a href="/diffs" tabindex="-1">
518 <span class="lbl">summary</span>523 <span class="lbl">summary</span>
519 <span class="k">section</span>524 <span class="k">section</span>
520 </a>525 </a>
521 </li>526 </li>
522-<li data-k="section files">527+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
523 <a href="/diffs/tree/main" tabindex="-1">528 <a href="/diffs/tree/main" tabindex="-1">
524 <span class="lbl">files</span>529 <span class="lbl">files</span>
525 <span class="k">section</span>530 <span class="k">section</span>
526 </a>531 </a>
527 </li>532 </li>
528-<li data-k="section commits">533+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
529 <a href="/diffs/commits/main" tabindex="-1">534 <a href="/diffs/commits/main" tabindex="-1">
530 <span class="lbl">commits</span>535 <span class="lbl">commits</span>
531 <span class="k">section</span>536 <span class="k">section</span>
532 </a>537 </a>
533 </li>538 </li>
534-<li data-k="section branches &amp; tags">539+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
535 <a href="/diffs/refs" tabindex="-1">540 <a href="/diffs/refs" tabindex="-1">
536 <span class="lbl">branches &amp; tags</span>541 <span class="lbl">branches &amp; tags</span>
537 <span class="k">section</span>542 <span class="k">section</span>
538 </a>543 </a>
539 </li>544 </li>
540-<li data-k="section merge requests">545+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
541 <a href="/diffs/merge-requests" tabindex="-1">546 <a href="/diffs/merge-requests" tabindex="-1">
542 <span class="lbl">merge requests</span>547 <span class="lbl">merge requests</span>
543 <span class="k">section</span>548 <span class="k">section</span>
544 </a>549 </a>
545 </li>550 </li>
546-<li data-k="section actions">551+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
547 <a href="/diffs/actions" tabindex="-1">552 <a href="/diffs/actions" tabindex="-1">
548 <span class="lbl">actions</span>553 <span class="lbl">actions</span>
549 <span class="k">section</span>554 <span class="k">section</span>
550 </a>555 </a>
551 </li>556 </li>
552-<li data-k="branch main">557+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
553 <a href="/diffs/tree/main" tabindex="-1">558 <a href="/diffs/tree/main" tabindex="-1">
554 <span class="lbl">main</span>559 <span class="lbl">main</span>
555 <span class="k">branch</span>560 <span class="k">branch</span>
556 </a>561 </a>
557 </li>562 </li>
558-<li data-k="branch side">563+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
559 <a href="/diffs/tree/side" tabindex="-1">564 <a href="/diffs/tree/side" tabindex="-1">
560 <span class="lbl">side</span>565 <span class="lbl">side</span>
561 <span class="k">branch</span>566 <span class="k">branch</span>
562 </a>567 </a>
563 </li>568 </li>
564-<li data-k="tag v1.0.0">569+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
565 <a href="/diffs/tree/v1.0.0" tabindex="-1">570 <a href="/diffs/tree/v1.0.0" tabindex="-1">
566 <span class="lbl">v1.0.0</span>571 <span class="lbl">v1.0.0</span>
567 <span class="k">tag</span>572 <span class="k">tag</span>
568 </a>573 </a>
569 </li>574 </li>
575+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
576+<a href="#" data-fire="keys" tabindex="-1">
577+<span class="lbl">turn single-key shortcuts off</span>
578+<span class="k">action</span>
579+</a>
580+</li>
570 </ul>581 </ul>
571 <p class="panel-foot">582 <p class="panel-foot">
572 <span>583 <span>
@@ -605,7 +616,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
605 k-permalink from:body -> ermite.permalink();616 k-permalink from:body -> ermite.permalink();
606 k-merge from:body -> window.ermiteMR?.openMerge();617 k-merge from:body -> window.ermiteMR?.openMerge();
607 k-read from:body -> window.ermiteMR?.markRead();618 k-read from:body -> window.ermiteMR?.markRead();
608- k-gf from:body -> window.ermiteMR?.focusFiles()">619+ k-gf from:body -> window.ermiteMR?.focusFiles();
620+ k-keys from:body -> ermite.toggleKeys()">
609 </div>621 </div>
610 </body>622 </body>
611 </html>623 </html>
M internal/app/testdata/web/commit-modify.html +26 -14
@@ -119,7 +119,7 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
119 </dd>119 </dd>
120 </dl>120 </dl>
121 </header>121 </header>
122-<div class="diff-tools" role="toolbar" aria-label="Diff view">122+<div class="diff-tools" role="group" aria-label="Diff view">
123 <span class="dim">2 files · <span class="plus">+2</span>123 <span class="dim">2 files · <span class="plus">+2</span>
124 <span class="minus">-1</span>124 <span class="minus">-1</span>
125 </span>125 </span>
@@ -348,6 +348,11 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
349 <kbd>esc</kbd> close</button>349 <kbd>esc</kbd> close</button>
350 </div>350 </div>
351+<div class="keys-pref">
352+<label>
353+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
354+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
355+</div>
351 <dl class="keys">356 <dl class="keys">
352 <div>357 <div>
353 <dt>358 <dt>
@@ -501,72 +506,78 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
501 <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()">506 <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()">
502 <div class="panel-head">507 <div class="panel-head">
503 <label for="cmd-q" class="sr">go to</label>508 <label for="cmd-q" class="sr">go to</label>
504-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">509+<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)">
505 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">510 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
506 <kbd>esc</kbd>511 <kbd>esc</kbd>
507 </button>512 </button>
508 </div>513 </div>
509-<ul id="cmd-list" class="cmd-list" aria-label="Targets">514+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
510-<li data-k="go home" data-cursor>515+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
511 <a href="/" tabindex="-1">516 <a href="/" tabindex="-1">
512 <span class="lbl">home</span>517 <span class="lbl">home</span>
513 <span class="k">go</span>518 <span class="k">go</span>
514 </a>519 </a>
515 </li>520 </li>
516-<li data-k="section summary">521+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
517 <a href="/diffs" tabindex="-1">522 <a href="/diffs" tabindex="-1">
518 <span class="lbl">summary</span>523 <span class="lbl">summary</span>
519 <span class="k">section</span>524 <span class="k">section</span>
520 </a>525 </a>
521 </li>526 </li>
522-<li data-k="section files">527+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
523 <a href="/diffs/tree/main" tabindex="-1">528 <a href="/diffs/tree/main" tabindex="-1">
524 <span class="lbl">files</span>529 <span class="lbl">files</span>
525 <span class="k">section</span>530 <span class="k">section</span>
526 </a>531 </a>
527 </li>532 </li>
528-<li data-k="section commits">533+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
529 <a href="/diffs/commits/main" tabindex="-1">534 <a href="/diffs/commits/main" tabindex="-1">
530 <span class="lbl">commits</span>535 <span class="lbl">commits</span>
531 <span class="k">section</span>536 <span class="k">section</span>
532 </a>537 </a>
533 </li>538 </li>
534-<li data-k="section branches &amp; tags">539+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
535 <a href="/diffs/refs" tabindex="-1">540 <a href="/diffs/refs" tabindex="-1">
536 <span class="lbl">branches &amp; tags</span>541 <span class="lbl">branches &amp; tags</span>
537 <span class="k">section</span>542 <span class="k">section</span>
538 </a>543 </a>
539 </li>544 </li>
540-<li data-k="section merge requests">545+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
541 <a href="/diffs/merge-requests" tabindex="-1">546 <a href="/diffs/merge-requests" tabindex="-1">
542 <span class="lbl">merge requests</span>547 <span class="lbl">merge requests</span>
543 <span class="k">section</span>548 <span class="k">section</span>
544 </a>549 </a>
545 </li>550 </li>
546-<li data-k="section actions">551+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
547 <a href="/diffs/actions" tabindex="-1">552 <a href="/diffs/actions" tabindex="-1">
548 <span class="lbl">actions</span>553 <span class="lbl">actions</span>
549 <span class="k">section</span>554 <span class="k">section</span>
550 </a>555 </a>
551 </li>556 </li>
552-<li data-k="branch main">557+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
553 <a href="/diffs/tree/main" tabindex="-1">558 <a href="/diffs/tree/main" tabindex="-1">
554 <span class="lbl">main</span>559 <span class="lbl">main</span>
555 <span class="k">branch</span>560 <span class="k">branch</span>
556 </a>561 </a>
557 </li>562 </li>
558-<li data-k="branch side">563+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
559 <a href="/diffs/tree/side" tabindex="-1">564 <a href="/diffs/tree/side" tabindex="-1">
560 <span class="lbl">side</span>565 <span class="lbl">side</span>
561 <span class="k">branch</span>566 <span class="k">branch</span>
562 </a>567 </a>
563 </li>568 </li>
564-<li data-k="tag v1.0.0">569+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
565 <a href="/diffs/tree/v1.0.0" tabindex="-1">570 <a href="/diffs/tree/v1.0.0" tabindex="-1">
566 <span class="lbl">v1.0.0</span>571 <span class="lbl">v1.0.0</span>
567 <span class="k">tag</span>572 <span class="k">tag</span>
568 </a>573 </a>
569 </li>574 </li>
575+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
576+<a href="#" data-fire="keys" tabindex="-1">
577+<span class="lbl">turn single-key shortcuts off</span>
578+<span class="k">action</span>
579+</a>
580+</li>
570 </ul>581 </ul>
571 <p class="panel-foot">582 <p class="panel-foot">
572 <span>583 <span>
@@ -605,7 +616,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
605 k-permalink from:body -> ermite.permalink();616 k-permalink from:body -> ermite.permalink();
606 k-merge from:body -> window.ermiteMR?.openMerge();617 k-merge from:body -> window.ermiteMR?.openMerge();
607 k-read from:body -> window.ermiteMR?.markRead();618 k-read from:body -> window.ermiteMR?.markRead();
608- k-gf from:body -> window.ermiteMR?.focusFiles()">619+ k-gf from:body -> window.ermiteMR?.focusFiles();
620+ k-keys from:body -> ermite.toggleKeys()">
609 </div>621 </div>
610 </body>622 </body>
611 </html>623 </html>
M internal/app/testdata/web/commit-rename.html +26 -14
@@ -113,7 +113,7 @@
113 </dd>113 </dd>
114 </dl>114 </dl>
115 </header>115 </header>
116-<div class="diff-tools" role="toolbar" aria-label="Diff view">116+<div class="diff-tools" role="group" aria-label="Diff view">
117 <span class="dim">2 files · <span class="plus">+0</span>117 <span class="dim">2 files · <span class="plus">+0</span>
118 <span class="minus">-1</span>118 <span class="minus">-1</span>
119 </span>119 </span>
@@ -252,6 +252,11 @@
252 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">252 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
253 <kbd>esc</kbd> close</button>253 <kbd>esc</kbd> close</button>
254 </div>254 </div>
255+<div class="keys-pref">
256+<label>
257+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
258+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
259+</div>
255 <dl class="keys">260 <dl class="keys">
256 <div>261 <div>
257 <dt>262 <dt>
@@ -405,72 +410,78 @@
405 <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()">410 <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()">
406 <div class="panel-head">411 <div class="panel-head">
407 <label for="cmd-q" class="sr">go to</label>412 <label for="cmd-q" class="sr">go to</label>
408-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">413+<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()">414 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
410 <kbd>esc</kbd>415 <kbd>esc</kbd>
411 </button>416 </button>
412 </div>417 </div>
413-<ul id="cmd-list" class="cmd-list" aria-label="Targets">418+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
414-<li data-k="go home" data-cursor>419+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
415 <a href="/" tabindex="-1">420 <a href="/" tabindex="-1">
416 <span class="lbl">home</span>421 <span class="lbl">home</span>
417 <span class="k">go</span>422 <span class="k">go</span>
418 </a>423 </a>
419 </li>424 </li>
420-<li data-k="section summary">425+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
421 <a href="/diffs" tabindex="-1">426 <a href="/diffs" tabindex="-1">
422 <span class="lbl">summary</span>427 <span class="lbl">summary</span>
423 <span class="k">section</span>428 <span class="k">section</span>
424 </a>429 </a>
425 </li>430 </li>
426-<li data-k="section files">431+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
427 <a href="/diffs/tree/main" tabindex="-1">432 <a href="/diffs/tree/main" tabindex="-1">
428 <span class="lbl">files</span>433 <span class="lbl">files</span>
429 <span class="k">section</span>434 <span class="k">section</span>
430 </a>435 </a>
431 </li>436 </li>
432-<li data-k="section commits">437+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
433 <a href="/diffs/commits/main" tabindex="-1">438 <a href="/diffs/commits/main" tabindex="-1">
434 <span class="lbl">commits</span>439 <span class="lbl">commits</span>
435 <span class="k">section</span>440 <span class="k">section</span>
436 </a>441 </a>
437 </li>442 </li>
438-<li data-k="section branches &amp; tags">443+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
439 <a href="/diffs/refs" tabindex="-1">444 <a href="/diffs/refs" tabindex="-1">
440 <span class="lbl">branches &amp; tags</span>445 <span class="lbl">branches &amp; tags</span>
441 <span class="k">section</span>446 <span class="k">section</span>
442 </a>447 </a>
443 </li>448 </li>
444-<li data-k="section merge requests">449+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
445 <a href="/diffs/merge-requests" tabindex="-1">450 <a href="/diffs/merge-requests" tabindex="-1">
446 <span class="lbl">merge requests</span>451 <span class="lbl">merge requests</span>
447 <span class="k">section</span>452 <span class="k">section</span>
448 </a>453 </a>
449 </li>454 </li>
450-<li data-k="section actions">455+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
451 <a href="/diffs/actions" tabindex="-1">456 <a href="/diffs/actions" tabindex="-1">
452 <span class="lbl">actions</span>457 <span class="lbl">actions</span>
453 <span class="k">section</span>458 <span class="k">section</span>
454 </a>459 </a>
455 </li>460 </li>
456-<li data-k="branch main">461+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
457 <a href="/diffs/tree/main" tabindex="-1">462 <a href="/diffs/tree/main" tabindex="-1">
458 <span class="lbl">main</span>463 <span class="lbl">main</span>
459 <span class="k">branch</span>464 <span class="k">branch</span>
460 </a>465 </a>
461 </li>466 </li>
462-<li data-k="branch side">467+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
463 <a href="/diffs/tree/side" tabindex="-1">468 <a href="/diffs/tree/side" tabindex="-1">
464 <span class="lbl">side</span>469 <span class="lbl">side</span>
465 <span class="k">branch</span>470 <span class="k">branch</span>
466 </a>471 </a>
467 </li>472 </li>
468-<li data-k="tag v1.0.0">473+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
469 <a href="/diffs/tree/v1.0.0" tabindex="-1">474 <a href="/diffs/tree/v1.0.0" tabindex="-1">
470 <span class="lbl">v1.0.0</span>475 <span class="lbl">v1.0.0</span>
471 <span class="k">tag</span>476 <span class="k">tag</span>
472 </a>477 </a>
473 </li>478 </li>
479+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
480+<a href="#" data-fire="keys" tabindex="-1">
481+<span class="lbl">turn single-key shortcuts off</span>
482+<span class="k">action</span>
483+</a>
484+</li>
474 </ul>485 </ul>
475 <p class="panel-foot">486 <p class="panel-foot">
476 <span>487 <span>
@@ -509,7 +520,8 @@
509 k-permalink from:body -> ermite.permalink();520 k-permalink from:body -> ermite.permalink();
510 k-merge from:body -> window.ermiteMR?.openMerge();521 k-merge from:body -> window.ermiteMR?.openMerge();
511 k-read from:body -> window.ermiteMR?.markRead();522 k-read from:body -> window.ermiteMR?.markRead();
512- k-gf from:body -> window.ermiteMR?.focusFiles()">523+ k-gf from:body -> window.ermiteMR?.focusFiles();
524+ k-keys from:body -> ermite.toggleKeys()">
513 </div>525 </div>
514 </body>526 </body>
515 </html>527 </html>
M internal/app/testdata/web/commit-root.html +26 -14
@@ -111,7 +111,7 @@
111 <dd>none (root commit) </dd>111 <dd>none (root commit) </dd>
112 </dl>112 </dl>
113 </header>113 </header>
114-<div class="diff-tools" role="toolbar" aria-label="Diff view">114+<div class="diff-tools" role="group" aria-label="Diff view">
115 <span class="dim">5 files · <span class="plus">+11</span>115 <span class="dim">5 files · <span class="plus">+11</span>
116 <span class="minus">-0</span>116 <span class="minus">-0</span>
117 </span>117 </span>
@@ -571,6 +571,11 @@
571 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">571 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
572 <kbd>esc</kbd> close</button>572 <kbd>esc</kbd> close</button>
573 </div>573 </div>
574+<div class="keys-pref">
575+<label>
576+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
577+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
578+</div>
574 <dl class="keys">579 <dl class="keys">
575 <div>580 <div>
576 <dt>581 <dt>
@@ -724,72 +729,78 @@
724 <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()">729 <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()">
725 <div class="panel-head">730 <div class="panel-head">
726 <label for="cmd-q" class="sr">go to</label>731 <label for="cmd-q" class="sr">go to</label>
727-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">732+<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)">
728 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">733 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
729 <kbd>esc</kbd>734 <kbd>esc</kbd>
730 </button>735 </button>
731 </div>736 </div>
732-<ul id="cmd-list" class="cmd-list" aria-label="Targets">737+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
733-<li data-k="go home" data-cursor>738+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
734 <a href="/" tabindex="-1">739 <a href="/" tabindex="-1">
735 <span class="lbl">home</span>740 <span class="lbl">home</span>
736 <span class="k">go</span>741 <span class="k">go</span>
737 </a>742 </a>
738 </li>743 </li>
739-<li data-k="section summary">744+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
740 <a href="/diffs" tabindex="-1">745 <a href="/diffs" tabindex="-1">
741 <span class="lbl">summary</span>746 <span class="lbl">summary</span>
742 <span class="k">section</span>747 <span class="k">section</span>
743 </a>748 </a>
744 </li>749 </li>
745-<li data-k="section files">750+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
746 <a href="/diffs/tree/main" tabindex="-1">751 <a href="/diffs/tree/main" tabindex="-1">
747 <span class="lbl">files</span>752 <span class="lbl">files</span>
748 <span class="k">section</span>753 <span class="k">section</span>
749 </a>754 </a>
750 </li>755 </li>
751-<li data-k="section commits">756+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
752 <a href="/diffs/commits/main" tabindex="-1">757 <a href="/diffs/commits/main" tabindex="-1">
753 <span class="lbl">commits</span>758 <span class="lbl">commits</span>
754 <span class="k">section</span>759 <span class="k">section</span>
755 </a>760 </a>
756 </li>761 </li>
757-<li data-k="section branches &amp; tags">762+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
758 <a href="/diffs/refs" tabindex="-1">763 <a href="/diffs/refs" tabindex="-1">
759 <span class="lbl">branches &amp; tags</span>764 <span class="lbl">branches &amp; tags</span>
760 <span class="k">section</span>765 <span class="k">section</span>
761 </a>766 </a>
762 </li>767 </li>
763-<li data-k="section merge requests">768+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
764 <a href="/diffs/merge-requests" tabindex="-1">769 <a href="/diffs/merge-requests" tabindex="-1">
765 <span class="lbl">merge requests</span>770 <span class="lbl">merge requests</span>
766 <span class="k">section</span>771 <span class="k">section</span>
767 </a>772 </a>
768 </li>773 </li>
769-<li data-k="section actions">774+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
770 <a href="/diffs/actions" tabindex="-1">775 <a href="/diffs/actions" tabindex="-1">
771 <span class="lbl">actions</span>776 <span class="lbl">actions</span>
772 <span class="k">section</span>777 <span class="k">section</span>
773 </a>778 </a>
774 </li>779 </li>
775-<li data-k="branch main">780+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
776 <a href="/diffs/tree/main" tabindex="-1">781 <a href="/diffs/tree/main" tabindex="-1">
777 <span class="lbl">main</span>782 <span class="lbl">main</span>
778 <span class="k">branch</span>783 <span class="k">branch</span>
779 </a>784 </a>
780 </li>785 </li>
781-<li data-k="branch side">786+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
782 <a href="/diffs/tree/side" tabindex="-1">787 <a href="/diffs/tree/side" tabindex="-1">
783 <span class="lbl">side</span>788 <span class="lbl">side</span>
784 <span class="k">branch</span>789 <span class="k">branch</span>
785 </a>790 </a>
786 </li>791 </li>
787-<li data-k="tag v1.0.0">792+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
788 <a href="/diffs/tree/v1.0.0" tabindex="-1">793 <a href="/diffs/tree/v1.0.0" tabindex="-1">
789 <span class="lbl">v1.0.0</span>794 <span class="lbl">v1.0.0</span>
790 <span class="k">tag</span>795 <span class="k">tag</span>
791 </a>796 </a>
792 </li>797 </li>
798+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
799+<a href="#" data-fire="keys" tabindex="-1">
800+<span class="lbl">turn single-key shortcuts off</span>
801+<span class="k">action</span>
802+</a>
803+</li>
793 </ul>804 </ul>
794 <p class="panel-foot">805 <p class="panel-foot">
795 <span>806 <span>
@@ -828,7 +839,8 @@
828 k-permalink from:body -> ermite.permalink();839 k-permalink from:body -> ermite.permalink();
829 k-merge from:body -> window.ermiteMR?.openMerge();840 k-merge from:body -> window.ermiteMR?.openMerge();
830 k-read from:body -> window.ermiteMR?.markRead();841 k-read from:body -> window.ermiteMR?.markRead();
831- k-gf from:body -> window.ermiteMR?.focusFiles()">842+ k-gf from:body -> window.ermiteMR?.focusFiles();
843+ k-keys from:body -> ermite.toggleKeys()">
832 </div>844 </div>
833 </body>845 </body>
834 </html>846 </html>
M internal/app/testdata/web/commit-trojan.html +26 -14
@@ -114,7 +114,7 @@
114 </dd>114 </dd>
115 </dl>115 </dl>
116 </header>116 </header>
117-<div class="diff-tools" role="toolbar" aria-label="Diff view">117+<div class="diff-tools" role="group" aria-label="Diff view">
118 <span class="dim">1 file · <span class="plus">+8</span>118 <span class="dim">1 file · <span class="plus">+8</span>
119 <span class="minus">-0</span>119 <span class="minus">-0</span>
120 </span>120 </span>
@@ -360,6 +360,11 @@
360 <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" hx-on:click="closest('dialog').close()">
361 <kbd>esc</kbd> close</button>361 <kbd>esc</kbd> close</button>
362 </div>362 </div>
363+<div class="keys-pref">
364+<label>
365+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
366+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
367+</div>
363 <dl class="keys">368 <dl class="keys">
364 <div>369 <div>
365 <dt>370 <dt>
@@ -513,72 +518,78 @@
513 <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()">518 <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()">
514 <div class="panel-head">519 <div class="panel-head">
515 <label for="cmd-q" class="sr">go to</label>520 <label for="cmd-q" class="sr">go to</label>
516-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">521+<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)">
517 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">522 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
518 <kbd>esc</kbd>523 <kbd>esc</kbd>
519 </button>524 </button>
520 </div>525 </div>
521-<ul id="cmd-list" class="cmd-list" aria-label="Targets">526+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
522-<li data-k="go home" data-cursor>527+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
523 <a href="/" tabindex="-1">528 <a href="/" tabindex="-1">
524 <span class="lbl">home</span>529 <span class="lbl">home</span>
525 <span class="k">go</span>530 <span class="k">go</span>
526 </a>531 </a>
527 </li>532 </li>
528-<li data-k="section summary">533+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
529 <a href="/diffs" tabindex="-1">534 <a href="/diffs" tabindex="-1">
530 <span class="lbl">summary</span>535 <span class="lbl">summary</span>
531 <span class="k">section</span>536 <span class="k">section</span>
532 </a>537 </a>
533 </li>538 </li>
534-<li data-k="section files">539+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
535 <a href="/diffs/tree/main" tabindex="-1">540 <a href="/diffs/tree/main" tabindex="-1">
536 <span class="lbl">files</span>541 <span class="lbl">files</span>
537 <span class="k">section</span>542 <span class="k">section</span>
538 </a>543 </a>
539 </li>544 </li>
540-<li data-k="section commits">545+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
541 <a href="/diffs/commits/main" tabindex="-1">546 <a href="/diffs/commits/main" tabindex="-1">
542 <span class="lbl">commits</span>547 <span class="lbl">commits</span>
543 <span class="k">section</span>548 <span class="k">section</span>
544 </a>549 </a>
545 </li>550 </li>
546-<li data-k="section branches &amp; tags">551+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
547 <a href="/diffs/refs" tabindex="-1">552 <a href="/diffs/refs" tabindex="-1">
548 <span class="lbl">branches &amp; tags</span>553 <span class="lbl">branches &amp; tags</span>
549 <span class="k">section</span>554 <span class="k">section</span>
550 </a>555 </a>
551 </li>556 </li>
552-<li data-k="section merge requests">557+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
553 <a href="/diffs/merge-requests" tabindex="-1">558 <a href="/diffs/merge-requests" tabindex="-1">
554 <span class="lbl">merge requests</span>559 <span class="lbl">merge requests</span>
555 <span class="k">section</span>560 <span class="k">section</span>
556 </a>561 </a>
557 </li>562 </li>
558-<li data-k="section actions">563+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
559 <a href="/diffs/actions" tabindex="-1">564 <a href="/diffs/actions" tabindex="-1">
560 <span class="lbl">actions</span>565 <span class="lbl">actions</span>
561 <span class="k">section</span>566 <span class="k">section</span>
562 </a>567 </a>
563 </li>568 </li>
564-<li data-k="branch main">569+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
565 <a href="/diffs/tree/main" tabindex="-1">570 <a href="/diffs/tree/main" tabindex="-1">
566 <span class="lbl">main</span>571 <span class="lbl">main</span>
567 <span class="k">branch</span>572 <span class="k">branch</span>
568 </a>573 </a>
569 </li>574 </li>
570-<li data-k="branch side">575+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
571 <a href="/diffs/tree/side" tabindex="-1">576 <a href="/diffs/tree/side" tabindex="-1">
572 <span class="lbl">side</span>577 <span class="lbl">side</span>
573 <span class="k">branch</span>578 <span class="k">branch</span>
574 </a>579 </a>
575 </li>580 </li>
576-<li data-k="tag v1.0.0">581+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
577 <a href="/diffs/tree/v1.0.0" tabindex="-1">582 <a href="/diffs/tree/v1.0.0" tabindex="-1">
578 <span class="lbl">v1.0.0</span>583 <span class="lbl">v1.0.0</span>
579 <span class="k">tag</span>584 <span class="k">tag</span>
580 </a>585 </a>
581 </li>586 </li>
587+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
588+<a href="#" data-fire="keys" tabindex="-1">
589+<span class="lbl">turn single-key shortcuts off</span>
590+<span class="k">action</span>
591+</a>
592+</li>
582 </ul>593 </ul>
583 <p class="panel-foot">594 <p class="panel-foot">
584 <span>595 <span>
@@ -617,7 +628,8 @@
617 k-permalink from:body -> ermite.permalink();628 k-permalink from:body -> ermite.permalink();
618 k-merge from:body -> window.ermiteMR?.openMerge();629 k-merge from:body -> window.ermiteMR?.openMerge();
619 k-read from:body -> window.ermiteMR?.markRead();630 k-read from:body -> window.ermiteMR?.markRead();
620- k-gf from:body -> window.ermiteMR?.focusFiles()">631+ k-gf from:body -> window.ermiteMR?.focusFiles();
632+ k-keys from:body -> ermite.toggleKeys()">
621 </div>633 </div>
622 </body>634 </body>
623 </html>635 </html>
M internal/app/testdata/web/commits-page2.html +24 -12
@@ -184,6 +184,11 @@
184 <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" hx-on:click="closest('dialog').close()">
185 <kbd>esc</kbd> close</button>185 <kbd>esc</kbd> close</button>
186 </div>186 </div>
187+<div class="keys-pref">
188+<label>
189+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
190+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
191+</div>
187 <dl class="keys">192 <dl class="keys">
188 <div>193 <div>
189 <dt>194 <dt>
@@ -329,66 +334,72 @@
329 <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()">334 <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()">
330 <div class="panel-head">335 <div class="panel-head">
331 <label for="cmd-q" class="sr">go to</label>336 <label for="cmd-q" class="sr">go to</label>
332-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">337+<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)">
333 <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" hx-on:click="closest('dialog').close()">
334 <kbd>esc</kbd>339 <kbd>esc</kbd>
335 </button>340 </button>
336 </div>341 </div>
337-<ul id="cmd-list" class="cmd-list" aria-label="Targets">342+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
338-<li data-k="go home" data-cursor>343+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
339 <a href="/" tabindex="-1">344 <a href="/" tabindex="-1">
340 <span class="lbl">home</span>345 <span class="lbl">home</span>
341 <span class="k">go</span>346 <span class="k">go</span>
342 </a>347 </a>
343 </li>348 </li>
344-<li data-k="section summary">349+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
345 <a href="/long" tabindex="-1">350 <a href="/long" tabindex="-1">
346 <span class="lbl">summary</span>351 <span class="lbl">summary</span>
347 <span class="k">section</span>352 <span class="k">section</span>
348 </a>353 </a>
349 </li>354 </li>
350-<li data-k="section files">355+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
351 <a href="/long/tree/main" tabindex="-1">356 <a href="/long/tree/main" tabindex="-1">
352 <span class="lbl">files</span>357 <span class="lbl">files</span>
353 <span class="k">section</span>358 <span class="k">section</span>
354 </a>359 </a>
355 </li>360 </li>
356-<li data-k="section commits">361+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
357 <a href="/long/commits/main" tabindex="-1">362 <a href="/long/commits/main" tabindex="-1">
358 <span class="lbl">commits</span>363 <span class="lbl">commits</span>
359 <span class="k">section</span>364 <span class="k">section</span>
360 </a>365 </a>
361 </li>366 </li>
362-<li data-k="section branches &amp; tags">367+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
363 <a href="/long/refs" tabindex="-1">368 <a href="/long/refs" tabindex="-1">
364 <span class="lbl">branches &amp; tags</span>369 <span class="lbl">branches &amp; tags</span>
365 <span class="k">section</span>370 <span class="k">section</span>
366 </a>371 </a>
367 </li>372 </li>
368-<li data-k="section merge requests">373+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
369 <a href="/long/merge-requests" tabindex="-1">374 <a href="/long/merge-requests" tabindex="-1">
370 <span class="lbl">merge requests</span>375 <span class="lbl">merge requests</span>
371 <span class="k">section</span>376 <span class="k">section</span>
372 </a>377 </a>
373 </li>378 </li>
374-<li data-k="section actions">379+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
375 <a href="/long/actions" tabindex="-1">380 <a href="/long/actions" tabindex="-1">
376 <span class="lbl">actions</span>381 <span class="lbl">actions</span>
377 <span class="k">section</span>382 <span class="k">section</span>
378 </a>383 </a>
379 </li>384 </li>
380-<li data-k="branch main">385+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
381 <a href="/long/tree/main" tabindex="-1">386 <a href="/long/tree/main" tabindex="-1">
382 <span class="lbl">main</span>387 <span class="lbl">main</span>
383 <span class="k">branch</span>388 <span class="k">branch</span>
384 </a>389 </a>
385 </li>390 </li>
386-<li data-k="repo diffs">391+<li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs">
387 <a href="/diffs" tabindex="-1">392 <a href="/diffs" tabindex="-1">
388 <span class="lbl">diffs</span>393 <span class="lbl">diffs</span>
389 <span class="k">repo</span>394 <span class="k">repo</span>
390 </a>395 </a>
391 </li>396 </li>
397+<li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
398+<a href="#" data-fire="keys" tabindex="-1">
399+<span class="lbl">turn single-key shortcuts off</span>
400+<span class="k">action</span>
401+</a>
402+</li>
392 </ul>403 </ul>
393 <p class="panel-foot">404 <p class="panel-foot">
394 <span>405 <span>
@@ -427,7 +438,8 @@
427 k-permalink from:body -> ermite.permalink();438 k-permalink from:body -> ermite.permalink();
428 k-merge from:body -> window.ermiteMR?.openMerge();439 k-merge from:body -> window.ermiteMR?.openMerge();
429 k-read from:body -> window.ermiteMR?.markRead();440 k-read from:body -> window.ermiteMR?.markRead();
430- k-gf from:body -> window.ermiteMR?.focusFiles()">441+ k-gf from:body -> window.ermiteMR?.focusFiles();
442+ k-keys from:body -> ermite.toggleKeys()">
431 </div>443 </div>
432 </body>444 </body>
433 </html>445 </html>
M internal/app/testdata/web/commits-path.html +24 -12
@@ -199,6 +199,11 @@
199 <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" hx-on:click="closest('dialog').close()">
200 <kbd>esc</kbd> close</button>200 <kbd>esc</kbd> close</button>
201 </div>201 </div>
202+<div class="keys-pref">
203+<label>
204+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
205+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
206+</div>
202 <dl class="keys">207 <dl class="keys">
203 <div>208 <div>
204 <dt>209 <dt>
@@ -344,66 +349,72 @@
344 <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()">349 <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()">
345 <div class="panel-head">350 <div class="panel-head">
346 <label for="cmd-q" class="sr">go to</label>351 <label for="cmd-q" class="sr">go to</label>
347-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">352+<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)">
348 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">353 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
349 <kbd>esc</kbd>354 <kbd>esc</kbd>
350 </button>355 </button>
351 </div>356 </div>
352-<ul id="cmd-list" class="cmd-list" aria-label="Targets">357+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
353-<li data-k="go home" data-cursor>358+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
354 <a href="/" tabindex="-1">359 <a href="/" tabindex="-1">
355 <span class="lbl">home</span>360 <span class="lbl">home</span>
356 <span class="k">go</span>361 <span class="k">go</span>
357 </a>362 </a>
358 </li>363 </li>
359-<li data-k="section summary">364+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
360 <a href="/long" tabindex="-1">365 <a href="/long" tabindex="-1">
361 <span class="lbl">summary</span>366 <span class="lbl">summary</span>
362 <span class="k">section</span>367 <span class="k">section</span>
363 </a>368 </a>
364 </li>369 </li>
365-<li data-k="section files">370+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
366 <a href="/long/tree/main" tabindex="-1">371 <a href="/long/tree/main" tabindex="-1">
367 <span class="lbl">files</span>372 <span class="lbl">files</span>
368 <span class="k">section</span>373 <span class="k">section</span>
369 </a>374 </a>
370 </li>375 </li>
371-<li data-k="section commits">376+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
372 <a href="/long/commits/main" tabindex="-1">377 <a href="/long/commits/main" tabindex="-1">
373 <span class="lbl">commits</span>378 <span class="lbl">commits</span>
374 <span class="k">section</span>379 <span class="k">section</span>
375 </a>380 </a>
376 </li>381 </li>
377-<li data-k="section branches &amp; tags">382+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
378 <a href="/long/refs" tabindex="-1">383 <a href="/long/refs" tabindex="-1">
379 <span class="lbl">branches &amp; tags</span>384 <span class="lbl">branches &amp; tags</span>
380 <span class="k">section</span>385 <span class="k">section</span>
381 </a>386 </a>
382 </li>387 </li>
383-<li data-k="section merge requests">388+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
384 <a href="/long/merge-requests" tabindex="-1">389 <a href="/long/merge-requests" tabindex="-1">
385 <span class="lbl">merge requests</span>390 <span class="lbl">merge requests</span>
386 <span class="k">section</span>391 <span class="k">section</span>
387 </a>392 </a>
388 </li>393 </li>
389-<li data-k="section actions">394+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
390 <a href="/long/actions" tabindex="-1">395 <a href="/long/actions" tabindex="-1">
391 <span class="lbl">actions</span>396 <span class="lbl">actions</span>
392 <span class="k">section</span>397 <span class="k">section</span>
393 </a>398 </a>
394 </li>399 </li>
395-<li data-k="branch main">400+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
396 <a href="/long/tree/main" tabindex="-1">401 <a href="/long/tree/main" tabindex="-1">
397 <span class="lbl">main</span>402 <span class="lbl">main</span>
398 <span class="k">branch</span>403 <span class="k">branch</span>
399 </a>404 </a>
400 </li>405 </li>
401-<li data-k="repo diffs">406+<li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs">
402 <a href="/diffs" tabindex="-1">407 <a href="/diffs" tabindex="-1">
403 <span class="lbl">diffs</span>408 <span class="lbl">diffs</span>
404 <span class="k">repo</span>409 <span class="k">repo</span>
405 </a>410 </a>
406 </li>411 </li>
412+<li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
413+<a href="#" data-fire="keys" tabindex="-1">
414+<span class="lbl">turn single-key shortcuts off</span>
415+<span class="k">action</span>
416+</a>
417+</li>
407 </ul>418 </ul>
408 <p class="panel-foot">419 <p class="panel-foot">
409 <span>420 <span>
@@ -442,7 +453,8 @@
442 k-permalink from:body -> ermite.permalink();453 k-permalink from:body -> ermite.permalink();
443 k-merge from:body -> window.ermiteMR?.openMerge();454 k-merge from:body -> window.ermiteMR?.openMerge();
444 k-read from:body -> window.ermiteMR?.markRead();455 k-read from:body -> window.ermiteMR?.markRead();
445- k-gf from:body -> window.ermiteMR?.focusFiles()">456+ k-gf from:body -> window.ermiteMR?.focusFiles();
457+ k-keys from:body -> ermite.toggleKeys()">
446 </div>458 </div>
447 </body>459 </body>
448 </html>460 </html>
M internal/app/testdata/web/commits-root.html +24 -12
@@ -1084,6 +1084,11 @@
1084 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">1084 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
1085 <kbd>esc</kbd> close</button>1085 <kbd>esc</kbd> close</button>
1086 </div>1086 </div>
1087+<div class="keys-pref">
1088+<label>
1089+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
1090+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
1091+</div>
1087 <dl class="keys">1092 <dl class="keys">
1088 <div>1093 <div>
1089 <dt>1094 <dt>
@@ -1229,66 +1234,72 @@
1229 <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()">1234 <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()">
1230 <div class="panel-head">1235 <div class="panel-head">
1231 <label for="cmd-q" class="sr">go to</label>1236 <label for="cmd-q" class="sr">go to</label>
1232-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">1237+<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)">
1233 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">1238 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
1234 <kbd>esc</kbd>1239 <kbd>esc</kbd>
1235 </button>1240 </button>
1236 </div>1241 </div>
1237-<ul id="cmd-list" class="cmd-list" aria-label="Targets">1242+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
1238-<li data-k="go home" data-cursor>1243+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
1239 <a href="/" tabindex="-1">1244 <a href="/" tabindex="-1">
1240 <span class="lbl">home</span>1245 <span class="lbl">home</span>
1241 <span class="k">go</span>1246 <span class="k">go</span>
1242 </a>1247 </a>
1243 </li>1248 </li>
1244-<li data-k="section summary">1249+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
1245 <a href="/long" tabindex="-1">1250 <a href="/long" tabindex="-1">
1246 <span class="lbl">summary</span>1251 <span class="lbl">summary</span>
1247 <span class="k">section</span>1252 <span class="k">section</span>
1248 </a>1253 </a>
1249 </li>1254 </li>
1250-<li data-k="section files">1255+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
1251 <a href="/long/tree/main" tabindex="-1">1256 <a href="/long/tree/main" tabindex="-1">
1252 <span class="lbl">files</span>1257 <span class="lbl">files</span>
1253 <span class="k">section</span>1258 <span class="k">section</span>
1254 </a>1259 </a>
1255 </li>1260 </li>
1256-<li data-k="section commits">1261+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
1257 <a href="/long/commits/main" tabindex="-1">1262 <a href="/long/commits/main" tabindex="-1">
1258 <span class="lbl">commits</span>1263 <span class="lbl">commits</span>
1259 <span class="k">section</span>1264 <span class="k">section</span>
1260 </a>1265 </a>
1261 </li>1266 </li>
1262-<li data-k="section branches &amp; tags">1267+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
1263 <a href="/long/refs" tabindex="-1">1268 <a href="/long/refs" tabindex="-1">
1264 <span class="lbl">branches &amp; tags</span>1269 <span class="lbl">branches &amp; tags</span>
1265 <span class="k">section</span>1270 <span class="k">section</span>
1266 </a>1271 </a>
1267 </li>1272 </li>
1268-<li data-k="section merge requests">1273+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
1269 <a href="/long/merge-requests" tabindex="-1">1274 <a href="/long/merge-requests" tabindex="-1">
1270 <span class="lbl">merge requests</span>1275 <span class="lbl">merge requests</span>
1271 <span class="k">section</span>1276 <span class="k">section</span>
1272 </a>1277 </a>
1273 </li>1278 </li>
1274-<li data-k="section actions">1279+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
1275 <a href="/long/actions" tabindex="-1">1280 <a href="/long/actions" tabindex="-1">
1276 <span class="lbl">actions</span>1281 <span class="lbl">actions</span>
1277 <span class="k">section</span>1282 <span class="k">section</span>
1278 </a>1283 </a>
1279 </li>1284 </li>
1280-<li data-k="branch main">1285+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
1281 <a href="/long/tree/main" tabindex="-1">1286 <a href="/long/tree/main" tabindex="-1">
1282 <span class="lbl">main</span>1287 <span class="lbl">main</span>
1283 <span class="k">branch</span>1288 <span class="k">branch</span>
1284 </a>1289 </a>
1285 </li>1290 </li>
1286-<li data-k="repo diffs">1291+<li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs">
1287 <a href="/diffs" tabindex="-1">1292 <a href="/diffs" tabindex="-1">
1288 <span class="lbl">diffs</span>1293 <span class="lbl">diffs</span>
1289 <span class="k">repo</span>1294 <span class="k">repo</span>
1290 </a>1295 </a>
1291 </li>1296 </li>
1297+<li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
1298+<a href="#" data-fire="keys" tabindex="-1">
1299+<span class="lbl">turn single-key shortcuts off</span>
1300+<span class="k">action</span>
1301+</a>
1302+</li>
1292 </ul>1303 </ul>
1293 <p class="panel-foot">1304 <p class="panel-foot">
1294 <span>1305 <span>
@@ -1327,7 +1338,8 @@
1327 k-permalink from:body -> ermite.permalink();1338 k-permalink from:body -> ermite.permalink();
1328 k-merge from:body -> window.ermiteMR?.openMerge();1339 k-merge from:body -> window.ermiteMR?.openMerge();
1329 k-read from:body -> window.ermiteMR?.markRead();1340 k-read from:body -> window.ermiteMR?.markRead();
1330- k-gf from:body -> window.ermiteMR?.focusFiles()">1341+ k-gf from:body -> window.ermiteMR?.focusFiles();
1342+ k-keys from:body -> ermite.toggleKeys()">
1331 </div>1343 </div>
1332 </body>1344 </body>
1333 </html>1345 </html>
M internal/app/testdata/web/commits-small.html +26 -14
@@ -345,6 +345,11 @@
345 <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" hx-on:click="closest('dialog').close()">
346 <kbd>esc</kbd> close</button>346 <kbd>esc</kbd> close</button>
347 </div>347 </div>
348+<div class="keys-pref">
349+<label>
350+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
351+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
352+</div>
348 <dl class="keys">353 <dl class="keys">
349 <div>354 <div>
350 <dt>355 <dt>
@@ -498,78 +503,84 @@
498 <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()">503 <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()">
499 <div class="panel-head">504 <div class="panel-head">
500 <label for="cmd-q" class="sr">go to</label>505 <label for="cmd-q" class="sr">go to</label>
501-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">506+<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)">
502 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">507 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
503 <kbd>esc</kbd>508 <kbd>esc</kbd>
504 </button>509 </button>
505 </div>510 </div>
506-<ul id="cmd-list" class="cmd-list" aria-label="Targets">511+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
507-<li data-k="go home" data-cursor>512+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
508 <a href="/" tabindex="-1">513 <a href="/" tabindex="-1">
509 <span class="lbl">home</span>514 <span class="lbl">home</span>
510 <span class="k">go</span>515 <span class="k">go</span>
511 </a>516 </a>
512 </li>517 </li>
513-<li data-k="section summary">518+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
514 <a href="/diffs" tabindex="-1">519 <a href="/diffs" tabindex="-1">
515 <span class="lbl">summary</span>520 <span class="lbl">summary</span>
516 <span class="k">section</span>521 <span class="k">section</span>
517 </a>522 </a>
518 </li>523 </li>
519-<li data-k="section files">524+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
520 <a href="/diffs/tree/main" tabindex="-1">525 <a href="/diffs/tree/main" tabindex="-1">
521 <span class="lbl">files</span>526 <span class="lbl">files</span>
522 <span class="k">section</span>527 <span class="k">section</span>
523 </a>528 </a>
524 </li>529 </li>
525-<li data-k="section commits">530+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
526 <a href="/diffs/commits/main" tabindex="-1">531 <a href="/diffs/commits/main" tabindex="-1">
527 <span class="lbl">commits</span>532 <span class="lbl">commits</span>
528 <span class="k">section</span>533 <span class="k">section</span>
529 </a>534 </a>
530 </li>535 </li>
531-<li data-k="section branches &amp; tags">536+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
532 <a href="/diffs/refs" tabindex="-1">537 <a href="/diffs/refs" tabindex="-1">
533 <span class="lbl">branches &amp; tags</span>538 <span class="lbl">branches &amp; tags</span>
534 <span class="k">section</span>539 <span class="k">section</span>
535 </a>540 </a>
536 </li>541 </li>
537-<li data-k="section merge requests">542+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
538 <a href="/diffs/merge-requests" tabindex="-1">543 <a href="/diffs/merge-requests" tabindex="-1">
539 <span class="lbl">merge requests</span>544 <span class="lbl">merge requests</span>
540 <span class="k">section</span>545 <span class="k">section</span>
541 </a>546 </a>
542 </li>547 </li>
543-<li data-k="section actions">548+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
544 <a href="/diffs/actions" tabindex="-1">549 <a href="/diffs/actions" tabindex="-1">
545 <span class="lbl">actions</span>550 <span class="lbl">actions</span>
546 <span class="k">section</span>551 <span class="k">section</span>
547 </a>552 </a>
548 </li>553 </li>
549-<li data-k="branch main">554+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch main">
550 <a href="/diffs/tree/main" tabindex="-1">555 <a href="/diffs/tree/main" tabindex="-1">
551 <span class="lbl">main</span>556 <span class="lbl">main</span>
552 <span class="k">branch</span>557 <span class="k">branch</span>
553 </a>558 </a>
554 </li>559 </li>
555-<li data-k="branch side">560+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch side">
556 <a href="/diffs/tree/side" tabindex="-1">561 <a href="/diffs/tree/side" tabindex="-1">
557 <span class="lbl">side</span>562 <span class="lbl">side</span>
558 <span class="k">branch</span>563 <span class="k">branch</span>
559 </a>564 </a>
560 </li>565 </li>
561-<li data-k="tag v1.0.0">566+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0">
562 <a href="/diffs/tree/v1.0.0" tabindex="-1">567 <a href="/diffs/tree/v1.0.0" tabindex="-1">
563 <span class="lbl">v1.0.0</span>568 <span class="lbl">v1.0.0</span>
564 <span class="k">tag</span>569 <span class="k">tag</span>
565 </a>570 </a>
566 </li>571 </li>
567-<li data-k="repo long">572+<li id="cmd-o10" role="option" aria-selected="false" data-k="repo long">
568 <a href="/long" tabindex="-1">573 <a href="/long" tabindex="-1">
569 <span class="lbl">long</span>574 <span class="lbl">long</span>
570 <span class="k">repo</span>575 <span class="k">repo</span>
571 </a>576 </a>
572 </li>577 </li>
578+<li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
579+<a href="#" data-fire="keys" tabindex="-1">
580+<span class="lbl">turn single-key shortcuts off</span>
581+<span class="k">action</span>
582+</a>
583+</li>
573 </ul>584 </ul>
574 <p class="panel-foot">585 <p class="panel-foot">
575 <span>586 <span>
@@ -608,7 +619,8 @@
608 k-permalink from:body -> ermite.permalink();619 k-permalink from:body -> ermite.permalink();
609 k-merge from:body -> window.ermiteMR?.openMerge();620 k-merge from:body -> window.ermiteMR?.openMerge();
610 k-read from:body -> window.ermiteMR?.markRead();621 k-read from:body -> window.ermiteMR?.markRead();
611- k-gf from:body -> window.ermiteMR?.focusFiles()">622+ k-gf from:body -> window.ermiteMR?.focusFiles();
623+ k-keys from:body -> ermite.toggleKeys()">
612 </div>624 </div>
613 </body>625 </body>
614 </html>626 </html>
M internal/app/testdata/web/empty.html +17 -5
@@ -102,6 +102,11 @@
102 <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" hx-on:click="closest('dialog').close()">
103 <kbd>esc</kbd> close</button>103 <kbd>esc</kbd> close</button>
104 </div>104 </div>
105+<div class="keys-pref">
106+<label>
107+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
108+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
109+</div>
105 <dl class="keys">110 <dl class="keys">
106 <div>111 <div>
107 <dt>112 <dt>
@@ -220,24 +225,30 @@
220 <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()">225 <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()">
221 <div class="panel-head">226 <div class="panel-head">
222 <label for="cmd-q" class="sr">go to</label>227 <label for="cmd-q" class="sr">go to</label>
223-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">228+<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)">
224 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">229 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
225 <kbd>esc</kbd>230 <kbd>esc</kbd>
226 </button>231 </button>
227 </div>232 </div>
228-<ul id="cmd-list" class="cmd-list" aria-label="Targets">233+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
229-<li data-k="go home" data-cursor>234+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
230 <a href="/" tabindex="-1">235 <a href="/" tabindex="-1">
231 <span class="lbl">home</span>236 <span class="lbl">home</span>
232 <span class="k">go</span>237 <span class="k">go</span>
233 </a>238 </a>
234 </li>239 </li>
235-<li data-k="repo demo">240+<li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo">
236 <a href="/demo" tabindex="-1">241 <a href="/demo" tabindex="-1">
237 <span class="lbl">demo</span>242 <span class="lbl">demo</span>
238 <span class="k">repo</span>243 <span class="k">repo</span>
239 </a>244 </a>
240 </li>245 </li>
246+<li id="cmd-o2" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
247+<a href="#" data-fire="keys" tabindex="-1">
248+<span class="lbl">turn single-key shortcuts off</span>
249+<span class="k">action</span>
250+</a>
251+</li>
241 </ul>252 </ul>
242 <p class="panel-foot">253 <p class="panel-foot">
243 <span>254 <span>
@@ -276,7 +287,8 @@
276 k-permalink from:body -> ermite.permalink();287 k-permalink from:body -> ermite.permalink();
277 k-merge from:body -> window.ermiteMR?.openMerge();288 k-merge from:body -> window.ermiteMR?.openMerge();
278 k-read from:body -> window.ermiteMR?.markRead();289 k-read from:body -> window.ermiteMR?.markRead();
279- k-gf from:body -> window.ermiteMR?.focusFiles()">290+ k-gf from:body -> window.ermiteMR?.focusFiles();
291+ k-keys from:body -> ermite.toggleKeys()">
280 </div>292 </div>
281 </body>293 </body>
282 </html>294 </html>
M internal/app/testdata/web/home.html +18 -6
@@ -127,6 +127,11 @@
127 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">127 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
128 <kbd>esc</kbd> close</button>128 <kbd>esc</kbd> close</button>
129 </div>129 </div>
130+<div class="keys-pref">
131+<label>
132+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
133+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
134+</div>
130 <dl class="keys">135 <dl class="keys">
131 <div>136 <div>
132 <dt>137 <dt>
@@ -245,30 +250,36 @@
245 <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()">250 <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()">
246 <div class="panel-head">251 <div class="panel-head">
247 <label for="cmd-q" class="sr">go to</label>252 <label for="cmd-q" class="sr">go to</label>
248-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">253+<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)">
249 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">254 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
250 <kbd>esc</kbd>255 <kbd>esc</kbd>
251 </button>256 </button>
252 </div>257 </div>
253-<ul id="cmd-list" class="cmd-list" aria-label="Targets">258+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
254-<li data-k="go home" data-cursor>259+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
255 <a href="/" tabindex="-1">260 <a href="/" tabindex="-1">
256 <span class="lbl">home</span>261 <span class="lbl">home</span>
257 <span class="k">go</span>262 <span class="k">go</span>
258 </a>263 </a>
259 </li>264 </li>
260-<li data-k="repo demo">265+<li id="cmd-o1" role="option" aria-selected="false" data-k="repo demo">
261 <a href="/demo" tabindex="-1">266 <a href="/demo" tabindex="-1">
262 <span class="lbl">demo</span>267 <span class="lbl">demo</span>
263 <span class="k">repo</span>268 <span class="k">repo</span>
264 </a>269 </a>
265 </li>270 </li>
266-<li data-k="repo quiet">271+<li id="cmd-o2" role="option" aria-selected="false" data-k="repo quiet">
267 <a href="/quiet" tabindex="-1">272 <a href="/quiet" tabindex="-1">
268 <span class="lbl">quiet</span>273 <span class="lbl">quiet</span>
269 <span class="k">repo</span>274 <span class="k">repo</span>
270 </a>275 </a>
271 </li>276 </li>
277+<li id="cmd-o3" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
278+<a href="#" data-fire="keys" tabindex="-1">
279+<span class="lbl">turn single-key shortcuts off</span>
280+<span class="k">action</span>
281+</a>
282+</li>
272 </ul>283 </ul>
273 <p class="panel-foot">284 <p class="panel-foot">
274 <span>285 <span>
@@ -307,7 +318,8 @@
307 k-permalink from:body -> ermite.permalink();318 k-permalink from:body -> ermite.permalink();
308 k-merge from:body -> window.ermiteMR?.openMerge();319 k-merge from:body -> window.ermiteMR?.openMerge();
309 k-read from:body -> window.ermiteMR?.markRead();320 k-read from:body -> window.ermiteMR?.markRead();
310- k-gf from:body -> window.ermiteMR?.focusFiles()">321+ k-gf from:body -> window.ermiteMR?.focusFiles();
322+ k-keys from:body -> ermite.toggleKeys()">
311 </div>323 </div>
312 </body>324 </body>
313 </html>325 </html>
M internal/app/testdata/web/not-found-page.html +16 -4
@@ -91,6 +91,11 @@
91 <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" hx-on:click="closest('dialog').close()">
92 <kbd>esc</kbd> close</button>92 <kbd>esc</kbd> close</button>
93 </div>93 </div>
94+<div class="keys-pref">
95+<label>
96+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
97+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
98+</div>
94 <dl class="keys">99 <dl class="keys">
95 <div>100 <div>
96 <dt>101 <dt>
@@ -209,18 +214,24 @@
209 <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()">214 <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()">
210 <div class="panel-head">215 <div class="panel-head">
211 <label for="cmd-q" class="sr">go to</label>216 <label for="cmd-q" class="sr">go to</label>
212-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">217+<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)">
213 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">218 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
214 <kbd>esc</kbd>219 <kbd>esc</kbd>
215 </button>220 </button>
216 </div>221 </div>
217-<ul id="cmd-list" class="cmd-list" aria-label="Targets">222+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
218-<li data-k="go home" data-cursor>223+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
219 <a href="/" tabindex="-1">224 <a href="/" tabindex="-1">
220 <span class="lbl">home</span>225 <span class="lbl">home</span>
221 <span class="k">go</span>226 <span class="k">go</span>
222 </a>227 </a>
223 </li>228 </li>
229+<li id="cmd-o1" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
230+<a href="#" data-fire="keys" tabindex="-1">
231+<span class="lbl">turn single-key shortcuts off</span>
232+<span class="k">action</span>
233+</a>
234+</li>
224 </ul>235 </ul>
225 <p class="panel-foot">236 <p class="panel-foot">
226 <span>237 <span>
@@ -259,7 +270,8 @@
259 k-permalink from:body -> ermite.permalink();270 k-permalink from:body -> ermite.permalink();
260 k-merge from:body -> window.ermiteMR?.openMerge();271 k-merge from:body -> window.ermiteMR?.openMerge();
261 k-read from:body -> window.ermiteMR?.markRead();272 k-read from:body -> window.ermiteMR?.markRead();
262- k-gf from:body -> window.ermiteMR?.focusFiles()">273+ k-gf from:body -> window.ermiteMR?.focusFiles();
274+ k-keys from:body -> ermite.toggleKeys()">
263 </div>275 </div>
264 </body>276 </body>
265 </html>277 </html>
M internal/app/testdata/web/not-found.html +16 -4
@@ -91,6 +91,11 @@
91 <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" hx-on:click="closest('dialog').close()">
92 <kbd>esc</kbd> close</button>92 <kbd>esc</kbd> close</button>
93 </div>93 </div>
94+<div class="keys-pref">
95+<label>
96+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
97+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
98+</div>
94 <dl class="keys">99 <dl class="keys">
95 <div>100 <div>
96 <dt>101 <dt>
@@ -209,18 +214,24 @@
209 <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()">214 <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()">
210 <div class="panel-head">215 <div class="panel-head">
211 <label for="cmd-q" class="sr">go to</label>216 <label for="cmd-q" class="sr">go to</label>
212-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">217+<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)">
213 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">218 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
214 <kbd>esc</kbd>219 <kbd>esc</kbd>
215 </button>220 </button>
216 </div>221 </div>
217-<ul id="cmd-list" class="cmd-list" aria-label="Targets">222+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
218-<li data-k="go home" data-cursor>223+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
219 <a href="/" tabindex="-1">224 <a href="/" tabindex="-1">
220 <span class="lbl">home</span>225 <span class="lbl">home</span>
221 <span class="k">go</span>226 <span class="k">go</span>
222 </a>227 </a>
223 </li>228 </li>
229+<li id="cmd-o1" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
230+<a href="#" data-fire="keys" tabindex="-1">
231+<span class="lbl">turn single-key shortcuts off</span>
232+<span class="k">action</span>
233+</a>
234+</li>
224 </ul>235 </ul>
225 <p class="panel-foot">236 <p class="panel-foot">
226 <span>237 <span>
@@ -259,7 +270,8 @@
259 k-permalink from:body -> ermite.permalink();270 k-permalink from:body -> ermite.permalink();
260 k-merge from:body -> window.ermiteMR?.openMerge();271 k-merge from:body -> window.ermiteMR?.openMerge();
261 k-read from:body -> window.ermiteMR?.markRead();272 k-read from:body -> window.ermiteMR?.markRead();
262- k-gf from:body -> window.ermiteMR?.focusFiles()">273+ k-gf from:body -> window.ermiteMR?.focusFiles();
274+ k-keys from:body -> ermite.toggleKeys()">
263 </div>275 </div>
264 </body>276 </body>
265 </html>277 </html>
M internal/app/testdata/web/refs.html +27 -15
@@ -221,6 +221,11 @@
221 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">221 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
222 <kbd>esc</kbd> close</button>222 <kbd>esc</kbd> close</button>
223 </div>223 </div>
224+<div class="keys-pref">
225+<label>
226+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
227+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
228+</div>
224 <dl class="keys">229 <dl class="keys">
225 <div>230 <div>
226 <dt>231 <dt>
@@ -378,84 +383,90 @@
378 <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()">383 <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()">
379 <div class="panel-head">384 <div class="panel-head">
380 <label for="cmd-q" class="sr">go to</label>385 <label for="cmd-q" class="sr">go to</label>
381-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">386+<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)">
382 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">387 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
383 <kbd>esc</kbd>388 <kbd>esc</kbd>
384 </button>389 </button>
385 </div>390 </div>
386-<ul id="cmd-list" class="cmd-list" aria-label="Targets">391+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
387-<li data-k="go home" data-cursor>392+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
388 <a href="/" tabindex="-1">393 <a href="/" tabindex="-1">
389 <span class="lbl">home</span>394 <span class="lbl">home</span>
390 <span class="k">go</span>395 <span class="k">go</span>
391 </a>396 </a>
392 </li>397 </li>
393-<li data-k="section summary">398+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
394 <a href="/demo" tabindex="-1">399 <a href="/demo" tabindex="-1">
395 <span class="lbl">summary</span>400 <span class="lbl">summary</span>
396 <span class="k">section</span>401 <span class="k">section</span>
397 </a>402 </a>
398 </li>403 </li>
399-<li data-k="section files">404+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
400 <a href="/demo/tree/main" tabindex="-1">405 <a href="/demo/tree/main" tabindex="-1">
401 <span class="lbl">files</span>406 <span class="lbl">files</span>
402 <span class="k">section</span>407 <span class="k">section</span>
403 </a>408 </a>
404 </li>409 </li>
405-<li data-k="section commits">410+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
406 <a href="/demo/commits/main" tabindex="-1">411 <a href="/demo/commits/main" tabindex="-1">
407 <span class="lbl">commits</span>412 <span class="lbl">commits</span>
408 <span class="k">section</span>413 <span class="k">section</span>
409 </a>414 </a>
410 </li>415 </li>
411-<li data-k="section branches &amp; tags">416+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
412 <a href="/demo/refs" tabindex="-1">417 <a href="/demo/refs" tabindex="-1">
413 <span class="lbl">branches &amp; tags</span>418 <span class="lbl">branches &amp; tags</span>
414 <span class="k">section</span>419 <span class="k">section</span>
415 </a>420 </a>
416 </li>421 </li>
417-<li data-k="section merge requests">422+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
418 <a href="/demo/merge-requests" tabindex="-1">423 <a href="/demo/merge-requests" tabindex="-1">
419 <span class="lbl">merge requests</span>424 <span class="lbl">merge requests</span>
420 <span class="k">section</span>425 <span class="k">section</span>
421 </a>426 </a>
422 </li>427 </li>
423-<li data-k="section actions">428+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
424 <a href="/demo/actions" tabindex="-1">429 <a href="/demo/actions" tabindex="-1">
425 <span class="lbl">actions</span>430 <span class="lbl">actions</span>
426 <span class="k">section</span>431 <span class="k">section</span>
427 </a>432 </a>
428 </li>433 </li>
429-<li data-k="branch feature/x">434+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
430 <a href="/demo/tree/feature/x" tabindex="-1">435 <a href="/demo/tree/feature/x" tabindex="-1">
431 <span class="lbl">feature/x</span>436 <span class="lbl">feature/x</span>
432 <span class="k">branch</span>437 <span class="k">branch</span>
433 </a>438 </a>
434 </li>439 </li>
435-<li data-k="branch main">440+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
436 <a href="/demo/tree/main" tabindex="-1">441 <a href="/demo/tree/main" tabindex="-1">
437 <span class="lbl">main</span>442 <span class="lbl">main</span>
438 <span class="k">branch</span>443 <span class="k">branch</span>
439 </a>444 </a>
440 </li>445 </li>
441-<li data-k="tag v0.1.0">446+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0">
442 <a href="/demo/tree/v0.1.0" tabindex="-1">447 <a href="/demo/tree/v0.1.0" tabindex="-1">
443 <span class="lbl">v0.1.0</span>448 <span class="lbl">v0.1.0</span>
444 <span class="k">tag</span>449 <span class="k">tag</span>
445 </a>450 </a>
446 </li>451 </li>
447-<li data-k="tag v0.2.0">452+<li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0">
448 <a href="/demo/tree/v0.2.0" tabindex="-1">453 <a href="/demo/tree/v0.2.0" tabindex="-1">
449 <span class="lbl">v0.2.0</span>454 <span class="lbl">v0.2.0</span>
450 <span class="k">tag</span>455 <span class="k">tag</span>
451 </a>456 </a>
452 </li>457 </li>
453-<li data-k="repo quiet">458+<li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet">
454 <a href="/quiet" tabindex="-1">459 <a href="/quiet" tabindex="-1">
455 <span class="lbl">quiet</span>460 <span class="lbl">quiet</span>
456 <span class="k">repo</span>461 <span class="k">repo</span>
457 </a>462 </a>
458 </li>463 </li>
464+<li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
465+<a href="#" data-fire="keys" tabindex="-1">
466+<span class="lbl">turn single-key shortcuts off</span>
467+<span class="k">action</span>
468+</a>
469+</li>
459 </ul>470 </ul>
460 <p class="panel-foot">471 <p class="panel-foot">
461 <span>472 <span>
@@ -494,7 +505,8 @@
494 k-permalink from:body -> ermite.permalink();505 k-permalink from:body -> ermite.permalink();
495 k-merge from:body -> window.ermiteMR?.openMerge();506 k-merge from:body -> window.ermiteMR?.openMerge();
496 k-read from:body -> window.ermiteMR?.markRead();507 k-read from:body -> window.ermiteMR?.markRead();
497- k-gf from:body -> window.ermiteMR?.focusFiles()">508+ k-gf from:body -> window.ermiteMR?.focusFiles();
509+ k-keys from:body -> ermite.toggleKeys()">
498 </div>510 </div>
499 </body>511 </body>
500 </html>512 </html>
M internal/app/testdata/web/summary.html +27 -15
@@ -245,6 +245,11 @@
245 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">245 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
246 <kbd>esc</kbd> close</button>246 <kbd>esc</kbd> close</button>
247 </div>247 </div>
248+<div class="keys-pref">
249+<label>
250+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
251+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
252+</div>
248 <dl class="keys">253 <dl class="keys">
249 <div>254 <div>
250 <dt>255 <dt>
@@ -402,84 +407,90 @@
402 <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()">407 <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()">
403 <div class="panel-head">408 <div class="panel-head">
404 <label for="cmd-q" class="sr">go to</label>409 <label for="cmd-q" class="sr">go to</label>
405-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">410+<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)">
406 <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" hx-on:click="closest('dialog').close()">
407 <kbd>esc</kbd>412 <kbd>esc</kbd>
408 </button>413 </button>
409 </div>414 </div>
410-<ul id="cmd-list" class="cmd-list" aria-label="Targets">415+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
411-<li data-k="go home" data-cursor>416+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
412 <a href="/" tabindex="-1">417 <a href="/" tabindex="-1">
413 <span class="lbl">home</span>418 <span class="lbl">home</span>
414 <span class="k">go</span>419 <span class="k">go</span>
415 </a>420 </a>
416 </li>421 </li>
417-<li data-k="section summary">422+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
418 <a href="/demo" tabindex="-1">423 <a href="/demo" tabindex="-1">
419 <span class="lbl">summary</span>424 <span class="lbl">summary</span>
420 <span class="k">section</span>425 <span class="k">section</span>
421 </a>426 </a>
422 </li>427 </li>
423-<li data-k="section files">428+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
424 <a href="/demo/tree/main" tabindex="-1">429 <a href="/demo/tree/main" tabindex="-1">
425 <span class="lbl">files</span>430 <span class="lbl">files</span>
426 <span class="k">section</span>431 <span class="k">section</span>
427 </a>432 </a>
428 </li>433 </li>
429-<li data-k="section commits">434+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
430 <a href="/demo/commits/main" tabindex="-1">435 <a href="/demo/commits/main" tabindex="-1">
431 <span class="lbl">commits</span>436 <span class="lbl">commits</span>
432 <span class="k">section</span>437 <span class="k">section</span>
433 </a>438 </a>
434 </li>439 </li>
435-<li data-k="section branches &amp; tags">440+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
436 <a href="/demo/refs" tabindex="-1">441 <a href="/demo/refs" tabindex="-1">
437 <span class="lbl">branches &amp; tags</span>442 <span class="lbl">branches &amp; tags</span>
438 <span class="k">section</span>443 <span class="k">section</span>
439 </a>444 </a>
440 </li>445 </li>
441-<li data-k="section merge requests">446+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
442 <a href="/demo/merge-requests" tabindex="-1">447 <a href="/demo/merge-requests" tabindex="-1">
443 <span class="lbl">merge requests</span>448 <span class="lbl">merge requests</span>
444 <span class="k">section</span>449 <span class="k">section</span>
445 </a>450 </a>
446 </li>451 </li>
447-<li data-k="section actions">452+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
448 <a href="/demo/actions" tabindex="-1">453 <a href="/demo/actions" tabindex="-1">
449 <span class="lbl">actions</span>454 <span class="lbl">actions</span>
450 <span class="k">section</span>455 <span class="k">section</span>
451 </a>456 </a>
452 </li>457 </li>
453-<li data-k="branch feature/x">458+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
454 <a href="/demo/tree/feature/x" tabindex="-1">459 <a href="/demo/tree/feature/x" tabindex="-1">
455 <span class="lbl">feature/x</span>460 <span class="lbl">feature/x</span>
456 <span class="k">branch</span>461 <span class="k">branch</span>
457 </a>462 </a>
458 </li>463 </li>
459-<li data-k="branch main">464+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
460 <a href="/demo/tree/main" tabindex="-1">465 <a href="/demo/tree/main" tabindex="-1">
461 <span class="lbl">main</span>466 <span class="lbl">main</span>
462 <span class="k">branch</span>467 <span class="k">branch</span>
463 </a>468 </a>
464 </li>469 </li>
465-<li data-k="tag v0.1.0">470+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0">
466 <a href="/demo/tree/v0.1.0" tabindex="-1">471 <a href="/demo/tree/v0.1.0" tabindex="-1">
467 <span class="lbl">v0.1.0</span>472 <span class="lbl">v0.1.0</span>
468 <span class="k">tag</span>473 <span class="k">tag</span>
469 </a>474 </a>
470 </li>475 </li>
471-<li data-k="tag v0.2.0">476+<li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0">
472 <a href="/demo/tree/v0.2.0" tabindex="-1">477 <a href="/demo/tree/v0.2.0" tabindex="-1">
473 <span class="lbl">v0.2.0</span>478 <span class="lbl">v0.2.0</span>
474 <span class="k">tag</span>479 <span class="k">tag</span>
475 </a>480 </a>
476 </li>481 </li>
477-<li data-k="repo quiet">482+<li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet">
478 <a href="/quiet" tabindex="-1">483 <a href="/quiet" tabindex="-1">
479 <span class="lbl">quiet</span>484 <span class="lbl">quiet</span>
480 <span class="k">repo</span>485 <span class="k">repo</span>
481 </a>486 </a>
482 </li>487 </li>
488+<li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
489+<a href="#" data-fire="keys" tabindex="-1">
490+<span class="lbl">turn single-key shortcuts off</span>
491+<span class="k">action</span>
492+</a>
493+</li>
483 </ul>494 </ul>
484 <p class="panel-foot">495 <p class="panel-foot">
485 <span>496 <span>
@@ -518,7 +529,8 @@
518 k-permalink from:body -> ermite.permalink();529 k-permalink from:body -> ermite.permalink();
519 k-merge from:body -> window.ermiteMR?.openMerge();530 k-merge from:body -> window.ermiteMR?.openMerge();
520 k-read from:body -> window.ermiteMR?.markRead();531 k-read from:body -> window.ermiteMR?.markRead();
521- k-gf from:body -> window.ermiteMR?.focusFiles()">532+ k-gf from:body -> window.ermiteMR?.focusFiles();
533+ k-keys from:body -> ermite.toggleKeys()">
522 </div>534 </div>
523 </body>535 </body>
524 </html>536 </html>
M internal/app/testdata/web/tree-nested.html +25 -13
@@ -168,6 +168,11 @@
168 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">168 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
169 <kbd>esc</kbd> close</button>169 <kbd>esc</kbd> close</button>
170 </div>170 </div>
171+<div class="keys-pref">
172+<label>
173+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
174+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
175+</div>
171 <dl class="keys">176 <dl class="keys">
172 <div>177 <div>
173 <dt>178 <dt>
@@ -321,72 +326,78 @@
321 <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()">326 <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()">
322 <div class="panel-head">327 <div class="panel-head">
323 <label for="cmd-q" class="sr">go to</label>328 <label for="cmd-q" class="sr">go to</label>
324-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">329+<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)">
325 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">330 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
326 <kbd>esc</kbd>331 <kbd>esc</kbd>
327 </button>332 </button>
328 </div>333 </div>
329-<ul id="cmd-list" class="cmd-list" aria-label="Targets">334+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
330-<li data-k="go home" data-cursor>335+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
331 <a href="/" tabindex="-1">336 <a href="/" tabindex="-1">
332 <span class="lbl">home</span>337 <span class="lbl">home</span>
333 <span class="k">go</span>338 <span class="k">go</span>
334 </a>339 </a>
335 </li>340 </li>
336-<li data-k="section summary">341+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
337 <a href="/files" tabindex="-1">342 <a href="/files" tabindex="-1">
338 <span class="lbl">summary</span>343 <span class="lbl">summary</span>
339 <span class="k">section</span>344 <span class="k">section</span>
340 </a>345 </a>
341 </li>346 </li>
342-<li data-k="section files">347+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
343 <a href="/files/tree/main" tabindex="-1">348 <a href="/files/tree/main" tabindex="-1">
344 <span class="lbl">files</span>349 <span class="lbl">files</span>
345 <span class="k">section</span>350 <span class="k">section</span>
346 </a>351 </a>
347 </li>352 </li>
348-<li data-k="section commits">353+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
349 <a href="/files/commits/main" tabindex="-1">354 <a href="/files/commits/main" tabindex="-1">
350 <span class="lbl">commits</span>355 <span class="lbl">commits</span>
351 <span class="k">section</span>356 <span class="k">section</span>
352 </a>357 </a>
353 </li>358 </li>
354-<li data-k="section branches &amp; tags">359+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
355 <a href="/files/refs" tabindex="-1">360 <a href="/files/refs" tabindex="-1">
356 <span class="lbl">branches &amp; tags</span>361 <span class="lbl">branches &amp; tags</span>
357 <span class="k">section</span>362 <span class="k">section</span>
358 </a>363 </a>
359 </li>364 </li>
360-<li data-k="section merge requests">365+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
361 <a href="/files/merge-requests" tabindex="-1">366 <a href="/files/merge-requests" tabindex="-1">
362 <span class="lbl">merge requests</span>367 <span class="lbl">merge requests</span>
363 <span class="k">section</span>368 <span class="k">section</span>
364 </a>369 </a>
365 </li>370 </li>
366-<li data-k="section actions">371+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
367 <a href="/files/actions" tabindex="-1">372 <a href="/files/actions" tabindex="-1">
368 <span class="lbl">actions</span>373 <span class="lbl">actions</span>
369 <span class="k">section</span>374 <span class="k">section</span>
370 </a>375 </a>
371 </li>376 </li>
372-<li data-k="branch feature/x">377+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
373 <a href="/files/tree/feature/x" tabindex="-1">378 <a href="/files/tree/feature/x" tabindex="-1">
374 <span class="lbl">feature/x</span>379 <span class="lbl">feature/x</span>
375 <span class="k">branch</span>380 <span class="k">branch</span>
376 </a>381 </a>
377 </li>382 </li>
378-<li data-k="branch main">383+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
379 <a href="/files/tree/main" tabindex="-1">384 <a href="/files/tree/main" tabindex="-1">
380 <span class="lbl">main</span>385 <span class="lbl">main</span>
381 <span class="k">branch</span>386 <span class="k">branch</span>
382 </a>387 </a>
383 </li>388 </li>
384-<li data-k="tag v1.0">389+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
385 <a href="/files/tree/v1.0" tabindex="-1">390 <a href="/files/tree/v1.0" tabindex="-1">
386 <span class="lbl">v1.0</span>391 <span class="lbl">v1.0</span>
387 <span class="k">tag</span>392 <span class="k">tag</span>
388 </a>393 </a>
389 </li>394 </li>
395+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
396+<a href="#" data-fire="keys" tabindex="-1">
397+<span class="lbl">turn single-key shortcuts off</span>
398+<span class="k">action</span>
399+</a>
400+</li>
390 </ul>401 </ul>
391 <p class="panel-foot">402 <p class="panel-foot">
392 <span>403 <span>
@@ -425,7 +436,8 @@
425 k-permalink from:body -> ermite.permalink();436 k-permalink from:body -> ermite.permalink();
426 k-merge from:body -> window.ermiteMR?.openMerge();437 k-merge from:body -> window.ermiteMR?.openMerge();
427 k-read from:body -> window.ermiteMR?.markRead();438 k-read from:body -> window.ermiteMR?.markRead();
428- k-gf from:body -> window.ermiteMR?.focusFiles()">439+ k-gf from:body -> window.ermiteMR?.focusFiles();
440+ k-keys from:body -> ermite.toggleKeys()">
429 </div>441 </div>
430 </body>442 </body>
431 </html>443 </html>
M internal/app/testdata/web/tree-root.html +25 -13
@@ -389,6 +389,11 @@
389 <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" hx-on:click="closest('dialog').close()">
390 <kbd>esc</kbd> close</button>390 <kbd>esc</kbd> close</button>
391 </div>391 </div>
392+<div class="keys-pref">
393+<label>
394+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
395+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
396+</div>
392 <dl class="keys">397 <dl class="keys">
393 <div>398 <div>
394 <dt>399 <dt>
@@ -542,72 +547,78 @@
542 <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()">547 <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()">
543 <div class="panel-head">548 <div class="panel-head">
544 <label for="cmd-q" class="sr">go to</label>549 <label for="cmd-q" class="sr">go to</label>
545-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">550+<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)">
546 <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" hx-on:click="closest('dialog').close()">
547 <kbd>esc</kbd>552 <kbd>esc</kbd>
548 </button>553 </button>
549 </div>554 </div>
550-<ul id="cmd-list" class="cmd-list" aria-label="Targets">555+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
551-<li data-k="go home" data-cursor>556+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
552 <a href="/" tabindex="-1">557 <a href="/" tabindex="-1">
553 <span class="lbl">home</span>558 <span class="lbl">home</span>
554 <span class="k">go</span>559 <span class="k">go</span>
555 </a>560 </a>
556 </li>561 </li>
557-<li data-k="section summary">562+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
558 <a href="/files" tabindex="-1">563 <a href="/files" tabindex="-1">
559 <span class="lbl">summary</span>564 <span class="lbl">summary</span>
560 <span class="k">section</span>565 <span class="k">section</span>
561 </a>566 </a>
562 </li>567 </li>
563-<li data-k="section files">568+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
564 <a href="/files/tree/main" tabindex="-1">569 <a href="/files/tree/main" tabindex="-1">
565 <span class="lbl">files</span>570 <span class="lbl">files</span>
566 <span class="k">section</span>571 <span class="k">section</span>
567 </a>572 </a>
568 </li>573 </li>
569-<li data-k="section commits">574+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
570 <a href="/files/commits/main" tabindex="-1">575 <a href="/files/commits/main" tabindex="-1">
571 <span class="lbl">commits</span>576 <span class="lbl">commits</span>
572 <span class="k">section</span>577 <span class="k">section</span>
573 </a>578 </a>
574 </li>579 </li>
575-<li data-k="section branches &amp; tags">580+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
576 <a href="/files/refs" tabindex="-1">581 <a href="/files/refs" tabindex="-1">
577 <span class="lbl">branches &amp; tags</span>582 <span class="lbl">branches &amp; tags</span>
578 <span class="k">section</span>583 <span class="k">section</span>
579 </a>584 </a>
580 </li>585 </li>
581-<li data-k="section merge requests">586+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
582 <a href="/files/merge-requests" tabindex="-1">587 <a href="/files/merge-requests" tabindex="-1">
583 <span class="lbl">merge requests</span>588 <span class="lbl">merge requests</span>
584 <span class="k">section</span>589 <span class="k">section</span>
585 </a>590 </a>
586 </li>591 </li>
587-<li data-k="section actions">592+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
588 <a href="/files/actions" tabindex="-1">593 <a href="/files/actions" tabindex="-1">
589 <span class="lbl">actions</span>594 <span class="lbl">actions</span>
590 <span class="k">section</span>595 <span class="k">section</span>
591 </a>596 </a>
592 </li>597 </li>
593-<li data-k="branch feature/x">598+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
594 <a href="/files/tree/feature/x" tabindex="-1">599 <a href="/files/tree/feature/x" tabindex="-1">
595 <span class="lbl">feature/x</span>600 <span class="lbl">feature/x</span>
596 <span class="k">branch</span>601 <span class="k">branch</span>
597 </a>602 </a>
598 </li>603 </li>
599-<li data-k="branch main">604+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
600 <a href="/files/tree/main" tabindex="-1">605 <a href="/files/tree/main" tabindex="-1">
601 <span class="lbl">main</span>606 <span class="lbl">main</span>
602 <span class="k">branch</span>607 <span class="k">branch</span>
603 </a>608 </a>
604 </li>609 </li>
605-<li data-k="tag v1.0">610+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
606 <a href="/files/tree/v1.0" tabindex="-1">611 <a href="/files/tree/v1.0" tabindex="-1">
607 <span class="lbl">v1.0</span>612 <span class="lbl">v1.0</span>
608 <span class="k">tag</span>613 <span class="k">tag</span>
609 </a>614 </a>
610 </li>615 </li>
616+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
617+<a href="#" data-fire="keys" tabindex="-1">
618+<span class="lbl">turn single-key shortcuts off</span>
619+<span class="k">action</span>
620+</a>
621+</li>
611 </ul>622 </ul>
612 <p class="panel-foot">623 <p class="panel-foot">
613 <span>624 <span>
@@ -646,7 +657,8 @@
646 k-permalink from:body -> ermite.permalink();657 k-permalink from:body -> ermite.permalink();
647 k-merge from:body -> window.ermiteMR?.openMerge();658 k-merge from:body -> window.ermiteMR?.openMerge();
648 k-read from:body -> window.ermiteMR?.markRead();659 k-read from:body -> window.ermiteMR?.markRead();
649- k-gf from:body -> window.ermiteMR?.focusFiles()">660+ k-gf from:body -> window.ermiteMR?.focusFiles();
661+ k-keys from:body -> ermite.toggleKeys()">
650 </div>662 </div>
651 </body>663 </body>
652 </html>664 </html>
M internal/app/testdata/web/tree-slash-ref.html +25 -13
@@ -165,6 +165,11 @@
165 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">165 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
166 <kbd>esc</kbd> close</button>166 <kbd>esc</kbd> close</button>
167 </div>167 </div>
168+<div class="keys-pref">
169+<label>
170+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
171+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
172+</div>
168 <dl class="keys">173 <dl class="keys">
169 <div>174 <div>
170 <dt>175 <dt>
@@ -318,72 +323,78 @@
318 <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()">323 <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()">
319 <div class="panel-head">324 <div class="panel-head">
320 <label for="cmd-q" class="sr">go to</label>325 <label for="cmd-q" class="sr">go to</label>
321-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">326+<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)">
322 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">327 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
323 <kbd>esc</kbd>328 <kbd>esc</kbd>
324 </button>329 </button>
325 </div>330 </div>
326-<ul id="cmd-list" class="cmd-list" aria-label="Targets">331+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
327-<li data-k="go home" data-cursor>332+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
328 <a href="/" tabindex="-1">333 <a href="/" tabindex="-1">
329 <span class="lbl">home</span>334 <span class="lbl">home</span>
330 <span class="k">go</span>335 <span class="k">go</span>
331 </a>336 </a>
332 </li>337 </li>
333-<li data-k="section summary">338+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
334 <a href="/files" tabindex="-1">339 <a href="/files" tabindex="-1">
335 <span class="lbl">summary</span>340 <span class="lbl">summary</span>
336 <span class="k">section</span>341 <span class="k">section</span>
337 </a>342 </a>
338 </li>343 </li>
339-<li data-k="section files">344+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
340 <a href="/files/tree/main" tabindex="-1">345 <a href="/files/tree/main" tabindex="-1">
341 <span class="lbl">files</span>346 <span class="lbl">files</span>
342 <span class="k">section</span>347 <span class="k">section</span>
343 </a>348 </a>
344 </li>349 </li>
345-<li data-k="section commits">350+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
346 <a href="/files/commits/main" tabindex="-1">351 <a href="/files/commits/main" tabindex="-1">
347 <span class="lbl">commits</span>352 <span class="lbl">commits</span>
348 <span class="k">section</span>353 <span class="k">section</span>
349 </a>354 </a>
350 </li>355 </li>
351-<li data-k="section branches &amp; tags">356+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
352 <a href="/files/refs" tabindex="-1">357 <a href="/files/refs" tabindex="-1">
353 <span class="lbl">branches &amp; tags</span>358 <span class="lbl">branches &amp; tags</span>
354 <span class="k">section</span>359 <span class="k">section</span>
355 </a>360 </a>
356 </li>361 </li>
357-<li data-k="section merge requests">362+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
358 <a href="/files/merge-requests" tabindex="-1">363 <a href="/files/merge-requests" tabindex="-1">
359 <span class="lbl">merge requests</span>364 <span class="lbl">merge requests</span>
360 <span class="k">section</span>365 <span class="k">section</span>
361 </a>366 </a>
362 </li>367 </li>
363-<li data-k="section actions">368+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
364 <a href="/files/actions" tabindex="-1">369 <a href="/files/actions" tabindex="-1">
365 <span class="lbl">actions</span>370 <span class="lbl">actions</span>
366 <span class="k">section</span>371 <span class="k">section</span>
367 </a>372 </a>
368 </li>373 </li>
369-<li data-k="branch feature/x">374+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
370 <a href="/files/tree/feature/x" tabindex="-1">375 <a href="/files/tree/feature/x" tabindex="-1">
371 <span class="lbl">feature/x</span>376 <span class="lbl">feature/x</span>
372 <span class="k">branch</span>377 <span class="k">branch</span>
373 </a>378 </a>
374 </li>379 </li>
375-<li data-k="branch main">380+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
376 <a href="/files/tree/main" tabindex="-1">381 <a href="/files/tree/main" tabindex="-1">
377 <span class="lbl">main</span>382 <span class="lbl">main</span>
378 <span class="k">branch</span>383 <span class="k">branch</span>
379 </a>384 </a>
380 </li>385 </li>
381-<li data-k="tag v1.0">386+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
382 <a href="/files/tree/v1.0" tabindex="-1">387 <a href="/files/tree/v1.0" tabindex="-1">
383 <span class="lbl">v1.0</span>388 <span class="lbl">v1.0</span>
384 <span class="k">tag</span>389 <span class="k">tag</span>
385 </a>390 </a>
386 </li>391 </li>
392+<li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard">
393+<a href="#" data-fire="keys" tabindex="-1">
394+<span class="lbl">turn single-key shortcuts off</span>
395+<span class="k">action</span>
396+</a>
397+</li>
387 </ul>398 </ul>
388 <p class="panel-foot">399 <p class="panel-foot">
389 <span>400 <span>
@@ -422,7 +433,8 @@
422 k-permalink from:body -> ermite.permalink();433 k-permalink from:body -> ermite.permalink();
423 k-merge from:body -> window.ermiteMR?.openMerge();434 k-merge from:body -> window.ermiteMR?.openMerge();
424 k-read from:body -> window.ermiteMR?.markRead();435 k-read from:body -> window.ermiteMR?.markRead();
425- k-gf from:body -> window.ermiteMR?.focusFiles()">436+ k-gf from:body -> window.ermiteMR?.focusFiles();
437+ k-keys from:body -> ermite.toggleKeys()">
426 </div>438 </div>
427 </body>439 </body>
428 </html>440 </html>
M internal/app/testdata/web/tree-subdir.html +25 -13
@@ -181,6 +181,11 @@
181 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">181 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
182 <kbd>esc</kbd> close</button>182 <kbd>esc</kbd> close</button>
183 </div>183 </div>
184+<div class="keys-pref">
185+<label>
186+<input type="checkbox" id="keys-off" aria-describedby="keys-note" hx-on:change="ermite.setKeysOff(this.checked)"> Turn off single-key shortcuts</label>
187+<p id="keys-note" class="dim">Single-key shortcuts are on. ctrl k and esc work whatever this says.</p>
188+</div>
184 <dl class="keys">189 <dl class="keys">
185 <div>190 <div>
186 <dt>191 <dt>
@@ -334,72 +339,78 @@
334 <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()">339 <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()">
335 <div class="panel-head">340 <div class="panel-head">
336 <label for="cmd-q" class="sr">go to</label>341 <label for="cmd-q" class="sr">go to</label>
337-<input id="cmd-q" type="search" placeholder="Go to a section, branch, tag, file or repository…" autocomplete="off" spellcheck="false" aria-controls="cmd-list" hx-on:input="ermite.cmdFilter(this); ermiteFinder.palette(this)" hx-on:keydown="ermite.cmdKey(event)">342+<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()">343 <button type="button" class="panel-x" aria-label="Close" hx-on:click="closest('dialog').close()">
339 <kbd>esc</kbd>344 <kbd>esc</kbd>
340 </button>345 </button>
341 </div>346 </div>
342-<ul id="cmd-list" class="cmd-list" aria-label="Targets">347+<ul id="cmd-list" class="cmd-list" role="listbox" aria-label="Targets">
343-<li data-k="go home" data-cursor>348+<li id="cmd-o0" role="option" aria-selected="true" data-k="go home" data-cursor>
344 <a href="/" tabindex="-1">349 <a href="/" tabindex="-1">
345 <span class="lbl">home</span>350 <span class="lbl">home</span>
346 <span class="k">go</span>351 <span class="k">go</span>
347 </a>352 </a>
348 </li>353 </li>
349-<li data-k="section summary">354+<li id="cmd-o1" role="option" aria-selected="false" data-k="section summary">
350 <a href="/files" tabindex="-1">355 <a href="/files" tabindex="-1">
351 <span class="lbl">summary</span>356 <span class="lbl">summary</span>
352 <span class="k">section</span>357 <span class="k">section</span>
353 </a>358 </a>
354 </li>359 </li>
355-<li data-k="section files">360+<li id="cmd-o2" role="option" aria-selected="false" data-k="section files">
356 <a href="/files/tree/main" tabindex="-1">361 <a href="/files/tree/main" tabindex="-1">
357 <span class="lbl">files</span>362 <span class="lbl">files</span>
358 <span class="k">section</span>363 <span class="k">section</span>
359 </a>364 </a>
360 </li>365 </li>
361-<li data-k="section commits">366+<li id="cmd-o3" role="option" aria-selected="false" data-k="section commits">
362 <a href="/files/commits/main" tabindex="-1">367 <a href="/files/commits/main" tabindex="-1">
363 <span class="lbl">commits</span>368 <span class="lbl">commits</span>
364 <span class="k">section</span>369 <span class="k">section</span>
365 </a>370 </a>
366 </li>371 </li>
367-<li data-k="section branches &amp; tags">372+<li id="cmd-o4" role="option" aria-selected="false" data-k="section branches &amp; tags">
368 <a href="/files/refs" tabindex="-1">373 <a href="/files/refs" tabindex="-1">
369 <span class="lbl">branches &amp; tags</span>374 <span class="lbl">branches &amp; tags</span>
370 <span class="k">section</span>375 <span class="k">section</span>
371 </a>376 </a>
372 </li>377 </li>
373-<li data-k="section merge requests">378+<li id="cmd-o5" role="option" aria-selected="false" data-k="section merge requests">
374 <a href="/files/merge-requests" tabindex="-1">379 <a href="/files/merge-requests" tabindex="-1">
375 <span class="lbl">merge requests</span>380 <span class="lbl">merge requests</span>
376 <span class="k">section</span>381 <span class="k">section</span>
377 </a>382 </a>
378 </li>383 </li>
379-<li data-k="section actions">384+<li id="cmd-o6" role="option" aria-selected="false" data-k="section actions">
380 <a href="/files/actions" tabindex="-1">385 <a href="/files/actions" tabindex="-1">
381 <span class="lbl">actions</span>386 <span class="lbl">actions</span>
382 <span class="k">section</span>387 <span class="k">section</span>
383 </a>388 </a>
384 </li>389 </li>
385-<li data-k="branch feature/x">390+<li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x">
386 <a href="/files/tree/feature/x" tabindex="-1">391 <a href="/files/tree/feature/x" tabindex="-1">
387 <span class="lbl">feature/x</span>392 <span class="lbl">feature/x</span>
388 <span class="k">branch</span>393 <span class="k">branch</span>
389 </a>394 </a>
390 </li>395 </li>
391-<li data-k="branch main">396+<li id="cmd-o8" role="option" aria-selected="false" data-k="branch main">
392 <a href="/files/tree/main" tabindex="-1">397 <a href="/files/tree/main" tabindex="-1">
393 <span class="lbl">main</span>398 <span class="lbl">main</span>
394 <span class="k">branch</span>399 <span class="k">branch</span>
395 </a>400 </a>
396 </li>401 </li>
397-<li data-k="tag v1.0">402+<li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0">
398 <a href="/files/tree/v1.0" tabindex="-1">403 <a href="/files/tree/v1.0" tabindex="-1">
399 <span class="lbl">v1.0</span>404 <span class="lbl">v1.0</span>
400 <span class="k">tag</span>405 <span class="k">tag</span>
401 </a>406 </a>
402 </li>407 </li>
408+<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">
410+<span class="lbl">turn single-key shortcuts off</span>
411+<span class="k">action</span>
412+</a>
413+</li>
403 </ul>414 </ul>
404 <p class="panel-foot">415 <p class="panel-foot">
405 <span>416 <span>
@@ -438,7 +449,8 @@
438 k-permalink from:body -> ermite.permalink();449 k-permalink from:body -> ermite.permalink();
439 k-merge from:body -> window.ermiteMR?.openMerge();450 k-merge from:body -> window.ermiteMR?.openMerge();
440 k-read from:body -> window.ermiteMR?.markRead();451 k-read from:body -> window.ermiteMR?.markRead();
441- k-gf from:body -> window.ermiteMR?.focusFiles()">452+ k-gf from:body -> window.ermiteMR?.focusFiles();
453+ k-keys from:body -> ermite.toggleKeys()">
442 </div>454 </div>
443 </body>455 </body>
444 </html>456 </html>

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 44

↑↓ moveenter openesc close