// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmaccessibility.h" #include "qwasmscreen.h" #include "qwasmwindow.h" #include "qwasmintegration.h" #include #include #include #if QT_CONFIG(accessibility) #include Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") // Qt WebAssembly a11y backend // // This backend implements accessibility support by creating "shadowing" html // elements for each Qt UI element. We access the DOM by using Emscripten's // val.h API. // // Currently, html elements are created in response to notifyAccessibilityUpdate // events. In addition or alternatively, we could also walk the accessibility tree // from setRootObject(). QWasmAccessibility::QWasmAccessibility() { s_instance = this; if (qEnvironmentVariableIntValue("QT_WASM_ENABLE_ACCESSIBILITY") == 1) enableAccessibility(); // Register accessibility element event handler QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get(); Q_ASSERT(suspendResume); m_eventHandlerIndex = suspendResume->registerEventHandler([this](const emscripten::val event){ this->handleEventFromHtmlElement(event); }); } QWasmAccessibility::~QWasmAccessibility() { // Remove accessibility element event handler QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get(); Q_ASSERT(suspendResume); suspendResume->removeEventHandler(m_eventHandlerIndex); s_instance = nullptr; } QWasmAccessibility *QWasmAccessibility::s_instance = nullptr; QWasmAccessibility* QWasmAccessibility::get() { return s_instance; } void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window) { get()->addAccessibilityEnableButtonImpl(window); } void QWasmAccessibility::onShowWindow(QWindow *window) { get()->onShowWindowImpl(window); } void QWasmAccessibility::onRemoveWindow(QWindow *window) { get()->onRemoveWindowImpl(window); } void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window) { if (m_accessibilityEnabled) return; emscripten::val container = getElementContainer(window); emscripten::val document = getDocument(container); emscripten::val button = document.call("createElement", std::string("button")); setProperty(button, "innerText", "Enable Screen Reader"); button["classList"].call("add", emscripten::val("hidden-visually-read-by-screen-reader")); container.call("appendChild", button); auto enableContext = std::make_tuple(button, std::make_unique (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); })); m_enableButtons.insert(std::make_pair(window, std::move(enableContext))); } void QWasmAccessibility::onShowWindowImpl(QWindow *window) { if (!m_accessibilityEnabled) return; populateAccessibilityTree(QAccessible::queryAccessibleInterface(window)); } void QWasmAccessibility::onRemoveWindowImpl(QWindow *window) { { const auto it = m_enableButtons.find(window); if (it != m_enableButtons.end()) { // Remove button auto [element, callback] = it->second; Q_UNUSED(callback); element["parentElement"].call("removeChild", element); m_enableButtons.erase(it); } } { auto a11yContainer = getA11yContainer(window); auto describedByContainer = getDescribedByContainer(window); auto elementContainer = getElementContainer(window); auto document = getDocument(a11yContainer); // Remove all items by replacing the container if (!describedByContainer.isUndefined()) { a11yContainer.call("removeChild", describedByContainer); describedByContainer = document.call("createElement", std::string("div")); a11yContainer.call("appendChild", elementContainer); a11yContainer.call("appendChild", describedByContainer); } } } void QWasmAccessibility::enableAccessibility() { // Enable accessibility. Remove all "enable" buttons and populate the // accessibility tree for each window. Q_ASSERT(!m_accessibilityEnabled); m_accessibilityEnabled = true; setActive(true); for (const auto& [key, value] : m_enableButtons) { const auto &[element, callback] = value; Q_UNUSED(callback); onShowWindowImpl(key); element["parentElement"].call("removeChild", element); } m_enableButtons.clear(); } emscripten::val QWasmAccessibility::getA11yContainer(QWindow *window) { const auto wasmWindow = QWasmWindow::fromWindow(window); if (!wasmWindow) return emscripten::val::undefined(); auto a11yContainer = wasmWindow->a11yContainer(); if (a11yContainer["childElementCount"].as() == 2) return a11yContainer; Q_ASSERT(a11yContainer["childElementCount"].as() == 0); const auto document = getDocument(a11yContainer); if (document.isUndefined()) return emscripten::val::undefined(); auto elementContainer = document.call("createElement", std::string("div")); auto describedByContainer = document.call("createElement", std::string("div")); a11yContainer.call("appendChild", elementContainer); a11yContainer.call("appendChild", describedByContainer); return a11yContainer; } emscripten::val QWasmAccessibility::getA11yContainer(QAccessibleInterface *iface) { return getA11yContainer(getWindow(iface)); } emscripten::val QWasmAccessibility::getDescribedByContainer(QWindow *window) { auto a11yContainer = getA11yContainer(window); if (a11yContainer.isUndefined()) return emscripten ::val::undefined(); Q_ASSERT(a11yContainer["childElementCount"].as() == 2); Q_ASSERT(!a11yContainer["children"][1].isUndefined()); return a11yContainer["children"][1]; } emscripten::val QWasmAccessibility::getDescribedByContainer(QAccessibleInterface *iface) { return getDescribedByContainer(getWindow(iface)); } emscripten::val QWasmAccessibility::getElementContainer(QWindow *window) { auto a11yContainer = getA11yContainer(window); if (a11yContainer.isUndefined()) return emscripten ::val::undefined(); Q_ASSERT(a11yContainer["childElementCount"].as() == 2); Q_ASSERT(!a11yContainer["children"][0].isUndefined()); return a11yContainer["children"][0]; } emscripten::val QWasmAccessibility::getElementContainer(QAccessibleInterface *iface) { if (!iface) return emscripten::val::undefined(); return getElementContainer(getWindow(iface)); } QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface) { QWindow *window = iface->window(); // this is needed to add tabs as the window is not available if (!window && iface->parent()) window = iface->parent()->window(); return window; } emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container) { if (container.isUndefined()) return emscripten::val::global("document"); return container["ownerDocument"]; } emscripten::val QWasmAccessibility::getDocument(QAccessibleInterface *iface) { return getDocument(getA11yContainer(iface)); } void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, const std::string &val) { if (val != "") element.call("setAttribute", attr, val); else element.call("removeAttribute", attr); } void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, const char *val) { setAttribute(element, attr, std::string(val)); } void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, bool val) { if (val) element.call("setAttribute", attr, val); else element.call("removeAttribute", attr); } void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, const std::string &val) { element.set(property, val); } void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, const char *val) { setProperty(element, property, std::string(val)); } void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, bool val) { element.set(property, val); } void QWasmAccessibility::addEventListener(emscripten::val element, const char *eventType) { element.call("addEventListener", emscripten::val(eventType), QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_eventHandlerIndex), true); } emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface) { // Get the html container element for the interface; this depends on which // QScreen it is on. If the interface is not on a screen yet we get an undefined // container, and the code below handles that case as well. emscripten::val container = getElementContainer(iface); // Get the correct html document for the container, or fall back // to the global document. TODO: Does using the correct document actually matter? emscripten::val document = getDocument(container); // Translate the Qt a11y elemen role into html element type + ARIA role. // Here we can either create
elements with a spesific ARIA role, // or create e.g.