Carry over what only the win7 tree had: middle-click close, ARIA, menu fix

The deleted win7 tree held a few behaviours the modern tree never got:

- Context menu: the first ArrowUp in a fresh menu skipped the last item
  (Delete in the row menu, Disconnect in the tab menu) — stepping from -1
  landed one short. The index math moves to core/menu-nav.ts stepIndex(),
  checked by menu-nav.spec.ts (bun test; verified to fail on the old logic).
  Items get ids and the menu exposes aria-activedescendant, so screen
  readers follow the arrow keys while focus stays on the menu.
- Middle-click closes a tab, as in browsers. auxclick also fires for the
  right button, so only button 1 counts; closeWorkspace already refuses the
  last tab. A middle press is swallowed so WebView2 doesn't start autoscroll
  over the scrollable strip. The hover ✕ stays.
- ARIA: the tab strip is a tablist of role=tab buttons with aria-selected
  (the in-tab ✕ is taken out of the a11y tree and tab order — Ctrl/Cmd+W and
  the tab menu remain the keyboard paths); transfer bars are progressbars
  with value and a name.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:22:30 +02:00
co-authored by Claude
parent 2bac7678ca
commit f7f02083fe
5 changed files with 80 additions and 14 deletions
+27
View File
@@ -0,0 +1,27 @@
import { expect, test } from 'bun:test';
import { stepIndex } from './menu-nav';
// item, separator, item, disabled, item — the shape of a real row menu
const menu = [true, false, true, false, true];
test('a fresh menu: down lands on the first entry, up on the last', () => {
expect(stepIndex(menu, -1, 1)).toBe(0);
expect(stepIndex(menu, -1, -1)).toBe(4);
});
test('separators and disabled entries are skipped', () => {
expect(stepIndex(menu, 0, 1)).toBe(2);
expect(stepIndex(menu, 2, 1)).toBe(4);
expect(stepIndex(menu, 4, -1)).toBe(2);
});
test('wraps both ways', () => {
expect(stepIndex(menu, 4, 1)).toBe(0);
expect(stepIndex(menu, 0, -1)).toBe(4);
});
test('nothing selectable leaves the highlight alone', () => {
expect(stepIndex([false, false], -1, 1)).toBe(-1);
expect(stepIndex([false, false], -1, -1)).toBe(-1);
expect(stepIndex([], -1, -1)).toBe(-1);
});
+15
View File
@@ -0,0 +1,15 @@
/**
* Next highlighted menu index: skips unselectable entries and wraps. From -1
* (nothing highlighted yet), down lands on the first selectable entry and up
* on the LAST — stepping up from -1 directly would land one short of it.
* Returns `from` unchanged when nothing is selectable.
*/
export function stepIndex(selectable: boolean[], from: number, delta: 1 | -1): number {
const count = selectable.length;
let index = from < 0 && delta < 0 ? 0 : from;
for (let hops = 0; hops < count; hops++) {
index = (index + delta + count) % count;
if (selectable[index]) return index;
}
return from;
}