Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasmaccessibility.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6#include "qwasmscreen.h"
7#include "qwasmwindow.h"
9#include <QtCore/private/qwasmsuspendresumecontrol_p.h>
10#include <QtGui/qguiapplication.h>
11#include <QtGui/qwindow.h>
12
14{
15 QWasmAccessibility::enable();
16}
17
18#if QT_CONFIG(accessibility)
19
20#include <QtGui/private/qaccessiblebridgeutils_p.h>
21
22Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility")
23
24namespace {
25EM_JS(emscripten::EM_VAL, getActiveElement_js, (emscripten::EM_VAL undefHandle), {
26 var activeEl = document.activeElement;
27 while (true) {
28 if (!activeEl) {
29 return undefHandle;
30 } else if (activeEl.shadowRoot) {
31 activeEl = activeEl.shadowRoot.activeElement;
32 } else {
33 return Emval.toHandle(activeEl);
34 }
35 }
36})
37
38static emscripten::val getActiveElement()
39{
40 return emscripten::val::take_ownership(
41 getActiveElement_js(emscripten::val::undefined().as_handle()));
42}
43
44#ifndef QT_NO_DEBUG
45static void logElement(emscripten::val element)
46{
47 emscripten::val::global("console").call<void>("log", element);
48}
49#endif
50
51}
52
53// Qt WebAssembly a11y backend
54//
55// This backend implements accessibility support by creating "shadowing" html
56// elements for each Qt UI element. We access the DOM by using Emscripten's
57// val.h API.
58//
59// Currently, html elements are created in response to notifyAccessibilityUpdate
60// events. In addition or alternatively, we could also walk the accessibility tree
61// from setRootObject().
62
63QWasmAccessibility::QWasmAccessibility()
64{
65 if (qEnvironmentVariableIntValue("QT_WASM_ENABLE_ACCESSIBILITY") == 1)
66 enableAccessibility();
67
68 // Register accessibility element event handler
69 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
70
71 m_eventHandlerIndex = suspendResume->registerEventHandler([this](const emscripten::val event){
72 this->handleEventFromHtmlElement(event);
73 });
74}
75
76QWasmAccessibility::~QWasmAccessibility()
77{
78 // Remove accessibility element event handler
79 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
80 suspendResume->removeEventHandler(m_eventHandlerIndex);
81}
82
83QWasmAccessibility *QWasmAccessibility::instance()
84{
85 QWasmIntegration *integration = QWasmIntegration::get();
86 return integration ? static_cast<QWasmAccessibility *>(integration->accessibility()) : nullptr;
87}
88
89void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window)
90{
91 if (QWasmAccessibility *accessibility = instance(); accessibility && !accessibility->isActive())
92 accessibility->addEnableButton(window);
93}
94
95void QWasmAccessibility::onShowWindow(QWindow *window)
96{
97 if (QWasmAccessibility *accessibility = instance(); accessibility && accessibility->isActive())
98 accessibility->populateAccessibilityTree(window->accessibleRoot());
99}
100
101void QWasmAccessibility::onRemoveWindow(QWindow *window)
102{
103 if (QWasmAccessibility *accessibility = instance())
104 accessibility->removeWindow(window);
105}
106
107bool QWasmAccessibility::isEnabled()
108{
109 const QWasmAccessibility *accessibility = instance();
110 return accessibility && accessibility->isActive();
111}
112
113void QWasmAccessibility::enable()
114{
115 if (QWasmAccessibility *accessibility = instance(); accessibility && !accessibility->isActive())
116 accessibility->enableAccessibility();
117}
118
119bool QWasmAccessibility::setFocusObject(QObject *object)
120{
121 QWasmAccessibility *accessibility = instance();
122 return accessibility && accessibility->setFocusObjectImpl(object);
123}
124
125void QWasmAccessibility::addEnableButton(QWindow *window)
126{
127 emscripten::val container = getElementContainer(window);
128 if (container.isUndefined())
129 return;
130
131 emscripten::val document = getDocument(container);
132 emscripten::val button = document.call<emscripten::val>("createElement", std::string("button"));
133 setProperty(button, "innerText", "Enable Screen Reader");
134 button["classList"].call<void>("add", emscripten::val("hidden-visually-read-by-screen-reader"));
135 button.set("tabIndex", -1);
136 container.call<void>("appendChild", button);
137
138 auto enableContext = std::make_tuple(button, std::make_unique<qstdweb::EventCallback>
139 (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); }));
140 m_enableButtons.insert(std::make_pair(window, std::move(enableContext)));
141}
142
143void QWasmAccessibility::removeWindow(QWindow *window)
144{
145 {
146 const auto it = m_enableButtons.find(window);
147 if (it != m_enableButtons.end()) {
148 // Remove button
149 auto [element, callback] = it->second;
150 Q_UNUSED(callback);
151 element["parentElement"].call<void>("removeChild", element);
152 m_enableButtons.erase(it);
153 }
154 }
155 const QWasmWindow *wasmWindow = QWasmWindow::fromWindow(window);
156 if (!isActive() || !wasmWindow)
157 return;
158
159 const emscripten::val a11yContainer = wasmWindow->a11yContainer();
160
161 // Forget the elements of the window, they go away with its container.
162 for (auto it = m_elements.begin(); it != m_elements.end();) {
163 if (a11yContainer.call<bool>("contains", it.value()))
164 it = m_elements.erase(it);
165 else
166 ++it;
167 }
168 getElementContainer(window).call<void>("replaceChildren");
169 getDescribedByContainer(window).call<void>("replaceChildren");
170}
171
172#ifndef QT_NO_DEBUG
173std::string QWasmAccessibility::logElementParentChain(emscripten::val element, QAccessibleInterface *iface, std::string tab)
174{
175 if (element.isUndefined())
176 element = getHtmlElement(iface);
177
178 QAccessibleInterface *parentIface = nullptr;
179 if (!element["parentElement"].isUndefined() &&
180 !element["parentElement"].isNull() &&
181 !element["parentElement"]["qtA11yId"].isUndefined()) {
182 parentIface = QAccessible::accessibleInterface(
183 element["parentElement"]["qtA11yId"].as<QAccessible::Id>());
184 }
185
186 emscripten::val parent = emscripten::val::undefined();
187 if (!element.isUndefined())
188 parent = element["parentElement"];
189 if (parent.isNull())
190 parent = emscripten::val::undefined();
191
192 if (!parent.isUndefined())
193 tab = logElementParentChain(parent, parentIface, tab);
194
195 QString outerHTML = QStringLiteral("No html");
196 if (!element.isUndefined())
197 outerHTML = QString::fromStdString(element["outerHTML"].as<std::string>()).left(40);
198
199 if (!iface)
200 qCDebug(lcQpaAccessibility) << tab << outerHTML << "No Role" << "No parent role"
201 << element.isUndefined() << parent.isUndefined();
202 else
203 qCDebug(lcQpaAccessibility) << tab << outerHTML << iface->role()
204 << (iface->parent() ? iface->parent()->role() : QAccessible::NoRole)
205 << element.isUndefined() << parent.isUndefined();
206 return tab + " ";
207}
208#endif
209
210void QWasmAccessibility::enableAccessibility()
211{
212 // Enable accessibility. Remove all "enable" buttons and populate the
213 // accessibility tree for each window.
214
215 Q_ASSERT(!isActive());
216 setActive(true);
217 for (const auto& [key, value] : m_enableButtons) {
218 const auto &[element, callback] = value;
219 Q_UNUSED(callback);
220 if (auto wasmWindow = QWasmWindow::fromWindow(key))
221 wasmWindow->onAccessibilityEnable();
222 populateAccessibilityTree(key->accessibleRoot());
223 element["parentElement"].call<void>("removeChild", element);
224 }
225 m_enableButtons.clear();
226}
227
228bool QWasmAccessibility::setFocusObjectImpl(QObject *object)
229{
230 auto iface = QAccessible::queryAccessibleInterface(object);
231 if (!iface)
232 return false;
233 return setHtmlElementFocus(iface);
234}
235
236bool QWasmAccessibility::isWindowNode(QAccessibleInterface *iface)
237{
238 return (iface && !getWindow(iface->parent()) && getWindow(iface));
239}
240
241emscripten::val QWasmAccessibility::getDescribedByContainer(QWindow *window)
242{
243 const QWasmWindow *wasmWindow = QWasmWindow::fromWindow(window);
244 return wasmWindow ? wasmWindow->a11yDescribedByContainer() : emscripten::val::undefined();
245}
246
247emscripten::val QWasmAccessibility::getDescribedByContainer(QAccessibleInterface *iface)
248{
249 return getDescribedByContainer(getWindow(iface));
250}
251
252emscripten::val QWasmAccessibility::getElementContainer(QWindow *window)
253{
254 const QWasmWindow *wasmWindow = QWasmWindow::fromWindow(window);
255 return wasmWindow ? wasmWindow->a11yElementContainer() : emscripten::val::undefined();
256}
257
258emscripten::val QWasmAccessibility::getElementContainer(QAccessibleInterface *iface)
259{
260 // Here we skip QWindow nodes, as they are already present. Such nodes
261 // has a parent window of null.
262 //
263 // The next node should return the a11y container.
264 // Further nodes should return the element of the parent.
265 if (!getWindow(iface))
266 return emscripten::val::undefined();
267
268 if (isWindowNode(iface))
269 return emscripten::val::undefined();
270
271 if (isWindowNode(iface->parent()))
272 return getElementContainer(getWindow(iface->parent()));
273
274 // Regular node
275 return getHtmlElement(iface->parent());
276}
277
278QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface)
279{
280 if (!iface)
281 return nullptr;
282
283 QWindow *window = iface->window();
284 // this is needed to add tabs as the window is not available
285 if (!window && iface->parent())
286 window = iface->parent()->window();
287 return window;
288}
289
290// Returns whether the focus window is a child window of the window of iface,
291// for instance the window of a window container
292bool QWasmAccessibility::isFocusInChildWindow(QAccessibleInterface *iface)
293{
294 const QWindow *window = getWindow(iface);
295 const QWindow *focusWindow = QGuiApplication::focusWindow();
296 return window && focusWindow && focusWindow != window
297 && window->isAncestorOf(focusWindow, QWindow::ExcludeTransients);
298}
299
300emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container)
301{
302 if (container.isUndefined())
303 return emscripten::val::global("document");
304 return container["ownerDocument"];
305}
306
307void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
308 const std::string &val)
309{
310 if (val != "")
311 element.call<void>("setAttribute", attr, val);
312 else
313 element.call<void>("removeAttribute", attr);
314}
315
316void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
317 const char *val)
318{
319 setAttribute(element, attr, std::string(val));
320}
321
322void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, bool val)
323{
324 if (val)
325 element.call<void>("setAttribute", attr, val);
326 else
327 element.call<void>("removeAttribute", attr);
328}
329
330void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property,
331 const std::string &val)
332{
333 element.set(property, val);
334}
335
336void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, const char *val)
337{
338 setProperty(element, property, std::string(val));
339}
340
341void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, bool val)
342{
343 element.set(property, val);
344}
345
346void QWasmAccessibility::setNamedAttribute(QAccessibleInterface *iface, const std::string &attribute, QAccessible::Text text)
347{
348 const emscripten::val element = getHtmlElement(iface);
349 setAttribute(element, attribute, iface->text(text).toStdString());
350}
351void QWasmAccessibility::setNamedProperty(QAccessibleInterface *iface, const std::string &property, QAccessible::Text text)
352{
353 const emscripten::val element = getHtmlElement(iface);
354 setProperty(element, property, iface->text(text).toStdString());
355}
356
357void QWasmAccessibility::addEventListener(QAccessibleInterface *iface, emscripten::val element, const char *eventType)
358{
359 element.set("qtA11yId", QAccessible::uniqueId(iface));
360 element.call<void>("addEventListener", emscripten::val(eventType),
361 QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_eventHandlerIndex),
362 true);
363}
364
365void QWasmAccessibility::sendEvent(QAccessibleInterface *iface, QAccessible::Event eventType)
366{
367 if (iface->object()) {
368 QAccessibleEvent event(iface->object(), eventType);
369 handleUpdateByInterfaceRole(&event);
370 } else {
371 QAccessibleEvent event(iface, eventType);
372 handleUpdateByInterfaceRole(&event);
373 }
374}
375
376emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface)
377{
378 // Get the html container element for the interface; this depends on which
379 // QScreen it is on. If the interface is not on a screen yet we get an undefined
380 // container, and the code below handles that case as well.
381 emscripten::val container = getElementContainer(iface);
382
383 // Get the correct html document for the container, or fall back
384 // to the global document. TODO: Does using the correct document actually matter?
385 emscripten::val document = getDocument(container);
386
387 // Translate the Qt a11y elemen role into html element type + ARIA role.
388 // Here we can either create <div> elements with a spesific ARIA role,
389 // or create e.g. <button> elements which should have built-in accessibility.
390 emscripten::val element = [this, iface, document] {
391
392 emscripten::val element = emscripten::val::undefined();
393
394 switch (iface->role()) {
395
396 case QAccessible::Button: {
397 element = document.call<emscripten::val>("createElement", std::string("button"));
398 addEventListener(iface, element, "click");
399 } break;
400
401 case QAccessible::Grouping:
402 case QAccessible::CheckBox: {
403 // Three cases:
404 // 1) role=CheckBox, childCount() == 0 -> Checkbox
405 // 2) role=CheckBox, childCount() > 0 -> GroupBox w/checkbox
406 // 3) role=Grouping -> GroupBox w/label
407
408 emscripten::val checkbox = emscripten::val::undefined();
409 if (iface->role() == QAccessible::CheckBox) {
410 checkbox = document.call<emscripten::val>("createElement", std::string("input"));
411 setAttribute(checkbox, "type", "checkbox");
412 // The checked attribute is only the default, the browser stops reading
413 // it once the input is activated, so always write the property.
414 setProperty(checkbox, "checked", iface->state().checked);
415 setProperty(checkbox, "indeterminate", iface->state().checkStateMixed);
416 addEventListener(iface, checkbox, "change");
417 }
418
419 if (iface->childCount() > 0 || iface->role() == QAccessible::Grouping) {
420 auto label = document.call<emscripten::val>("createElement", std::string("span"));
421
422 const std::string id = "lbid_" + std::to_string(QAccessible::uniqueId(iface));
423 setAttribute(label, "id", id);
424
425 element = document.call<emscripten::val>("createElement", std::string("div"));
426 element.call<void>("appendChild", label);
427
428 setAttribute(element, "role", "group");
429 setAttribute(element, "aria-labelledby", id);
430
431 if (!checkbox.isUndefined()) {
432 element.call<void>("appendChild", checkbox);
433 addEventListener(iface, checkbox, "focus");
434 }
435 } else {
436 element = checkbox;
437 }
438 } break;
439
440 case QAccessible::Switch: {
441 element = document.call<emscripten::val>("createElement", std::string("button"));
442 setAttribute(element, "type", "button");
443 setAttribute(element, "role", "switch");
444 if (iface->state().checked)
445 setAttribute(element, "aria-checked", "true");
446 else
447 setAttribute(element, "aria-checked", "false");
448 addEventListener(iface, element, "click");
449 } break;
450
451 case QAccessible::RadioButton: {
452 element = document.call<emscripten::val>("createElement", std::string("input"));
453 setAttribute(element, "type", "radio");
454 setProperty(element, "checked", iface->state().checked);
455 // Qt makes sibling radio buttons exclusive, so group them by their parent.
456 // A shared name would merge every group in the window into one.
457 if (QAccessibleInterface *parentInterface = iface->parent()) {
458 setProperty(element, "name",
459 "qt-radio-group-" + std::to_string(QAccessible::uniqueId(parentInterface)));
460 }
461 addEventListener(iface, element, "change");
462 } break;
463
464 case QAccessible::Dial: {
465 // A dial is rotary and has no html counterpart, so use the slider role
466 // rather than a number input, which reads as a spin button.
467 element = document.call<emscripten::val>("createElement", std::string("div"));
468 setAttribute(element, "tabindex", "0");
469 setAttribute(element, "role", "slider");
470 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
471 setAttribute(element, "aria-valuemin",
472 valueInterface->minimumValue().toString().toStdString());
473 setAttribute(element, "aria-valuemax",
474 valueInterface->maximumValue().toString().toStdString());
475 setAttribute(element, "aria-valuenow",
476 valueInterface->currentValue().toString().toStdString());
477 }
478 // No aria-orientation, a dial turns rather than runs along an axis.
479 } break;
480
481 case QAccessible::SpinBox: {
482 // A number input is a text field, and writing its value while it has
483 // the focus is announced as a text edit, not as a new value.
484 element = document.call<emscripten::val>("createElement", std::string("div"));
485 setAttribute(element, "tabindex", "0");
486 setAttribute(element, "role", "spinbutton");
487 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
488 setAttribute(element, "aria-valuemin",
489 valueInterface->minimumValue().toString().toStdString());
490 setAttribute(element, "aria-valuemax",
491 valueInterface->maximumValue().toString().toStdString());
492 setAttribute(element, "aria-valuenow",
493 valueInterface->currentValue().toString().toStdString());
494 }
495 // The displayed text carries the prefix and suffix, the bare number does not.
496 setAttribute(element, "aria-valuetext",
497 iface->text(QAccessible::Value).toStdString());
498 } break;
499
500 case QAccessible::Slider: {
501 // A number input reads as a spin button, and writing its value while
502 // it has the focus is announced as a text edit, not as a new value.
503 element = document.call<emscripten::val>("createElement", std::string("div"));
504 setAttribute(element, "tabindex", "0");
505 setAttribute(element, "role", "slider");
506 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
507 setAttribute(element, "aria-valuemin",
508 valueInterface->minimumValue().toString().toStdString());
509 setAttribute(element, "aria-valuemax",
510 valueInterface->maximumValue().toString().toStdString());
511 setAttribute(element, "aria-valuenow",
512 valueInterface->currentValue().toString().toStdString());
513 }
514 setHtmlElementOrientation(element, iface);
515 } break;
516
517 case QAccessible::PageTabList:{
518 element = document.call<emscripten::val>("createElement", std::string("div"));
519 setAttribute(element, "role", "tablist");
520 setHtmlElementOrientation(element, iface);
521
522 m_elements[QAccessible::uniqueId(iface)] = element;
523
524 for (int i = 0; i < iface->childCount(); ++i)
525 createHtmlElement(iface->child(i));
526
527 } break;
528
529 case QAccessible::PageTab:{
530 const QString text = iface->text(QAccessible::Name);
531 element = document.call<emscripten::val>("createElement", std::string("button"));
532 setAttribute(element, "role", "tab");
533 setAttribute(element, "title", text.toStdString());
534 addEventListener(iface, element, "click");
535 } break;
536
537 case QAccessible::ScrollBar: {
538 // Events for scrollbars are handled by Qt, but setFocusPolicy must
539 // be called for this to happen.
540 element = document.call<emscripten::val>("createElement", std::string("div"));
541 setAttribute(element, "tabindex", "0");
542 setAttribute(element, "role", "scrollbar");
543 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
544 setAttribute(element, "aria-valuemin",
545 valueInterface->minimumValue().toString().toStdString());
546 setAttribute(element, "aria-valuemax",
547 valueInterface->maximumValue().toString().toStdString());
548 setAttribute(element, "aria-valuenow",
549 valueInterface->currentValue().toString().toStdString());
550 }
551 setHtmlElementOrientation(element, iface);
552 addEventListener(iface, element, "change");
553 } break;
554
555 case QAccessible::StaticText: {
556 element = document.call<emscripten::val>("createElement", std::string("div"));
557 } break;
558 case QAccessible::AlertMessage:
559 case QAccessible::Dialog: {
560 element = document.call<emscripten::val>("createElement", std::string("dialog"));
561 setAttribute(element, "open", true);
562 }break;
563 case QAccessible::ToolBar:{
564 const QString text = iface->text(QAccessible::Name);
565 element = document.call<emscripten::val>("createElement", std::string("div"));
566 setAttribute(element, "role", "toolbar");
567 setAttribute(element, "title", text.toStdString());
568 setHtmlElementOrientation(element, iface);
569 addEventListener(iface, element, "click");
570 }break;
571 case QAccessible::MenuItem:
572 case QAccessible::ButtonDropDown:
573 case QAccessible::ButtonMenu: {
574 const QString text = iface->text(QAccessible::Name);
575 element = document.call<emscripten::val>("createElement", std::string("button"));
576 setAttribute(element, "role", "menuitem");
577 setAttribute(element, "title", text.toStdString());
578 addEventListener(iface, element, "click");
579 element["classList"].call<void>("add", emscripten::val("menu-item"));
580 }break;
581 case QAccessible::MenuBar:
582 case QAccessible::PopupMenu: {
583 const QString text = iface->text(QAccessible::Name);
584 element = document.call<emscripten::val>("createElement", std::string("div"));
585 setAttribute(element, "role",
586 iface->role() == QAccessible::PopupMenu ? "menu" : "menubar");
587 setAttribute(element, "tabindex", "-1");
588 setAttribute(element, "title", text.toStdString());
589 setHtmlElementOrientation(element, iface);
590 m_elements[QAccessible::uniqueId(iface)] = element;
591
592 // The items are nested in the menu element, which already expresses
593 // ownership. aria-owns takes element ids, not the menu title.
594 for (int i = 0; i < iface->childCount(); ++i)
595 createHtmlElement(iface->child(i));
596 }break;
597 case QAccessible::EditableText: {
598 element = document.call<emscripten::val>("createElement", std::string("input"));
599 setAttribute(element, "type", "text");
600 setAttribute(element, "contenteditable", "true");
601 setAttribute(element, "readonly", iface->state().readOnly);
602 setAttribute(element, "autocorrect", "off");
603 setProperty(element, "inputMode", "text");
604 } break;
605 case QAccessible::List: {
606 element = document.call<emscripten::val>("createElement", std::string("div"));
607 setAttribute(element, "tabindex", "0");
608 setAttribute(element, "role", "list");
609 } break;
610 case QAccessible::ListItem: {
611 element = document.call<emscripten::val>("createElement", std::string("div"));
612 setAttribute(element, "tabindex", "0");
613 setAttribute(element, "role", "listitem");
614 } break;
615 case QAccessible::ProgressBar: {
616 element = document.call<emscripten::val>("createElement", std::string("div"));
617 setAttribute(element, "tabindex", "0");
618 setAttribute(element, "role", "progressbar");
619 setAttribute(element, "aria-live", "polite");
620 if (auto valueInterface = iface->valueInterface()) {
621 const auto name = iface->text(QAccessible::Name).toStdString();
622 const auto min = valueInterface->minimumValue().toInt();
623 const auto max = valueInterface->maximumValue().toInt();
624 const auto cur = valueInterface->currentValue().toInt();
625
626 // from qprogressbar.cpp
627 const auto totalSteps = qint64(max) - min;
628 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
629
630 if (cur <= min)
631 setAttribute(element, "aria-valuenow", "0");
632 else if (cur >= max)
633 setAttribute(element, "aria-valuenow", "100");
634 else
635 setAttribute(element, "aria-valuenow", std::to_string(progress));
636 setAttribute(element, "aria-valuemin", "0");
637 setAttribute(element, "aria-valuemax", "100");
638 setAttribute(element, "aria-label", name);
639 }
640 } break;
641 default:
642 qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role();
643 element = document.call<emscripten::val>("createElement", std::string("div"));
644 }
645
646 addEventListener(iface, element, "focus");
647 return element;
648
649 }();
650
651 m_elements[QAccessible::uniqueId(iface)] = element;
652
653 setHtmlElementGeometry(iface);
654 setHtmlElementDisabled(iface);
655 setHtmlElementVisibility(iface, !iface->state().invisible);
656 handleIdentifierUpdate(iface);
657 handleDescriptionChanged(iface);
658 sendEvent(iface, QAccessible::NameChanged);
659
660 // Remove from browser Tab order; a11y elements can be focused
661 // programmatically by Qt, but not by the browser
662 element.set("tabIndex", -1);
663
664 linkToParent(iface);
665 // Link in child elements
666 for (int i = 0; i < iface->childCount(); ++i) {
667 if (!getHtmlElement(iface->child(i)).isUndefined())
668 linkToParent(iface->child(i));
669 }
670
671 return element;
672}
673
674void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface)
675{
676 Q_UNUSED(iface);
677 qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement";
678}
679
680emscripten::val QWasmAccessibility::getHtmlElement(QAccessibleInterface *iface)
681{
682 if (!iface)
683 return emscripten::val::undefined();
684 return m_elements.value(QAccessible::uniqueId(iface), emscripten::val::undefined());
685}
686
687void QWasmAccessibility::repairLinks(QAccessibleInterface *iface)
688{
689 // relink any children that are linked to the wrong parent,
690 // This can be caused by a missing ParentChanged event.
691 bool moved = false;
692 for (int i = 0; i < iface->childCount(); ++i) {
693 const auto elementI = getHtmlElement(iface->child(i));
694 const auto containerI = getElementContainer(iface->child(i));
695
696 if (!elementI.isUndefined() &&
697 !containerI.isUndefined() &&
698 !elementI["parentElement"].isUndefined() &&
699 !elementI["parentElement"].isNull() &&
700 elementI["parentElement"] != containerI) {
701 moved = true;
702 break;
703 }
704 }
705 if (moved) {
706 for (int i = 0; i < iface->childCount(); ++i) {
707 const auto elementI = getHtmlElement(iface->child(i));
708 const auto containerI = getElementContainer(iface->child(i));
709 if (!elementI.isUndefined() && !containerI.isUndefined())
710 containerI.call<void>("appendChild", elementI);
711 }
712 }
713}
714
715void QWasmAccessibility::linkToParent(QAccessibleInterface *iface)
716{
717 emscripten::val element = getHtmlElement(iface);
718 emscripten::val container = getElementContainer(iface);
719
720 if (container.isUndefined() || element.isUndefined())
721 return;
722
723 // Make sure that we don't change the focused element
724 const auto activeElementBefore = getActiveElement();
725
726 repairLinks(iface->parent());
727
728 emscripten::val next = emscripten::val::undefined();
729 const int thisIndex = iface->parent()->indexOfChild(iface);
730 if (thisIndex >= 0) {
731 Q_ASSERT(thisIndex < iface->parent()->childCount());
732 for (int i = thisIndex + 1; i < iface->parent()->childCount(); ++i) {
733 const auto elementI = getHtmlElement(iface->parent()->child(i));
734 if (!elementI.isUndefined() &&
735 elementI["parentElement"] == container) {
736 next = elementI;
737 break;
738 }
739 }
740 if (next.isUndefined())
741 container.call<void>("appendChild", element);
742 else
743 container.call<void>("insertBefore", element, next);
744 }
745 const auto activeElementAfter = getActiveElement();
746 if (activeElementBefore != activeElementAfter) {
747 if (!activeElementBefore.isUndefined() && !activeElementBefore.isNull())
748 activeElementBefore.call<void>("focus");
749 }
750}
751
752void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible)
753{
754 emscripten::val element = getHtmlElement(iface);
755 setAttribute(element, "inert", !visible);
756}
757
758void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface)
759{
760 const emscripten::val element = getHtmlElement(iface);
761
762 QRect windowGeometry = iface->rect();
763 if (iface->parent()) {
764 // Both iface and iface->parent returns geometry in screen coordinates
765 // We only want the relative coordinates, so the coordinate system does
766 // not matter as long as it is the same.
767 const QRect parentRect = iface->parent()->rect();
768 const QRect thisRect = iface->rect();
769 const QRect result(thisRect.topLeft() - parentRect.topLeft(), thisRect.size());
770 windowGeometry = result;
771 } else {
772 // Elements without a parent are not a part of the a11y tree, and don't
773 // have meaningful geometry.
774 Q_ASSERT(!getWindow(iface));
775 }
776 setHtmlElementGeometry(element, windowGeometry);
777}
778
779void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry)
780{
781 // Position the element using "position: absolute" in order to place
782 // it under the corresponding Qt element in the screen.
783 emscripten::val style = element["style"];
784 style.set("position", std::string("absolute"));
785 style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the
786 // screen element, but isn't
787 style.set("left", std::to_string(geometry.x()) + "px");
788 style.set("top", std::to_string(geometry.y()) + "px");
789 style.set("width", std::to_string(geometry.width()) + "px");
790 style.set("height", std::to_string(geometry.height()) + "px");
791}
792
793bool QWasmAccessibility::setHtmlElementFocus(QAccessibleInterface *iface)
794{
795 m_focusId = QAccessible::uniqueId(iface);
796 const auto element = getHtmlElement(iface);
797
798 if (element.isUndefined())
799 return false;
800
801 // To break focus loops
802 if (element == m_isSettingFocusIface)
803 return true;
804
805 m_isFollowingQtFocus = true;
806 element.call<void>("focus");
807 m_isFollowingQtFocus = false;
808
809#ifndef QT_NO_DEBUG
810 if (lcQpaAccessibility().isDebugEnabled()) {
811 const auto activeElementAfter = getActiveElement();
812
813 if (element != activeElementAfter) {
814 qCDebug(lcQpaAccessibility) << "Focus failed" << iface;
815 logElement(element);
816 logElement(activeElementAfter);
817 logElementParentChain(element, iface);
818 }
819 }
820#endif
821 // A detached or inert element does not take focus, let the caller fall back.
822 return element == getActiveElement();
823}
824
825void QWasmAccessibility::setHtmlElementDisabled(QAccessibleInterface *iface)
826{
827 auto element = getHtmlElement(iface);
828 setAttribute(element, "aria-disabled", iface->state().disabled);
829}
830
831void QWasmAccessibility::setHtmlElementOrientation(emscripten::val element, QAccessibleInterface *iface)
832{
833 Q_ASSERT(iface);
834 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
835 const QVariant orientationVariant =
836 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
837 if (orientationVariant.isValid()) {
838 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
839 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
840 const std::string value = orientation == Qt::Horizontal ? "horizontal" : "vertical";
841 setAttribute(element, "aria-orientation", value);
842 }
843 }
844}
845
846void QWasmAccessibility::handleListItemUpdate(QAccessibleEvent *event)
847{
848 auto iface = event->accessibleInterface();
849
850 switch (event->type()) {
851 case QAccessible::NameChanged: {
852 // ListItem is a div
853 setNamedProperty(iface, "innerText", QAccessible::Name);
854 } break;
855 default:
856 qCDebug(lcQpaAccessibility) << "TODO: implement handleListItemUpdate for event" << event->type();
857 break;
858 }
859}
860
861void QWasmAccessibility::handleDialUpdate(QAccessibleEvent *event)
862{
863 switch (event->type()) {
864 case QAccessible::ObjectCreated:
865 case QAccessible::StateChanged: {
866 } break;
867 case QAccessible::Focus:
868 case QAccessible::NameChanged: {
869 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
870 } break;
871 case QAccessible::ValueChanged: {
872 QAccessibleInterface *accessible = event->accessibleInterface();
873 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
874 setAttribute(getHtmlElement(accessible), "aria-valuenow",
875 valueInterface->currentValue().toString().toStdString());
876 }
877 } break;
878 default:
879 qCDebug(lcQpaAccessibility) << "TODO: implement handleDialUpdate for event" << event->type();
880 break;
881 }
882}
883
884void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event)
885{
886 switch (event->type()) {
887 case QAccessible::NameChanged: {
888 // StaticText is a div
889 setNamedProperty(event->accessibleInterface(), "innerText", QAccessible::Name);
890 } break;
891 default:
892 qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type();
893 break;
894 }
895}
896
897void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event)
898{
899 switch (event->type()) {
900 case QAccessible::StateChanged: {
901 auto iface = event->accessibleInterface();
902 auto element = getHtmlElement(iface);
903 setAttribute(element, "readonly", iface->state().readOnly);
904 if (iface->state().passwordEdit)
905 setProperty(element, "type", "password");
906 else
907 setProperty(element, "type", "text");
908 } break;
909 case QAccessible::ValueChanged:
910 case QAccessible::NameChanged: {
911 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
912 } break;
913 case QAccessible::ObjectShow:
914 case QAccessible::Focus: {
915 auto iface = event->accessibleInterface();
916 auto element = getHtmlElement(iface);
917 if (!element.isUndefined()) {
918 setAttribute(element, "readonly", iface->state().readOnly);
919 if (iface->state().passwordEdit)
920 setProperty(element, "type", "password");
921 else
922 setProperty(element, "type", "text");
923 }
924 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
925 } break;
926 case QAccessible::TextRemoved:
927 case QAccessible::TextInserted:
928 case QAccessible::TextUpdated: {
929 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
930 } break;
931 case QAccessible::TextCaretMoved: {
932 // We lack ValueChanged event, sync on caret move instead
933 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
934 } break;
935 default:
936 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
937 break;
938 }
939}
940
941void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event)
942{
943 const emscripten::val target = event["target"];
944 if (target.isNull() || target.isUndefined() || target["qtA11yId"].isUndefined())
945 return;
946
947 // Resolve the id, the interface may be gone by the time the event arrives.
948 QAccessibleInterface *iface =
949 QAccessible::accessibleInterface(target["qtA11yId"].as<QAccessible::Id>());
950 if (!iface || !iface->isValid())
951 return;
952
953 const auto eventType = QString::fromStdString(event["type"].as<std::string>());
954 const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
955 bool handled = false;
956
957 if (eventType == "focus") {
958 // The dom focus follows Qt here, performing the focus action would take the
959 // keyboard focus from the item that has it, for instance a combo box with an open popup.
960 if (m_isFollowingQtFocus)
961 return;
962 m_isSettingFocusIface = event["target"];
963 if (actionNames.contains(QAccessibleActionInterface::setFocusAction()))
964 iface->actionInterface()->doAction(QAccessibleActionInterface::setFocusAction());
965 m_isSettingFocusIface = emscripten::val::undefined();
966 } else if (eventType == "click") {
967 handled = true;
968 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
969 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
970 else if (actionNames.contains(QAccessibleActionInterface::showMenuAction()))
971 iface->actionInterface()->doAction(QAccessibleActionInterface::showMenuAction());
972 } else if (eventType == "change") {
973 handled = true;
974 // Checkboxes can have both toggle and press, in this case it is
975 // important to invoke press to handle tristate correctly
976 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
977 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
978 else if (actionNames.contains(QAccessibleActionInterface::toggleAction()))
979 iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction());
980 } else {
981 qCDebug(lcQpaAccessibility) << "Unknown event" << eventType;
982 }
983
984 if (handled) {
985 event.call<void>("preventDefault");
986 event.call<void>("stopPropagation");
987 }
988}
989
990void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event)
991{
992 switch (event->type()) {
993 case QAccessible::Focus:
994 case QAccessible::NameChanged: {
995 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
996 } break;
997 default:
998 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
999 break;
1000 }
1001}
1002
1003void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event)
1004{
1005 switch (event->type()) {
1006 case QAccessible::Focus:
1007 case QAccessible::NameChanged: {
1008 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1009 } break;
1010 case QAccessible::StateChanged: {
1011 QAccessibleInterface *accessible = event->accessibleInterface();
1012 const emscripten::val element = getHtmlElement(accessible);
1013 setProperty(element, "checked", accessible->state().checked);
1014 setProperty(element, "indeterminate", accessible->state().checkStateMixed);
1015 } break;
1016 default:
1017 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
1018 break;
1019 }
1020}
1021
1022void QWasmAccessibility::handleGroupBoxUpdate(QAccessibleEvent *event)
1023{
1024 QAccessibleInterface *iface = event->accessibleInterface();
1025
1026 emscripten::val parent = getHtmlElement(iface);
1027 emscripten::val label = parent["children"][0];
1028 emscripten::val checkbox = emscripten::val::undefined();
1029 if (iface->role() == QAccessible::CheckBox)
1030 checkbox = parent["children"][1];
1031
1032 switch (event->type()) {
1033 case QAccessible::Focus:
1034 case QAccessible::NameChanged: {
1035 setProperty(label, "innerText", iface->text(QAccessible::Name).toStdString());
1036 if (!checkbox.isUndefined())
1037 setAttribute(checkbox, "aria-label", iface->text(QAccessible::Name).toStdString());
1038 } break;
1039 case QAccessible::StateChanged: {
1040 if (!checkbox.isUndefined()) {
1041 setProperty(checkbox, "checked", iface->state().checked);
1042 setProperty(checkbox, "indeterminate", iface->state().checkStateMixed);
1043 }
1044 } break;
1045 default:
1046 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
1047 break;
1048 }
1049}
1050
1051void QWasmAccessibility::handleSwitchUpdate(QAccessibleEvent *event)
1052{
1053 switch (event->type()) {
1054 case QAccessible::Focus:
1055 case QAccessible::NameChanged: {
1056 /* A switch is like a button in this regard */
1057 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1058 } break;
1059 case QAccessible::StateChanged: {
1060 QAccessibleInterface *accessible = event->accessibleInterface();
1061 const emscripten::val element = getHtmlElement(accessible);
1062 if (accessible->state().checked)
1063 setAttribute(element, "aria-checked", "true");
1064 else
1065 setAttribute(element, "aria-checked", "false");
1066 } break;
1067 default:
1068 qCDebug(lcQpaAccessibility) << "TODO: implement handleSwitchUpdate for event" << event->type();
1069 break;
1070 }
1071}
1072
1073void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event)
1074{
1075 QAccessibleInterface *iface = event->accessibleInterface();
1076 QString text = iface->text(QAccessible::Name);
1077 QString desc = iface->text(QAccessible::Description);
1078 switch (event->type()) {
1079 case QAccessible::NameChanged:
1080 case QAccessible::StateChanged:{
1081 const emscripten::val element = getHtmlElement(iface);
1082 setAttribute(element, "title", text.toStdString());
1083 } break;
1084 default:
1085 qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type();
1086 break;
1087 }
1088}
1089void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event)
1090{
1091 QAccessibleInterface *iface = event->accessibleInterface();
1092 QString text = iface->text(QAccessible::Name);
1093 QString desc = iface->text(QAccessible::Description);
1094 switch (event->type()) {
1095 case QAccessible::Focus:
1096 case QAccessible::NameChanged:
1097 case QAccessible::MenuStart ://"TODO: To implement later
1098 case QAccessible::StateChanged:{
1099 const emscripten::val element = getHtmlElement(iface);
1100 setAttribute(element, "title", text.toStdString());
1101 } break;
1102 case QAccessible::PopupMenuStart: {
1103 if (iface->childCount() > 0)
1104 setHtmlElementFocus(iface->child(0));
1105 } break;
1106 default:
1107 qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type();
1108 break;
1109 }
1110}
1111void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) {
1112
1113 switch (event->type()) {
1114 case QAccessible::ObjectShow:
1115 // We get focus before the dialog is visible, retry the focus operation
1116 if (QAccessibleInterface *focusInterface = QAccessible::accessibleInterface(m_focusId))
1117 setHtmlElementFocus(focusInterface);
1118 break;
1119
1120 case QAccessible::NameChanged:
1121 case QAccessible::Focus:
1122 case QAccessible::DialogStart:
1123 case QAccessible::StateChanged: {
1124 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1125 } break;
1126 default:
1127 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
1128 break;
1129 }
1130}
1131
1132void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface)
1133{
1134 if (!iface)
1135 return;
1136
1137 // We ignore toplevel windows which is categorized
1138 // by getWindow(iface->parent()) != getWindow(iface)
1139 const QWindow *window1 = getWindow(iface);
1140 const QWindow *window0 = (iface->parent()) ? getWindow(iface->parent()) : nullptr;
1141
1142 if (window1 && window0 == window1) {
1143 // Create html element for the interface, sync up properties.
1144 bool exists = !getHtmlElement(iface).isUndefined();
1145 if (!exists)
1146 exists = !createHtmlElement(iface).isUndefined();
1147
1148 if (exists) {
1149 linkToParent(iface);
1150 setHtmlElementVisibility(iface, !iface->state().invisible);
1151 setHtmlElementGeometry(iface);
1152 setHtmlElementDisabled(iface);
1153 handleIdentifierUpdate(iface);
1154 handleDescriptionChanged(iface);
1155 sendEvent(iface, QAccessible::NameChanged);
1156 }
1157 }
1158 for (int i = 0; i < iface->childCount(); ++i)
1159 populateAccessibilityTree(iface->child(i));
1160}
1161
1162void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event)
1163{
1164 switch (event->type()) {
1165 case QAccessible::Focus:
1166 case QAccessible::NameChanged: {
1167 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1168 } break;
1169 case QAccessible::StateChanged: {
1170 QAccessibleInterface *accessible = event->accessibleInterface();
1171 const emscripten::val element = getHtmlElement(accessible);
1172 setProperty(element, "checked", accessible->state().checked);
1173 } break;
1174 default:
1175 qCDebug(lcQpaAccessibility) << "TODO: implement handleRadioButtonUpdate for event" << event->type();
1176 break;
1177 }
1178}
1179
1180void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event)
1181{
1182 switch (event->type()) {
1183 case QAccessible::ObjectCreated:
1184 case QAccessible::StateChanged: {
1185 } break;
1186 case QAccessible::Focus:
1187 case QAccessible::NameChanged: {
1188 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1189 } break;
1190 case QAccessible::ValueChanged: {
1191 QAccessibleInterface *accessible = event->accessibleInterface();
1192 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1193 setAttribute(getHtmlElement(accessible), "aria-valuenow",
1194 valueInterface->currentValue().toString().toStdString());
1195 }
1196 setNamedAttribute(accessible, "aria-valuetext", QAccessible::Value);
1197 } break;
1198 default:
1199 qCDebug(lcQpaAccessibility) << "TODO: implement handleSpinBoxUpdate for event" << event->type();
1200 break;
1201 }
1202}
1203
1204void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event)
1205{
1206 switch (event->type()) {
1207 case QAccessible::ObjectCreated:
1208 case QAccessible::StateChanged: {
1209 } break;
1210 case QAccessible::Focus:
1211 case QAccessible::NameChanged: {
1212 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1213 } break;
1214 case QAccessible::ValueChanged: {
1215 QAccessibleInterface *accessible = event->accessibleInterface();
1216 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1217 setAttribute(getHtmlElement(accessible), "aria-valuenow",
1218 valueInterface->currentValue().toString().toStdString());
1219 }
1220 } break;
1221 default:
1222 qCDebug(lcQpaAccessibility) << "TODO: implement handleSliderUpdate for event" << event->type();
1223 break;
1224 }
1225}
1226
1227void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event)
1228{
1229 switch (event->type()) {
1230 case QAccessible::Focus:
1231 case QAccessible::NameChanged: {
1232 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1233 } break;
1234 case QAccessible::ValueChanged: {
1235 QAccessibleInterface *accessible = event->accessibleInterface();
1236 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1237 setAttribute(getHtmlElement(accessible), "aria-valuenow",
1238 valueInterface->currentValue().toString().toStdString());
1239 }
1240 } break;
1241 default:
1242 qCDebug(lcQpaAccessibility) << "TODO: implement handleScrollBarUpdate for event" << event->type();
1243 break;
1244 }
1245}
1246
1247void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event)
1248{
1249 switch (event->type()) {
1250 case QAccessible::NameChanged: {
1251 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1252 } break;
1253 case QAccessible::Focus: {
1254 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1255 } break;
1256 default:
1257 qCDebug(lcQpaAccessibility) << "TODO: implement handlePageTabUpdate for event" << event->type();
1258 break;
1259 }
1260}
1261
1262void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event)
1263{
1264 switch (event->type()) {
1265 case QAccessible::NameChanged: {
1266 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1267 } break;
1268 case QAccessible::Focus: {
1269 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1270 } break;
1271 default:
1272 qCDebug(lcQpaAccessibility) << "TODO: implement handlePageTabListUpdate for event" << event->type();
1273 break;
1274 }
1275}
1276
1277void QWasmAccessibility::handleIdentifierUpdate(QAccessibleInterface *iface)
1278{
1279 // The tabs are nested in the tab list element, so they need no aria-owns
1280 // pointing back at it, which would also be the wrong direction.
1281 const emscripten::val element = getHtmlElement(iface);
1282 // Every element gets an id, so that other elements can refer to it.
1283 QString id = iface->text(QAccessible::Identifier).replace(" ", "_");
1284 if (id.isEmpty())
1285 id = QStringLiteral("qt-a11y-") + QString::number(QAccessible::uniqueId(iface));
1286 setAttribute(element, "id", id.toStdString());
1287}
1288
1289void QWasmAccessibility::handleDescriptionChanged(QAccessibleInterface *iface)
1290{
1291 // The element keeps a reference to its description, so that removeObject()
1292 // finds it without the interface.
1293 const auto desc = iface->text(QAccessible::Description).toStdString();
1294 auto element = getHtmlElement(iface);
1295 auto container = getDescribedByContainer(iface);
1296 if (element.isUndefined() || container.isUndefined())
1297 return;
1298
1299 auto describedBy = element["qtA11yDescribedBy"];
1300 if (desc.empty()) {
1301 if (!describedBy.isUndefined())
1302 describedBy.call<void>("remove");
1303 element.set("qtA11yDescribedBy", emscripten::val::undefined());
1304 setAttribute(element, "aria-describedby", "");
1305 } else {
1306 const std::string id = "dbid_" + std::to_string(QAccessible::uniqueId(iface));
1307 if (describedBy.isUndefined()) {
1308 describedBy = getDocument(container).call<emscripten::val>("createElement",
1309 std::string("p"));
1310 container.call<void>("appendChild", describedBy);
1311 element.set("qtA11yDescribedBy", describedBy);
1312 }
1313 setAttribute(describedBy, "id", id);
1314 setAttribute(describedBy, "inert", true);
1315 setAttribute(element, "aria-describedby", id);
1316 setProperty(describedBy, "innerText", desc);
1317 }
1318}
1319
1320void QWasmAccessibility::handleAnnouncement(QAccessibleInterface *iface,
1321 QAccessibleAnnouncementEvent *announcementEvent)
1322{
1323 emscripten::val element = getHtmlElement(iface);
1324 if (element["ariaNotify"].isUndefined())
1325 return;
1326
1327 const std::string message = announcementEvent->message().toStdString();
1328 const std::string prio(announcementEvent->politeness()
1329 == QAccessible::AnnouncementPoliteness::Assertive
1330 ? "high"
1331 : "normal");
1332 emscripten::val options = emscripten::val::object();
1333 options.set("priority", prio);
1334 element.call<void>("ariaNotify", message, options);
1335}
1336
1337void QWasmAccessibility::handleProgressBarUpdate(QAccessibleEvent *event)
1338{
1339 auto iface = event->accessibleInterface();
1340 auto valueInterface = iface->valueInterface();
1341 auto element = getHtmlElement(iface);
1342
1343 switch (event->type()) {
1344 case QAccessible::Focus:
1345 case QAccessible::NameChanged: {
1346 const auto name = iface->text(QAccessible::Name).toStdString();
1347 setAttribute(element, "aria-label", name);
1348 } // fallthrough
1349 case QAccessible::ValueChanged: {
1350 if (valueInterface) {
1351 const auto min = valueInterface->minimumValue().toInt();
1352 const auto max = valueInterface->maximumValue().toInt();
1353 const auto cur = valueInterface->currentValue().toInt();
1354
1355 // from qprogressbar.cpp
1356 const auto totalSteps = qint64(max) - min;
1357 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
1358
1359 if (cur <= min)
1360 setAttribute(element, "aria-valuenow", "0");
1361 else if (cur >= max)
1362 setAttribute(element, "aria-valuenow", "100");
1363 else
1364 setAttribute(element, "aria-valuenow", std::to_string(progress));
1365 }
1366 } break;
1367 default: {
1368 } break;
1369 } /* end-switch */
1370}
1371
1372void QWasmAccessibility::createObject(QAccessibleInterface *iface)
1373{
1374 if (getHtmlElement(iface).isUndefined())
1375 createHtmlElement(iface);
1376}
1377
1378void QWasmAccessibility::removeObject(QAccessible::Id id)
1379{
1380 // The interface may already be invalid, so work from the id and the element
1381 // alone. Objects are removed in no particular order, and remove() copes with
1382 // an element that is already detached.
1383 const auto it = m_elements.find(id);
1384 if (it == m_elements.end())
1385 return;
1386
1387 const emscripten::val element = it.value();
1388 if (!element["qtA11yDescribedBy"].isUndefined())
1389 element["qtA11yDescribedBy"].call<void>("remove");
1390 element.call<void>("remove");
1391 m_elements.erase(it);
1392}
1393
1394void QWasmAccessibility::unlinkParentForChildren(QAccessibleInterface *iface)
1395{
1396 auto element = getHtmlElement(iface);
1397 if (!element.isUndefined()) {
1398 auto oldContainer = element["parentElement"];
1399 auto newContainer = getElementContainer(iface);
1400 if (!oldContainer.isUndefined() &&
1401 !oldContainer.isNull() &&
1402 oldContainer != newContainer) {
1403 oldContainer.call<void>("removeChild", element);
1404 }
1405 }
1406 for (int i = 0; i < iface->childCount(); ++i)
1407 unlinkParentForChildren(iface->child(i));
1408}
1409
1410void QWasmAccessibility::relinkParentForChildren(QAccessibleInterface *iface)
1411{
1412 auto element = getHtmlElement(iface);
1413 if (!element.isUndefined()) {
1414 if (element["parentElement"].isUndefined() ||
1415 element["parentElement"].isNull()) {
1416 linkToParent(iface);
1417 }
1418 }
1419 for (int i = 0; i < iface->childCount(); ++i)
1420 relinkParentForChildren(iface->child(i));
1421}
1422
1423void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event)
1424{
1425 if (handleUpdateByEventType(event))
1426 handleUpdateByInterfaceRole(event);
1427}
1428
1429bool QWasmAccessibility::handleUpdateByEventType(QAccessibleEvent *event)
1430{
1431 if (!isActive())
1432 return false;
1433
1434 QAccessibleInterface *iface = event->accessibleInterface();
1435 if (!iface) {
1436 qWarning() << "handleUpdateByEventType with null a11y interface" << event->type() << event->object();
1437 return false;
1438 }
1439
1440 // Handle event types that creates/removes objects.
1441 switch (event->type()) {
1442 case QAccessible::ObjectCreated:
1443 // Do nothing, there are too many changes to the interface
1444 // before ObjectShow is called
1445 return false;
1446
1447 case QAccessible::ObjectDestroyed:
1448 // The object might be under destruction, and the interface not valid.
1449 removeObject(event->uniqueId());
1450 return false;
1451
1452 case QAccessible::ObjectShow: // We do not get ObjectCreated from widgets, we get ObjectShow
1453 createObject(iface);
1454 break;
1455
1456 case QAccessible::ParentChanged:
1457 unlinkParentForChildren(iface);
1458 relinkParentForChildren(iface);
1459 break;
1460
1461 default:
1462 break;
1463 };
1464
1465 // Qt focuses widgets before the window is shown, so the element may not exist
1466 // yet. Remember the focus, ObjectShow applies it once the element is there.
1467 if (event->type() == QAccessible::Focus)
1468 m_focusId = event->uniqueId();
1469
1470 if (getHtmlElement(iface).isUndefined())
1471 return false;
1472
1473 // Handle some common event types. See
1474 // https://doc.qt.io/qt-6/qaccessible.html#Event-enum
1475 switch (event->type()) {
1476 case QAccessible::Announcement:
1477 handleAnnouncement(iface, static_cast<QAccessibleAnnouncementEvent *>(event));
1478 return false;
1479
1480 case QAccessible::StateChanged: {
1481 QAccessibleStateChangeEvent *stateChangeEvent = (QAccessibleStateChangeEvent *)event;
1482 if (stateChangeEvent->changedStates().disabled)
1483 setHtmlElementDisabled(iface);
1484 } break;
1485
1486 case QAccessible::DescriptionChanged:
1487 handleDescriptionChanged(iface);
1488 return false;
1489
1490 case QAccessible::Focus:
1491 // We do not get all callbacks for the geometry
1492 // hence we update here as well.
1493 setHtmlElementGeometry(iface);
1494 // The focus event may arrive after the focus has moved on to a child
1495 // window: QWidget::setFocus() sends it after the FocusIn event, which a
1496 // window container forwards to its child window. Moving the dom focus
1497 // back would take the focus from the child window.
1498 if (!isFocusInChildWindow(iface))
1499 setHtmlElementFocus(iface);
1500 break;
1501
1502 case QAccessible::IdentifierChanged:
1503 handleIdentifierUpdate(iface);
1504 return false;
1505
1506 case QAccessible::ObjectShow:
1507 linkToParent(iface);
1508 setHtmlElementVisibility(iface, true);
1509
1510 // Sync up properties on show;
1511 setHtmlElementGeometry(iface);
1512 sendEvent(iface, QAccessible::NameChanged);
1513
1514 // Qt focuses the element before it is shown, so apply the pending focus
1515 // once the element is in the dom.
1516 if (QAccessible::uniqueId(iface) == m_focusId)
1517 setHtmlElementFocus(iface);
1518 break;
1519
1520 case QAccessible::ObjectHide:
1521 // Do not call linkToParent, only set visibility to false
1522 setHtmlElementVisibility(iface, false);
1523 return false;
1524
1525 case QAccessible::LocationChanged:
1526 setHtmlElementGeometry(iface);
1527 return false;
1528
1529 // TODO: maybe handle more types here
1530 default:
1531 break;
1532 };
1533
1534 return true;
1535}
1536
1537void QWasmAccessibility::handleUpdateByInterfaceRole(QAccessibleEvent *event)
1538{
1539 if (!isActive())
1540 return;
1541
1542 QAccessibleInterface *iface = event->accessibleInterface();
1543 if (!iface) {
1544 qWarning() << "handleUpdateByInterfaceRole with null a11y interface" << event->type() << event->object();
1545 return;
1546 }
1547
1548 // Switch on interface role, see
1549 // https://doc.qt.io/qt-5/qaccessibleinterface.html#role
1550 switch (iface->role()) {
1551 case QAccessible::List:
1552 break;
1553 case QAccessible::ListItem:
1554 handleListItemUpdate(event);
1555 break;
1556 case QAccessible::StaticText:
1557 handleStaticTextUpdate(event);
1558 break;
1559 case QAccessible::Button:
1560 handleButtonUpdate(event);
1561 break;
1562 case QAccessible::CheckBox:
1563 if (iface->childCount() > 0)
1564 handleGroupBoxUpdate(event);
1565 else
1566 handleCheckBoxUpdate(event);
1567 break;
1568 case QAccessible::Switch:
1569 handleSwitchUpdate(event);
1570 break;
1571 case QAccessible::EditableText:
1572 handleLineEditUpdate(event);
1573 break;
1574 case QAccessible::Dialog:
1575 case QAccessible::AlertMessage:
1576 handleDialogUpdate(event);
1577 break;
1578 case QAccessible::MenuItem:
1579 case QAccessible::MenuBar:
1580 case QAccessible::PopupMenu:
1581 handleMenuUpdate(event);
1582 break;
1583 case QAccessible::ToolBar:
1584 case QAccessible::ButtonMenu:
1585 handleToolUpdate(event);
1586 break;
1587 case QAccessible::RadioButton:
1588 handleRadioButtonUpdate(event);
1589 break;
1590 case QAccessible::Dial:
1591 handleDialUpdate(event);
1592 break;
1593 case QAccessible::SpinBox:
1594 handleSpinBoxUpdate(event);
1595 break;
1596 case QAccessible::Slider:
1597 handleSliderUpdate(event);
1598 break;
1599 case QAccessible::PageTab:
1600 handlePageTabUpdate(event);
1601 break;
1602 case QAccessible::PageTabList:
1603 handlePageTabListUpdate(event);
1604 break;
1605 case QAccessible::ScrollBar:
1606 handleScrollBarUpdate(event);
1607 break;
1608 case QAccessible::Grouping:
1609 handleGroupBoxUpdate(event);
1610 break;
1611 case QAccessible::ProgressBar:
1612 handleProgressBarUpdate(event);
1613 break;
1614 default:
1615 qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role();
1616 };
1617}
1618
1619#endif // QT_CONFIG(accessibility)
void QWasmAccessibilityEnable()