@cplieger/web-terminal-ui 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "GPL-3.0-or-later",
@@ -908,6 +908,48 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
908
908
  }
909
909
  switchTo(tab.id);
910
910
  });
911
+ // Keyboard interaction for the ARIA tabs pattern (WCAG 2.1.1): arrows
912
+ // move selection (wrapping), Home/End jump to the boundaries, Delete
913
+ // closes the focused tab. Pairs with the roving tabindex the kernel's
914
+ // registerTab manages (selected tab is tabIndex 0, others -1).
915
+ el.addEventListener("keydown", (e) => {
916
+ const current = tabList.indexOf(tab);
917
+ if (current < 0) {
918
+ return;
919
+ }
920
+
921
+ if (e.key === "Delete") {
922
+ e.preventDefault();
923
+ void close_(tab.id);
924
+ return;
925
+ }
926
+
927
+ let targetIndex: number;
928
+ switch (e.key) {
929
+ case "ArrowLeft":
930
+ targetIndex = (current - 1 + tabList.length) % tabList.length;
931
+ break;
932
+ case "ArrowRight":
933
+ targetIndex = (current + 1) % tabList.length;
934
+ break;
935
+ case "Home":
936
+ targetIndex = 0;
937
+ break;
938
+ case "End":
939
+ targetIndex = tabList.length - 1;
940
+ break;
941
+ default:
942
+ return;
943
+ }
944
+
945
+ e.preventDefault();
946
+ const target = tabList[targetIndex];
947
+ if (!target) {
948
+ return;
949
+ }
950
+ switchTo(target.id);
951
+ target.el.focus();
952
+ });
911
953
  close.addEventListener("click", (e) => {
912
954
  e.stopPropagation();
913
955
  void close_(tab.id);
@@ -1169,6 +1211,29 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1169
1211
  }
1170
1212
  }
1171
1213
 
1214
+ // moveTab shifts a tab one slot left or right in tabList and re-inserts
1215
+ // every tab element in list order (before the "+") so the DOM matches.
1216
+ // It is the single-pointer / non-drag alternative to the drag-and-drop
1217
+ // reorder (WCAG 2.5.7), surfaced as Move left / Move right in the tab
1218
+ // context menu.
1219
+ function moveTab(id: string, delta: -1 | 1): void {
1220
+ const from = tabList.findIndex((t) => t.id === id);
1221
+ const to = from + delta;
1222
+ if (from < 0 || to < 0 || to >= tabList.length) {
1223
+ return;
1224
+ }
1225
+ const [tab] = tabList.splice(from, 1);
1226
+ if (!tab) {
1227
+ return;
1228
+ }
1229
+ tabList.splice(to, 0, tab);
1230
+ for (const item of tabList) {
1231
+ scroller.insertBefore(item.el, newBtn);
1232
+ }
1233
+ syncChrome();
1234
+ ctx.announce(`Moved ${tab.display} to position ${String(to + 1)}`);
1235
+ }
1236
+
1172
1237
  // adoptSession adds a tab for a session that exists server-side but has no
1173
1238
  // local tab yet, e.g. one created in another browser (the server pushes it
1174
1239
  // over the status SSE, and the poll fallback lists it). This keeps every
@@ -1410,6 +1475,12 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
1410
1475
  return;
1411
1476
  }
1412
1477
  const n = tabList.length;
1478
+ tabMenuItem("Move left", idx <= 0, () => {
1479
+ moveTab(id, -1);
1480
+ });
1481
+ tabMenuItem("Move right", idx >= n - 1, () => {
1482
+ moveTab(id, 1);
1483
+ });
1413
1484
  tabMenuItem("Close", false, () => {
1414
1485
  void close_(id);
1415
1486
  });
@@ -97,10 +97,14 @@ export function createTablist(panel: HTMLElement): TablistController {
97
97
  tab.setAttribute("role", "tab");
98
98
  tab.setAttribute("aria-controls", panelId);
99
99
  tab.setAttribute("aria-selected", "false");
100
+ // Roving tabindex (WAI-ARIA APG Tabs pattern): only the selected tab is in
101
+ // the Tab sequence; the others are focusable programmatically (arrow keys).
102
+ tab.tabIndex = -1;
100
103
 
101
104
  return {
102
105
  setSelected(selected: boolean): void {
103
106
  tab.setAttribute("aria-selected", selected ? "true" : "false");
107
+ tab.tabIndex = selected ? 0 : -1;
104
108
  if (selected) {
105
109
  // The panel is labelled by whichever tab is currently selected.
106
110
  panel.setAttribute("aria-labelledby", tabId);
@@ -113,6 +117,7 @@ export function createTablist(panel: HTMLElement): TablistController {
113
117
  tab.removeAttribute("role");
114
118
  tab.removeAttribute("aria-controls");
115
119
  tab.removeAttribute("aria-selected");
120
+ tab.removeAttribute("tabindex");
116
121
  if (panel.getAttribute("aria-labelledby") === tabId) {
117
122
  panel.removeAttribute("aria-labelledby");
118
123
  }