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