Canvas/WebGL editor의 accessibility layer
DOM으로 만들 때 브라우저가 제공하던 accessibility는 Canvas/WebGL로 옮기면 대부분 사라집니다. 다시 설계해야 합니다.
selected node만 DOM accessibility mirror로 만드는 코드
모든 node를 DOM으로 복제하지 않아도, 현재 선택과 입력 가능한 대상은 접근 가능한 요소로 노출할 수 있습니다.
function renderAccessibilityLayer(root, snapshot, dispatch) {
root.replaceChildren();
for (const nodeId of snapshot.selection) {
const node = snapshot.scene.nodesById.get(nodeId);
const button = document.createElement("button");
button.className = "sr-only-node-proxy";
button.textContent = `${node.type} ${node.name}`;
button.setAttribute("aria-label", `${node.name}, ${node.type}, selected`);
button.onclick = () => dispatch({ type: "focusNode", id: node.id });
root.append(button);
}
if (snapshot.tool.editingTextId) {
const node = snapshot.scene.nodesById.get(snapshot.tool.editingTextId);
const input = document.createElement("textarea");
input.value = node.text;
input.setAttribute("aria-label", `Edit text ${node.name}`);
input.oninput = () => dispatch({ type: "updateTextLive", id: node.id, text: input.value });
root.append(input);
}
}
필요한 계층
focus proxy
keyboard command layer
semantic node mirror
DOM text input overlay
screen reader labels
모든 시각 node를 DOM으로 복제할 필요는 없습니다. 하지만 선택된 node, 입력 가능한 text, 주요 command는 접근 가능한 경로가 필요합니다.
keyboard command layer를 같이 둔다
function handleAccessibleCommand(event, editor) {
if (event.key === "Delete") {
editor.dispatch({ type: "deleteSelection", commit: true });
}
if ((event.metaKey || event.ctrlKey) && event.key === "z") {
editor.dispatch({ type: event.shiftKey ? "redo" : "undo" });
}
if (event.key === "Enter") {
const [id] = editor.getSnapshot().selection;
if (id) editor.dispatch({ type: "startTextEditing", id });
}
}
accessibility layer는 screen reader만을 위한 구조가 아닙니다. keyboard-only 사용자가 command를 실행하고 focus를 이동할 수 있는 경로도 함께 제공해야 합니다.
오늘의 핵심
accessibility layer는 나중에 붙이는 장식이 아닙니다. Canvas/WebGL editor가 DOM을 버릴 때 잃어버린 기능을 되찾는 구조입니다.