@swifttui/web 0.0.14 → 0.0.16
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/README.md +24 -10
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +2 -0
- package/dist/src/AccessibilityTree.js +156 -0
- package/dist/src/AccessibilityTree.js.map +1 -0
- package/dist/src/BoxDrawingRenderer.js +1106 -0
- package/dist/src/BoxDrawingRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +41 -0
- package/dist/src/WebHostApp.js +135 -0
- package/dist/src/WebHostApp.js.map +1 -0
- package/dist/src/WebHostSceneManifest.d.ts +18 -0
- package/dist/src/WebHostSceneManifest.js +70 -0
- package/dist/src/WebHostSceneManifest.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +112 -0
- package/dist/src/WebHostSceneRuntime.js +651 -0
- package/dist/src/WebHostSceneRuntime.js.map +1 -0
- package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
- package/dist/src/WebHostSurfaceTransport.js +252 -0
- package/dist/src/WebHostSurfaceTransport.js.map +1 -0
- package/dist/src/WebHostTerminalStyle.d.ts +92 -0
- package/dist/src/WebHostTerminalStyle.js +277 -0
- package/dist/src/WebHostTerminalStyle.js.map +1 -0
- package/dist/src/WebHostTestFixtures.d.ts +5 -0
- package/dist/src/WebHostTestFixtures.js +9 -0
- package/dist/src/WebHostTestFixtures.js.map +1 -0
- package/dist/src/WebSocketSceneBridge.d.ts +53 -0
- package/dist/src/WebSocketSceneBridge.js +124 -0
- package/dist/src/WebSocketSceneBridge.js.map +1 -0
- package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
- package/dist/src/wasi/BrowserWASIBridge.js +97 -0
- package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
- package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
- package/dist/src/wasi/SharedInputQueue.js +102 -0
- package/dist/src/wasi/SharedInputQueue.js.map +1 -0
- package/dist/src/wasi/StdIOPipe.d.ts +15 -0
- package/dist/src/wasi/StdIOPipe.js +56 -0
- package/dist/src/wasi/StdIOPipe.js.map +1 -0
- package/dist/src/wasi/WasiPollScheduler.js +114 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
- package/dist/src/wasi/WasmSceneRuntime.js +119 -0
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
- package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
- package/dist/src/wasi/WasmSceneWorker.js +109 -0
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/dist/wasi-worker.d.ts +2 -0
- package/dist/wasi-worker.js +2 -0
- package/dist/wasi.d.ts +6 -0
- package/dist/wasi.js +6 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +2 -0
- package/package.json +49 -18
- package/AGENTS.md +0 -52
- package/cli.ts +0 -168
- package/index.html +0 -50
- package/index.ts +0 -8
- package/manifest.ts +0 -1
- package/src/AccessibilityTree.ts +0 -262
- package/src/BoxDrawingRenderer.ts +0 -585
- package/src/PublicEntrypointBoundary.test.ts +0 -20
- package/src/WebHostApp.test.ts +0 -222
- package/src/WebHostApp.ts +0 -269
- package/src/WebHostSceneManifest.test.ts +0 -38
- package/src/WebHostSceneManifest.ts +0 -156
- package/src/WebHostSceneRuntime.test.ts +0 -1982
- package/src/WebHostSceneRuntime.ts +0 -1142
- package/src/WebHostSurfaceTransport.test.ts +0 -362
- package/src/WebHostSurfaceTransport.ts +0 -691
- package/src/WebHostTerminalStyle.test.ts +0 -123
- package/src/WebHostTerminalStyle.ts +0 -471
- package/src/WebHostTestFixtures.ts +0 -10
- package/src/WebSocketSceneBridge.test.ts +0 -198
- package/src/WebSocketSceneBridge.ts +0 -233
- package/src/browser.ts +0 -59
- package/src/wasi/BrowserWASIBridge.test.ts +0 -168
- package/src/wasi/BrowserWASIBridge.ts +0 -167
- package/src/wasi/SharedInputQueue.test.ts +0 -146
- package/src/wasi/SharedInputQueue.ts +0 -199
- package/src/wasi/StdIOPipe.ts +0 -72
- package/src/wasi/WasiPollScheduler.test.ts +0 -176
- package/src/wasi/WasiPollScheduler.ts +0 -305
- package/src/wasi/WasmSceneRuntime.ts +0 -205
- package/src/wasi/WasmSceneWorker.ts +0 -182
- package/testing.ts +0 -1
- package/tsconfig.json +0 -29
- package/wasi-worker.ts +0 -1
- package/wasi.ts +0 -4
- package/websocket.ts +0 -1
package/README.md
CHANGED
|
@@ -6,9 +6,19 @@ This package owns browser-safe runtime APIs: scene manifest loading, canvas
|
|
|
6
6
|
rendering, ARIA mounting, WebSocket scene bridges, and WASI scene bridges. Build
|
|
7
7
|
tooling lives in the sibling [`@swifttui/build`](../build) workspace package.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Published to npm as an ESM package with bundled TypeScript declarations — no
|
|
12
|
+
TypeScript toolchain required to consume it:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @swifttui/web
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package ships compiled `dist/` JavaScript (`.js` + `.d.ts`); consuming it
|
|
19
|
+
does **not** require Bun or a TypeScript build step. Subpath entrypoints
|
|
20
|
+
(`./wasi`, `./wasi-worker`, `./manifest`, `./websocket`, `./testing`) and the
|
|
21
|
+
`./style.css` asset are declared in `package.json` `exports`.
|
|
12
22
|
|
|
13
23
|
## Toolchains
|
|
14
24
|
|
|
@@ -86,21 +96,25 @@ startWasmSceneWorker();
|
|
|
86
96
|
## Scripts
|
|
87
97
|
|
|
88
98
|
- `bun test`
|
|
99
|
+
- `bun run build` — compile the publishable package to `dist/` with tsdown
|
|
100
|
+
(ESM `.js` + `.d.ts`). Run automatically on publish via `prepublishOnly`.
|
|
89
101
|
- `bun run build:manifest -- --app <AppExecutable>`
|
|
90
102
|
- `bun run build:wasm -- --app <AppExecutable>`
|
|
91
103
|
- `bun run build:web`
|
|
92
|
-
- `bun run build -- --app <AppExecutable>`
|
|
104
|
+
- `bun run build:app -- --app <AppExecutable>`
|
|
93
105
|
- `bun run dev`
|
|
94
106
|
|
|
95
|
-
`build:manifest`, `build:wasm`, and
|
|
96
|
-
`@swifttui/build
|
|
97
|
-
|
|
98
|
-
debug-oriented wasm builds.
|
|
107
|
+
`build` produces the published library. `build:manifest`, `build:wasm`, and
|
|
108
|
+
`build:app` delegate manifest/WASI packaging to `@swifttui/build`; `build:wasm`
|
|
109
|
+
and `build:app` default to `--configuration release` (pass
|
|
110
|
+
`--configuration debug` for local debug-oriented wasm builds). The demo/app
|
|
111
|
+
pipeline writes its artifacts to `dist-demo/` so they stay separate from the
|
|
112
|
+
published `dist/` library output.
|
|
99
113
|
|
|
100
|
-
The build flow is intentionally small:
|
|
114
|
+
The demo/app build flow is intentionally small:
|
|
101
115
|
|
|
102
116
|
1. `build:manifest` captures `TUIGUI_MODE=manifest` output from the Swift app by invoking `swiftly run swift`.
|
|
103
|
-
2. `build:wasm` copies the app's wasm artifact into `dist/assets/app.wasm`,
|
|
117
|
+
2. `build:wasm` copies the app's wasm artifact into `dist-demo/assets/app.wasm`,
|
|
104
118
|
validates it with the browser `WebAssembly` API, then keeps the stripped
|
|
105
119
|
artifact only if stripping still produces browser-parseable wasm.
|
|
106
120
|
3. `build:web` bundles `index.html` and the browser entrypoint with Bun.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, WebHostANSIColors, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
|
|
2
|
+
import { WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
3
|
+
import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
4
|
+
import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WheelMode } from "./src/WebHostSceneRuntime.js";
|
|
5
|
+
import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
6
|
+
import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, createWebHostApp } from "./src/WebHostApp.js";
|
|
7
|
+
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
8
|
+
import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
|
|
9
|
+
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, StdIOPipe, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
2
|
+
import { applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
|
|
3
|
+
import { WebHostOutputDecoder, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
4
|
+
import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
|
|
5
|
+
import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
6
|
+
import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
7
|
+
import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
|
|
8
|
+
import { createWebHostApp } from "./src/WebHostApp.js";
|
|
9
|
+
export { BrowserWASIBridge, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
2
|
+
export { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON };
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
2
|
+
export { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//#region src/AccessibilityTree.ts
|
|
2
|
+
var AccessibilityTreeMounter = class {
|
|
3
|
+
element;
|
|
4
|
+
announcerElement;
|
|
5
|
+
nodesById = /* @__PURE__ */ new Map();
|
|
6
|
+
previousLabelsById = /* @__PURE__ */ new Map();
|
|
7
|
+
hasLiveRegionBaseline = false;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.element = document.createElement("div");
|
|
10
|
+
this.element.className = "webhost-scene__accessibility-tree";
|
|
11
|
+
applyScreenReaderOnlyStyle(this.element);
|
|
12
|
+
this.announcerElement = document.createElement("div");
|
|
13
|
+
this.announcerElement.className = "webhost-scene__accessibility-announcer";
|
|
14
|
+
this.announcerElement.setAttribute("aria-atomic", "true");
|
|
15
|
+
applyScreenReaderOnlyStyle(this.announcerElement);
|
|
16
|
+
}
|
|
17
|
+
present(nodes, metrics, announcements = [], options = {}) {
|
|
18
|
+
this.element.replaceChildren();
|
|
19
|
+
this.nodesById.clear();
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
const element = this.elementForNode(node, metrics);
|
|
22
|
+
this.nodesById.set(node.id, element);
|
|
23
|
+
}
|
|
24
|
+
for (const node of nodes) {
|
|
25
|
+
const element = this.nodesById.get(node.id);
|
|
26
|
+
if (!element) continue;
|
|
27
|
+
((node.parentId ? this.nodesById.get(node.parentId) : void 0) ?? this.element).appendChild(element);
|
|
28
|
+
}
|
|
29
|
+
this.announceLiveRegionChanges(nodes, announcements);
|
|
30
|
+
const focused = nodes.find((node) => node.isFocused);
|
|
31
|
+
if ((options.synchronizeFocus ?? true) && focused) this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });
|
|
32
|
+
}
|
|
33
|
+
elementForNode(node, metrics) {
|
|
34
|
+
const element = document.createElement("div");
|
|
35
|
+
element.id = `swifttui-a11y-${stableDOMId(node.id)}`;
|
|
36
|
+
element.dataset.accessibilityId = node.id;
|
|
37
|
+
element.tabIndex = node.isFocused ? 0 : -1;
|
|
38
|
+
const role = roleMapping(node.role);
|
|
39
|
+
if (role.role) element.setAttribute("role", role.role);
|
|
40
|
+
if (role.level !== void 0) element.setAttribute("aria-level", String(role.level));
|
|
41
|
+
if (node.label) element.setAttribute("aria-label", node.label);
|
|
42
|
+
if (node.hint) element.setAttribute("aria-description", node.hint);
|
|
43
|
+
if (node.liveRegion) element.setAttribute("aria-live", node.liveRegion);
|
|
44
|
+
if (node.isFocused) element.dataset.focused = "true";
|
|
45
|
+
const [x, y, width, height] = node.rect;
|
|
46
|
+
element.style.position = "absolute";
|
|
47
|
+
element.style.left = `${x * metrics.cellWidth}px`;
|
|
48
|
+
element.style.top = `${y * metrics.cellHeight}px`;
|
|
49
|
+
element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;
|
|
50
|
+
element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;
|
|
51
|
+
return element;
|
|
52
|
+
}
|
|
53
|
+
announceLiveRegionChanges(nodes, announcements) {
|
|
54
|
+
const candidates = nodes.filter((node) => node.liveRegion && node.liveRegion !== "off" && node.label);
|
|
55
|
+
const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? ""]));
|
|
56
|
+
const imperativeAssertive = announcements.filter((announcement) => announcement.politeness === "assertive");
|
|
57
|
+
const imperativePolite = announcements.filter((announcement) => announcement.politeness === "polite");
|
|
58
|
+
if (!this.hasLiveRegionBaseline) {
|
|
59
|
+
this.previousLabelsById = currentLabelsById;
|
|
60
|
+
this.hasLiveRegionBaseline = true;
|
|
61
|
+
this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const changed = candidates.filter((node) => {
|
|
65
|
+
const previous = this.previousLabelsById.get(node.id);
|
|
66
|
+
return previous !== void 0 && previous !== node.label;
|
|
67
|
+
});
|
|
68
|
+
this.previousLabelsById = currentLabelsById;
|
|
69
|
+
const assertive = changed.filter((node) => node.liveRegion === "assertive");
|
|
70
|
+
const polite = changed.filter((node) => node.liveRegion === "polite");
|
|
71
|
+
this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);
|
|
72
|
+
}
|
|
73
|
+
publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite) {
|
|
74
|
+
const ordered = [
|
|
75
|
+
...assertive,
|
|
76
|
+
...imperativeAssertive,
|
|
77
|
+
...polite,
|
|
78
|
+
...imperativePolite
|
|
79
|
+
];
|
|
80
|
+
if (ordered.length === 0) return;
|
|
81
|
+
const politeness = assertive.length > 0 || imperativeAssertive.length > 0 ? "assertive" : "polite";
|
|
82
|
+
this.announcerElement.setAttribute("aria-live", politeness);
|
|
83
|
+
this.announcerElement.textContent = ordered.map((entry) => {
|
|
84
|
+
if ("message" in entry) return entry.message;
|
|
85
|
+
return entry.label ?? "";
|
|
86
|
+
}).join("\n");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
function applyScreenReaderOnlyStyle(element) {
|
|
90
|
+
element.style.position = "absolute";
|
|
91
|
+
element.style.left = "0";
|
|
92
|
+
element.style.top = "0";
|
|
93
|
+
element.style.width = "1px";
|
|
94
|
+
element.style.height = "1px";
|
|
95
|
+
element.style.overflow = "hidden";
|
|
96
|
+
element.style.clipPath = "inset(50%)";
|
|
97
|
+
element.style.whiteSpace = "nowrap";
|
|
98
|
+
}
|
|
99
|
+
function roleMapping(role) {
|
|
100
|
+
const heading = /^heading\(level: ([0-9]+)\)$/.exec(role);
|
|
101
|
+
if (heading) return {
|
|
102
|
+
role: "heading",
|
|
103
|
+
level: Math.max(1, Math.min(6, Number(heading[1])))
|
|
104
|
+
};
|
|
105
|
+
const custom = /^custom\((.+)\)$/.exec(role);
|
|
106
|
+
if (custom) return { role: custom[1] };
|
|
107
|
+
switch (role) {
|
|
108
|
+
case "alert":
|
|
109
|
+
case "button":
|
|
110
|
+
case "cell":
|
|
111
|
+
case "checkbox":
|
|
112
|
+
case "grid":
|
|
113
|
+
case "group":
|
|
114
|
+
case "link":
|
|
115
|
+
case "list":
|
|
116
|
+
case "menu":
|
|
117
|
+
case "region":
|
|
118
|
+
case "separator":
|
|
119
|
+
case "slider":
|
|
120
|
+
case "status":
|
|
121
|
+
case "tab":
|
|
122
|
+
case "table":
|
|
123
|
+
case "timer": return { role };
|
|
124
|
+
case "columnHeader": return { role: "columnheader" };
|
|
125
|
+
case "confirmationDialog":
|
|
126
|
+
case "sheet": return { role: "dialog" };
|
|
127
|
+
case "disclosureGroup":
|
|
128
|
+
case "scrollView":
|
|
129
|
+
case "scrollViewWithIndicators":
|
|
130
|
+
case "section": return { role: "region" };
|
|
131
|
+
case "image": return { role: "img" };
|
|
132
|
+
case "menuItem": return { role: "menuitem" };
|
|
133
|
+
case "picker": return { role: "combobox" };
|
|
134
|
+
case "progressBar": return { role: "progressbar" };
|
|
135
|
+
case "rowHeader": return { role: "rowheader" };
|
|
136
|
+
case "secureField":
|
|
137
|
+
case "textEditor":
|
|
138
|
+
case "textField": return { role: "textbox" };
|
|
139
|
+
case "stepper": return { role: "spinbutton" };
|
|
140
|
+
case "tabPanel": return { role: "tabpanel" };
|
|
141
|
+
case "tableRow": return { role: "row" };
|
|
142
|
+
case "tabView": return { role: "tablist" };
|
|
143
|
+
case "toggle": return { role: "checkbox" };
|
|
144
|
+
default: return { role: "group" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function stableDOMId(id) {
|
|
148
|
+
return Array.from(id).map((character) => {
|
|
149
|
+
if (/^[a-zA-Z0-9_-]$/.test(character)) return character;
|
|
150
|
+
return `-${character.codePointAt(0)?.toString(16) ?? "0"}-`;
|
|
151
|
+
}).join("");
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { AccessibilityTreeMounter };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=AccessibilityTree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessibilityTree.js","names":[],"sources":["../../src/AccessibilityTree.ts"],"sourcesContent":["import type {\n WebHostAccessibilityAnnouncement,\n WebHostAccessibilityNode,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface AccessibilityTreeMetrics {\n cellWidth: number;\n cellHeight: number;\n}\n\ninterface AccessibilityTreePresentationOptions {\n synchronizeFocus?: boolean;\n}\n\ninterface RoleMapping {\n role?: string;\n level?: number;\n}\n\nexport class AccessibilityTreeMounter {\n readonly element: HTMLElement;\n readonly announcerElement: HTMLElement;\n\n private readonly nodesById = new Map<string, HTMLElement>();\n private previousLabelsById = new Map<string, string>();\n private hasLiveRegionBaseline = false;\n\n constructor() {\n this.element = document.createElement(\"div\");\n this.element.className = \"webhost-scene__accessibility-tree\";\n applyScreenReaderOnlyStyle(this.element);\n\n this.announcerElement = document.createElement(\"div\");\n this.announcerElement.className = \"webhost-scene__accessibility-announcer\";\n this.announcerElement.setAttribute(\"aria-atomic\", \"true\");\n applyScreenReaderOnlyStyle(this.announcerElement);\n }\n\n present(\n nodes: WebHostAccessibilityNode[],\n metrics: AccessibilityTreeMetrics,\n announcements: WebHostAccessibilityAnnouncement[] = [],\n options: AccessibilityTreePresentationOptions = {}\n ): void {\n this.element.replaceChildren();\n this.nodesById.clear();\n\n for (const node of nodes) {\n const element = this.elementForNode(node, metrics);\n this.nodesById.set(node.id, element);\n }\n\n for (const node of nodes) {\n const element = this.nodesById.get(node.id);\n if (!element) {\n continue;\n }\n\n const parent = node.parentId ? this.nodesById.get(node.parentId) : undefined;\n (parent ?? this.element).appendChild(element);\n }\n\n this.announceLiveRegionChanges(nodes, announcements);\n\n const focused = nodes.find((node) => node.isFocused);\n if ((options.synchronizeFocus ?? true) && focused) {\n this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });\n }\n }\n\n private elementForNode(\n node: WebHostAccessibilityNode,\n metrics: AccessibilityTreeMetrics\n ): HTMLElement {\n const element = document.createElement(\"div\");\n element.id = `swifttui-a11y-${stableDOMId(node.id)}`;\n element.dataset.accessibilityId = node.id;\n element.tabIndex = node.isFocused ? 0 : -1;\n\n const role = roleMapping(node.role);\n if (role.role) {\n element.setAttribute(\"role\", role.role);\n }\n if (role.level !== undefined) {\n element.setAttribute(\"aria-level\", String(role.level));\n }\n if (node.label) {\n element.setAttribute(\"aria-label\", node.label);\n }\n if (node.hint) {\n element.setAttribute(\"aria-description\", node.hint);\n }\n if (node.liveRegion) {\n element.setAttribute(\"aria-live\", node.liveRegion);\n }\n if (node.isFocused) {\n element.dataset.focused = \"true\";\n }\n\n const [x, y, width, height] = node.rect;\n element.style.position = \"absolute\";\n element.style.left = `${x * metrics.cellWidth}px`;\n element.style.top = `${y * metrics.cellHeight}px`;\n element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;\n element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;\n\n return element;\n }\n\n private announceLiveRegionChanges(\n nodes: WebHostAccessibilityNode[],\n announcements: WebHostAccessibilityAnnouncement[]\n ): void {\n const candidates = nodes.filter(\n (node) => node.liveRegion && node.liveRegion !== \"off\" && node.label\n );\n const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? \"\"]));\n const imperativeAssertive = announcements.filter(\n (announcement) => announcement.politeness === \"assertive\"\n );\n const imperativePolite = announcements.filter(\n (announcement) => announcement.politeness === \"polite\"\n );\n\n if (!this.hasLiveRegionBaseline) {\n this.previousLabelsById = currentLabelsById;\n this.hasLiveRegionBaseline = true;\n this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);\n return;\n }\n\n const changed = candidates.filter((node) => {\n const previous = this.previousLabelsById.get(node.id);\n return previous !== undefined && previous !== node.label;\n });\n this.previousLabelsById = currentLabelsById;\n\n const assertive = changed.filter((node) => node.liveRegion === \"assertive\");\n const polite = changed.filter((node) => node.liveRegion === \"polite\");\n this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);\n }\n\n private publishAnnouncements(\n assertive: WebHostAccessibilityNode[],\n imperativeAssertive: WebHostAccessibilityAnnouncement[],\n polite: WebHostAccessibilityNode[],\n imperativePolite: WebHostAccessibilityAnnouncement[]\n ): void {\n const ordered = [...assertive, ...imperativeAssertive, ...polite, ...imperativePolite];\n if (ordered.length === 0) {\n return;\n }\n\n const politeness = assertive.length > 0 || imperativeAssertive.length > 0\n ? \"assertive\"\n : \"polite\";\n this.announcerElement.setAttribute(\"aria-live\", politeness);\n this.announcerElement.textContent = ordered.map((entry) => {\n if (\"message\" in entry) {\n return entry.message;\n }\n return entry.label ?? \"\";\n }).join(\"\\n\");\n }\n}\n\nfunction applyScreenReaderOnlyStyle(\n element: HTMLElement\n): void {\n element.style.position = \"absolute\";\n element.style.left = \"0\";\n element.style.top = \"0\";\n element.style.width = \"1px\";\n element.style.height = \"1px\";\n element.style.overflow = \"hidden\";\n element.style.clipPath = \"inset(50%)\";\n element.style.whiteSpace = \"nowrap\";\n}\n\nfunction roleMapping(\n role: string\n): RoleMapping {\n const heading = /^heading\\(level: ([0-9]+)\\)$/.exec(role);\n if (heading) {\n return {\n role: \"heading\",\n level: Math.max(1, Math.min(6, Number(heading[1]))),\n };\n }\n\n const custom = /^custom\\((.+)\\)$/.exec(role);\n if (custom) {\n return { role: custom[1] };\n }\n\n switch (role) {\n case \"alert\":\n case \"button\":\n case \"cell\":\n case \"checkbox\":\n case \"grid\":\n case \"group\":\n case \"link\":\n case \"list\":\n case \"menu\":\n case \"region\":\n case \"separator\":\n case \"slider\":\n case \"status\":\n case \"tab\":\n case \"table\":\n case \"timer\":\n return { role };\n case \"columnHeader\":\n return { role: \"columnheader\" };\n case \"confirmationDialog\":\n case \"sheet\":\n return { role: \"dialog\" };\n case \"disclosureGroup\":\n case \"scrollView\":\n case \"scrollViewWithIndicators\":\n case \"section\":\n return { role: \"region\" };\n case \"image\":\n return { role: \"img\" };\n case \"menuItem\":\n return { role: \"menuitem\" };\n case \"picker\":\n return { role: \"combobox\" };\n case \"progressBar\":\n return { role: \"progressbar\" };\n case \"rowHeader\":\n return { role: \"rowheader\" };\n case \"secureField\":\n case \"textEditor\":\n case \"textField\":\n return { role: \"textbox\" };\n case \"stepper\":\n return { role: \"spinbutton\" };\n case \"tabPanel\":\n return { role: \"tabpanel\" };\n case \"tableRow\":\n return { role: \"row\" };\n case \"tabView\":\n return { role: \"tablist\" };\n case \"toggle\":\n return { role: \"checkbox\" };\n default:\n return { role: \"group\" };\n }\n}\n\nfunction stableDOMId(\n id: string\n): string {\n return Array.from(id).map((character) => {\n if (/^[a-zA-Z0-9_-]$/.test(character)) {\n return character;\n }\n return `-${character.codePointAt(0)?.toString(16) ?? \"0\"}-`;\n }).join(\"\");\n}\n"],"mappings":"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"}
|