From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- accessible/generic/ARIAGridAccessible.cpp | 77 + accessible/generic/ARIAGridAccessible.h | 35 + accessible/generic/ApplicationAccessible.cpp | 142 + accessible/generic/ApplicationAccessible.h | 109 + accessible/generic/BaseAccessibles.cpp | 142 + accessible/generic/BaseAccessibles.h | 136 + accessible/generic/DocAccessible-inl.h | 183 ++ accessible/generic/DocAccessible.cpp | 2847 +++++++++++++++++ accessible/generic/DocAccessible.h | 825 +++++ accessible/generic/FormControlAccessible.cpp | 83 + accessible/generic/FormControlAccessible.h | 65 + accessible/generic/HyperTextAccessible-inl.h | 48 + accessible/generic/HyperTextAccessible.cpp | 1141 +++++++ accessible/generic/HyperTextAccessible.h | 266 ++ accessible/generic/ImageAccessible.cpp | 260 ++ accessible/generic/ImageAccessible.h | 94 + accessible/generic/LocalAccessible-inl.h | 107 + accessible/generic/LocalAccessible.cpp | 4255 ++++++++++++++++++++++++++ accessible/generic/LocalAccessible.h | 1028 +++++++ accessible/generic/OuterDocAccessible.cpp | 224 ++ accessible/generic/OuterDocAccessible.h | 80 + accessible/generic/RootAccessible.cpp | 706 +++++ accessible/generic/RootAccessible.h | 93 + accessible/generic/TextLeafAccessible.cpp | 42 + accessible/generic/TextLeafAccessible.h | 46 + accessible/generic/moz.build | 63 + 26 files changed, 13097 insertions(+) create mode 100644 accessible/generic/ARIAGridAccessible.cpp create mode 100644 accessible/generic/ARIAGridAccessible.h create mode 100644 accessible/generic/ApplicationAccessible.cpp create mode 100644 accessible/generic/ApplicationAccessible.h create mode 100644 accessible/generic/BaseAccessibles.cpp create mode 100644 accessible/generic/BaseAccessibles.h create mode 100644 accessible/generic/DocAccessible-inl.h create mode 100644 accessible/generic/DocAccessible.cpp create mode 100644 accessible/generic/DocAccessible.h create mode 100644 accessible/generic/FormControlAccessible.cpp create mode 100644 accessible/generic/FormControlAccessible.h create mode 100644 accessible/generic/HyperTextAccessible-inl.h create mode 100644 accessible/generic/HyperTextAccessible.cpp create mode 100644 accessible/generic/HyperTextAccessible.h create mode 100644 accessible/generic/ImageAccessible.cpp create mode 100644 accessible/generic/ImageAccessible.h create mode 100644 accessible/generic/LocalAccessible-inl.h create mode 100644 accessible/generic/LocalAccessible.cpp create mode 100644 accessible/generic/LocalAccessible.h create mode 100644 accessible/generic/OuterDocAccessible.cpp create mode 100644 accessible/generic/OuterDocAccessible.h create mode 100644 accessible/generic/RootAccessible.cpp create mode 100644 accessible/generic/RootAccessible.h create mode 100644 accessible/generic/TextLeafAccessible.cpp create mode 100644 accessible/generic/TextLeafAccessible.h create mode 100644 accessible/generic/moz.build (limited to 'accessible/generic') diff --git a/accessible/generic/ARIAGridAccessible.cpp b/accessible/generic/ARIAGridAccessible.cpp new file mode 100644 index 0000000000..58e49bee1f --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ARIAGridAccessible.h" + +#include +#include "LocalAccessible-inl.h" +#include "AccAttributes.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsGkAtoms.h" +#include "mozilla/a11y/Role.h" +#include "States.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ARIAGridCellAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Constructor + +ARIAGridCellAccessible::ARIAGridCellAccessible(nsIContent* aContent, + DocAccessible* aDoc) + : HyperTextAccessible(aContent, aDoc) { + mGenericTypes |= eTableCell; +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void ARIAGridCellAccessible::ApplyARIAState(uint64_t* aState) const { + HyperTextAccessible::ApplyARIAState(aState); + + // Return if the gridcell has aria-selected="true". + if (*aState & states::SELECTED) return; + + // Check aria-selected="true" on the row. + LocalAccessible* row = LocalParent(); + if (!row || row->Role() != roles::ROW) return; + + nsIContent* rowContent = row->GetContent(); + if (nsAccUtils::HasDefinedARIAToken(rowContent, nsGkAtoms::aria_selected) && + !nsAccUtils::ARIAAttrValueIs(rowContent->AsElement(), + nsGkAtoms::aria_selected, nsGkAtoms::_false, + eCaseMatters)) { + *aState |= states::SELECTABLE | states::SELECTED; + } +} + +already_AddRefed ARIAGridCellAccessible::NativeAttributes() { + RefPtr attributes = HyperTextAccessible::NativeAttributes(); + + // We only need to expose table-cell-index to clients. If we're in the content + // process, we don't need this, so building a CachedTableAccessible is very + // wasteful. This will be exposed by RemoteAccessible in the parent process + // instead. + if (!IPCAccessibilityActive()) { + if (const TableCellAccessible* cell = AsTableCell()) { + TableAccessible* table = cell->Table(); + const uint32_t row = cell->RowIdx(); + const uint32_t col = cell->ColIdx(); + const int32_t cellIdx = table->CellIndexAt(row, col); + if (cellIdx != -1) { + attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx); + } + } + } + + return attributes.forget(); +} diff --git a/accessible/generic/ARIAGridAccessible.h b/accessible/generic/ARIAGridAccessible.h new file mode 100644 index 0000000000..35590b0446 --- /dev/null +++ b/accessible/generic/ARIAGridAccessible.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_A11Y_ARIAGridAccessible_h_ +#define MOZILLA_A11Y_ARIAGridAccessible_h_ + +#include "HyperTextAccessible.h" + +namespace mozilla { +namespace a11y { + +/** + * Accessible for ARIA gridcell and rowheader/columnheader. + */ +class ARIAGridCellAccessible : public HyperTextAccessible { + public: + ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridCellAccessible, + HyperTextAccessible) + + // LocalAccessible + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual already_AddRefed NativeAttributes() override; + + protected: + virtual ~ARIAGridCellAccessible() {} +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/ApplicationAccessible.cpp b/accessible/generic/ApplicationAccessible.cpp new file mode 100644 index 0000000000..90539cf2bd --- /dev/null +++ b/accessible/generic/ApplicationAccessible.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ApplicationAccessible.h" + +#include "LocalAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "Relation.h" +#include "mozilla/a11y/Role.h" +#include "States.h" + +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Components.h" +#include "nsGlobalWindowOuter.h" +#include "nsIStringBundle.h" + +using namespace mozilla::a11y; + +ApplicationAccessible::ApplicationAccessible() + : AccessibleWrap(nullptr, nullptr) { + mType = eApplicationType; + mAppInfo = do_GetService("@mozilla.org/xre/app-info;1"); + MOZ_ASSERT(mAppInfo, "no application info"); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag ApplicationAccessible::Name(nsString& aName) const { + aName.Truncate(); + + nsCOMPtr bundleService = + mozilla::components::StringBundle::Service(); + + NS_ASSERTION(bundleService, "String bundle service must be present!"); + if (!bundleService) return eNameOK; + + nsCOMPtr bundle; + nsresult rv = bundleService->CreateBundle( + "chrome://branding/locale/brand.properties", getter_AddRefs(bundle)); + if (NS_FAILED(rv)) return eNameOK; + + nsAutoString appName; + rv = bundle->GetStringFromName("brandShortName", appName); + if (NS_FAILED(rv) || appName.IsEmpty()) { + NS_WARNING("brandShortName not found, using default app name"); + appName.AssignLiteral("Gecko based application"); + } + + aName.Assign(appName); + return eNameOK; +} + +void ApplicationAccessible::Description(nsString& aDescription) const { + aDescription.Truncate(); +} + +void ApplicationAccessible::Value(nsString& aValue) const { aValue.Truncate(); } + +uint64_t ApplicationAccessible::State() { + return IsDefunct() ? states::DEFUNCT : 0; +} + +already_AddRefed ApplicationAccessible::NativeAttributes() { + RefPtr attributes = new AccAttributes(); + return attributes.forget(); +} + +GroupPos ApplicationAccessible::GroupPosition() { return GroupPos(); } + +LocalAccessible* ApplicationAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + return nullptr; +} + +Accessible* ApplicationAccessible::FocusedChild() { + LocalAccessible* focus = FocusMgr()->FocusedLocalAccessible(); + if (focus && focus->LocalParent() == this) { + return focus; + } + + return nullptr; +} + +Relation ApplicationAccessible::RelationByType( + RelationType aRelationType) const { + return Relation(); +} + +mozilla::LayoutDeviceIntRect ApplicationAccessible::Bounds() const { + return mozilla::LayoutDeviceIntRect(); +} + +nsRect ApplicationAccessible::BoundsInAppUnits() const { return nsRect(); } + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible public methods + +void ApplicationAccessible::Shutdown() { mAppInfo = nullptr; } + +void ApplicationAccessible::ApplyARIAState(uint64_t* aState) const {} + +role ApplicationAccessible::NativeRole() const { return roles::APP_ROOT; } + +uint64_t ApplicationAccessible::NativeState() const { return 0; } + +KeyBinding ApplicationAccessible::AccessKey() const { return KeyBinding(); } + +void ApplicationAccessible::Init() { + // Basically children are kept updated by Append/RemoveChild method calls. + // However if there are open windows before accessibility was started + // then we need to make sure root accessibles for open windows are created so + // that all root accessibles are stored in application accessible children + // array. + + nsGlobalWindowOuter::OuterWindowByIdTable* windowsById = + nsGlobalWindowOuter::GetWindowsTable(); + + if (!windowsById) { + return; + } + + for (const auto& window : windowsById->Values()) { + if (window->GetDocShell() && window->IsRootOuterWindow()) { + if (RefPtr docNode = window->GetExtantDoc()) { + GetAccService()->GetDocAccessible(docNode); // ensure creation + } + } + } +} + +LocalAccessible* ApplicationAccessible::GetSiblingAtOffset( + int32_t aOffset, nsresult* aError) const { + if (aError) *aError = NS_OK; // fail peacefully + + return nullptr; +} diff --git a/accessible/generic/ApplicationAccessible.h b/accessible/generic/ApplicationAccessible.h new file mode 100644 index 0000000000..1b21ca8e8b --- /dev/null +++ b/accessible/generic/ApplicationAccessible.h @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_ApplicationAccessible_h__ +#define mozilla_a11y_ApplicationAccessible_h__ + +#include "AccessibleWrap.h" + +#include "nsIXULAppInfo.h" + +namespace mozilla { +namespace a11y { + +/** + * ApplicationAccessible is for the whole application of Mozilla. + * Only one instance of ApplicationAccessible exists for one Mozilla instance. + * And this one should be created when Mozilla Startup (if accessibility + * feature has been enabled) and destroyed when Mozilla Shutdown. + * + * All the accessibility objects for toplevel windows are direct children of + * the ApplicationAccessible instance. + */ + +class ApplicationAccessible : public AccessibleWrap { + public: + ApplicationAccessible(); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ApplicationAccessible, AccessibleWrap) + + // LocalAccessible + virtual void Shutdown() override; + virtual LayoutDeviceIntRect Bounds() const override; + virtual nsRect BoundsInAppUnits() const override; + virtual already_AddRefed NativeAttributes() override; + virtual GroupPos GroupPosition() override; + virtual ENameValueFlag Name(nsString& aName) const override; + virtual void ApplyARIAState(uint64_t* aState) const override; + virtual void Description(nsString& aDescription) const override; + virtual void Value(nsString& aValue) const override; + virtual mozilla::a11y::role NativeRole() const override; + virtual uint64_t State() override; + virtual uint64_t NativeState() const override; + virtual Relation RelationByType(RelationType aType) const override; + + virtual LocalAccessible* LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override; + virtual Accessible* FocusedChild() override; + + // ActionAccessible + virtual KeyBinding AccessKey() const override; + + // ApplicationAccessible + void Init(); + + void AppName(nsAString& aName) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cname; + mAppInfo->GetName(cname); + AppendUTF8toUTF16(cname, aName); + } + } + + void AppVersion(nsAString& aVersion) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cversion; + mAppInfo->GetVersion(cversion); + AppendUTF8toUTF16(cversion, aVersion); + } + } + + void PlatformName(nsAString& aName) const { aName.AssignLiteral("Gecko"); } + + void PlatformVersion(nsAString& aVersion) const { + MOZ_ASSERT(mAppInfo, "no application info"); + + if (mAppInfo) { + nsAutoCString cversion; + mAppInfo->GetPlatformVersion(cversion); + AppendUTF8toUTF16(cversion, aVersion); + } + } + + protected: + virtual ~ApplicationAccessible() {} + + // LocalAccessible + virtual LocalAccessible* GetSiblingAtOffset( + int32_t aOffset, nsresult* aError = nullptr) const override; + + private: + nsCOMPtr mAppInfo; +}; + +inline ApplicationAccessible* LocalAccessible::AsApplication() { + return IsApplication() ? static_cast(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/BaseAccessibles.cpp b/accessible/generic/BaseAccessibles.cpp new file mode 100644 index 0000000000..520f54e96b --- /dev/null +++ b/accessible/generic/BaseAccessibles.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BaseAccessibles.h" + +#include "States.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// LeafAccessible +//////////////////////////////////////////////////////////////////////////////// + +LeafAccessible::LeafAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) { + mStateFlags |= eNoKidsFromDOM; +} + +//////////////////////////////////////////////////////////////////////////////// +// LeafAccessible: LocalAccessible public + +LocalAccessible* LeafAccessible::LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { + // Don't walk into leaf accessibles. + return this; +} + +bool LeafAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) { + MOZ_ASSERT_UNREACHABLE("InsertChildAt called on leaf accessible!"); + return false; +} + +bool LeafAccessible::RemoveChild(LocalAccessible* aChild) { + MOZ_ASSERT_UNREACHABLE("RemoveChild called on leaf accessible!"); + return false; +} + +bool LeafAccessible::IsAcceptableChild(nsIContent* aEl) const { + // No children for leaf accessible. + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// LinkableAccessible. nsIAccessible + +void LinkableAccessible::TakeFocus() const { + if (const LocalAccessible* actionAcc = ActionWalk()) { + actionAcc->TakeFocus(); + } else { + AccessibleWrap::TakeFocus(); + } +} + +uint64_t LinkableAccessible::NativeLinkState() const { + bool isLink; + const LocalAccessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + return states::LINKED | (actionAcc->LinkState() & states::TRAVERSED); + } + + return 0; +} + +void LinkableAccessible::Value(nsString& aValue) const { + aValue.Truncate(); + + LocalAccessible::Value(aValue); + if (!aValue.IsEmpty()) { + return; + } + + bool isLink; + const LocalAccessible* actionAcc = ActionWalk(&isLink); + if (isLink) { + actionAcc->Value(aValue); + } +} + +const LocalAccessible* LinkableAccessible::ActionWalk(bool* aIsLink, + bool* aIsOnclick) const { + if (aIsOnclick) { + *aIsOnclick = false; + } + if (aIsLink) { + *aIsLink = false; + } + + if (HasPrimaryAction()) { + if (aIsOnclick) { + *aIsOnclick = true; + } + + return nullptr; + } + + const Accessible* actionAcc = ActionAncestor(); + + const LocalAccessible* localAction = + actionAcc ? const_cast(actionAcc)->AsLocal() : nullptr; + + if (!localAction) { + return nullptr; + } + + if (localAction->LinkState() & states::LINKED) { + if (aIsLink) { + *aIsLink = true; + } + } else if (aIsOnclick) { + *aIsOnclick = true; + } + + return localAction; +} + +KeyBinding LinkableAccessible::AccessKey() const { + if (const LocalAccessible* actionAcc = + const_cast(this)->ActionWalk()) { + return actionAcc->AccessKey(); + } + + return LocalAccessible::AccessKey(); +} + +//////////////////////////////////////////////////////////////////////////////// +// DummyAccessible +//////////////////////////////////////////////////////////////////////////////// + +uint64_t DummyAccessible::NativeState() const { return 0; } +uint64_t DummyAccessible::NativeInteractiveState() const { return 0; } + +uint64_t DummyAccessible::NativeLinkState() const { return 0; } + +bool DummyAccessible::NativelyUnavailable() const { return false; } + +void DummyAccessible::ApplyARIAState(uint64_t* aState) const {} diff --git a/accessible/generic/BaseAccessibles.h b/accessible/generic/BaseAccessibles.h new file mode 100644 index 0000000000..e67d208786 --- /dev/null +++ b/accessible/generic/BaseAccessibles.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_BaseAccessibles_h__ +#define mozilla_a11y_BaseAccessibles_h__ + +#include "AccessibleWrap.h" +#include "HyperTextAccessible.h" + +class nsIContent; + +/** + * This file contains a number of classes that are used as base + * classes for the different accessibility implementations of + * the HTML and XUL widget sets. --jgaunt + */ + +namespace mozilla { +namespace a11y { + +/** + * Leaf version of DOM Accessible -- has no children + */ +class LeafAccessible : public AccessibleWrap { + public: + LeafAccessible(nsIContent* aContent, DocAccessible* aDoc); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(LeafAccessible, AccessibleWrap) + + // LocalAccessible + virtual LocalAccessible* LocalChildAtPoint( + int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override; + bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) final; + bool RemoveChild(LocalAccessible* aChild) final; + + virtual bool IsAcceptableChild(nsIContent* aEl) const override; + + protected: + virtual ~LeafAccessible() {} +}; + +/** + * Used for text or image accessible nodes contained by link accessibles or + * accessibles for nodes with registered click event handler. It knows how to + * report the state of the host link (traveled or not) and can focus the host + * accessible programmatically. + */ +class LinkableAccessible : public AccessibleWrap { + public: + LinkableAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + + NS_INLINE_DECL_REFCOUNTING_INHERITED(LinkableAccessible, AccessibleWrap) + + // LocalAccessible + virtual void Value(nsString& aValue) const override; + virtual uint64_t NativeLinkState() const override; + virtual void TakeFocus() const override; + + // ActionAccessible + virtual KeyBinding AccessKey() const override; + + // ActionAccessible helpers + const LocalAccessible* ActionWalk(bool* aIsLink = nullptr, + bool* aIsOnclick = nullptr) const; + + protected: + virtual ~LinkableAccessible() {} +}; + +/** + * A simple accessible that gets its enumerated role. + */ +template +class EnumRoleAccessible : public AccessibleWrap { + public: + EnumRoleAccessible(nsIContent* aContent, DocAccessible* aDoc) + : AccessibleWrap(aContent, aDoc) {} + + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aPtr) override { + return LocalAccessible::QueryInterface(aIID, aPtr); + } + + // LocalAccessible + virtual a11y::role NativeRole() const override { return R; } + + protected: + virtual ~EnumRoleAccessible() {} +}; + +/** + * Like EnumRoleAccessible, but with text support. + */ +template +class EnumRoleHyperTextAccessible : public HyperTextAccessible { + public: + EnumRoleHyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc) + : HyperTextAccessible(aContent, aDoc) {} + + // LocalAccessible + virtual a11y::role NativeRole() const override { return R; } + + protected: + virtual ~EnumRoleHyperTextAccessible() {} +}; + +/** + * A wrapper accessible around native accessible to connect it with + * crossplatform accessible tree. + */ +class DummyAccessible : public AccessibleWrap { + public: + explicit DummyAccessible(DocAccessible* aDocument = nullptr) + : AccessibleWrap(nullptr, aDocument) { + // IsDefunct() asserts if mContent is null, which is always true for + // DummyAccessible. We can prevent this by setting eSharedNode. + mStateFlags |= eSharedNode; + } + + uint64_t NativeState() const final; + uint64_t NativeInteractiveState() const final; + uint64_t NativeLinkState() const final; + bool NativelyUnavailable() const final; + void ApplyARIAState(uint64_t* aState) const final; + + protected: + virtual ~DummyAccessible() {} +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/DocAccessible-inl.h b/accessible/generic/DocAccessible-inl.h new file mode 100644 index 0000000000..2e358724cd --- /dev/null +++ b/accessible/generic/DocAccessible-inl.h @@ -0,0 +1,183 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_DocAccessible_inl_h_ +#define mozilla_a11y_DocAccessible_inl_h_ + +#include "DocAccessible.h" +#include "LocalAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "NotificationController.h" +#include "States.h" +#include "nsIScrollableFrame.h" +#include "mozilla/dom/DocumentInlines.h" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +inline LocalAccessible* DocAccessible::AccessibleOrTrueContainer( + nsINode* aNode, bool aNoContainerIfPruned) const { + // HTML comboboxes have no-content list accessible as an intermediate + // containing all options. + LocalAccessible* container = + GetAccessibleOrContainer(aNode, aNoContainerIfPruned); + if (container && container->IsHTMLCombobox()) { + return container->LocalFirstChild(); + } + return container; +} + +inline bool DocAccessible::IsContentLoaded() const { + // eDOMLoaded flag check is used for error pages as workaround to make this + // method return correct result since error pages do not receive 'pageshow' + // event and as consequence Document::IsShowing() returns false. + return mDocumentNode && mDocumentNode->IsVisible() && + (mDocumentNode->IsShowing() || HasLoadState(eDOMLoaded)); +} + +inline bool DocAccessible::IsHidden() const { return mDocumentNode->Hidden(); } + +inline void DocAccessible::FireDelayedEvent(AccEvent* aEvent) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) logging::DocLoadEventFired(aEvent); +#endif + + mNotificationController->QueueEvent(aEvent); +} + +inline void DocAccessible::FireDelayedEvent(uint32_t aEventType, + LocalAccessible* aTarget) { + RefPtr event = new AccEvent(aEventType, aTarget); + FireDelayedEvent(event); +} + +inline void DocAccessible::BindChildDocument(DocAccessible* aDocument) { + mNotificationController->ScheduleChildDocBinding(aDocument); +} + +template +inline void DocAccessible::HandleNotification( + Class* aInstance, typename TNotification::Callback aMethod, + Args*... aArgs) { + if (mNotificationController) { + mNotificationController->HandleNotification( + aInstance, aMethod, aArgs...); + } +} + +inline void DocAccessible::UpdateText(nsIContent* aTextNode) { + NS_ASSERTION(mNotificationController, "The document was shut down!"); + + // Ignore the notification if initial tree construction hasn't been done yet. + if (mNotificationController && HasLoadState(eTreeConstructed)) { + mNotificationController->ScheduleTextUpdate(aTextNode); + } +} + +inline void DocAccessible::NotifyOfLoad(uint32_t aLoadEventType) { + mLoadState |= eDOMLoaded; + mLoadEventType = aLoadEventType; + + // If the document is loaded completely then network activity was presumingly + // caused by file loading. Fire busy state change event. + if (HasLoadState(eCompletelyLoaded) && IsLoadEventTarget()) { + RefPtr stateEvent = + new AccStateChangeEvent(this, states::BUSY, false); + FireDelayedEvent(stateEvent); + } +} + +inline void DocAccessible::MaybeNotifyOfValueChange( + LocalAccessible* aAccessible) { + if (aAccessible->IsCombobox() || aAccessible->Role() == roles::ENTRY || + aAccessible->Role() == roles::SPINBUTTON) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible); + } +} + +inline LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMapOrContainer( + nsINode* aNode) const { + LocalAccessible* acc = GetAccessibleEvenIfNotInMap(aNode); + return acc ? acc : GetContainerAccessible(aNode); +} + +inline void DocAccessible::CreateSubtree(LocalAccessible* aChild) { + // If a focused node has been shown then it could mean its frame was recreated + // while the node stays focused and we need to fire focus event on + // the accessible we just created. If the queue contains a focus event for + // this node already then it will be suppressed by this one. + LocalAccessible* focusedAcc = nullptr; + CacheChildrenInSubtree(aChild, &focusedAcc); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eVerbose)) { + logging::Tree("TREE", "Created subtree", aChild); + } +#endif + + // Fire events for ARIA elements. + if (aChild->HasARIARole()) { + roles::Role role = aChild->ARIARole(); + if (role == roles::MENUPOPUP) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild); + } else if (role == roles::ALERT) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild); + } + } + + // XXX: do we really want to send focus to focused DOM node not taking into + // account active item? + if (focusedAcc) { + FocusMgr()->DispatchFocusEvent(this, focusedAcc); + SelectionMgr()->SetControlSelectionListener( + focusedAcc->GetNode()->AsElement()); + } +} + +inline DocAccessible::AttrRelProviders* DocAccessible::GetRelProviders( + dom::Element* aElement, const nsAString& aID) const { + DependentIDsHashtable* hash = mDependentIDsHashes.Get( + aElement->GetUncomposedDocOrConnectedShadowRoot()); + if (hash) { + return hash->Get(aID); + } + return nullptr; +} + +inline DocAccessible::AttrRelProviders* DocAccessible::GetOrCreateRelProviders( + dom::Element* aElement, const nsAString& aID) { + dom::DocumentOrShadowRoot* docOrShadowRoot = + aElement->GetUncomposedDocOrConnectedShadowRoot(); + DependentIDsHashtable* hash = + mDependentIDsHashes.GetOrInsertNew(docOrShadowRoot); + + return hash->GetOrInsertNew(aID); +} + +inline void DocAccessible::RemoveRelProvidersIfEmpty(dom::Element* aElement, + const nsAString& aID) { + dom::DocumentOrShadowRoot* docOrShadowRoot = + aElement->GetUncomposedDocOrConnectedShadowRoot(); + DependentIDsHashtable* hash = mDependentIDsHashes.Get(docOrShadowRoot); + if (hash) { + AttrRelProviders* providers = hash->Get(aID); + if (providers && providers->Length() == 0) { + hash->Remove(aID); + if (mDependentIDsHashes.IsEmpty()) { + mDependentIDsHashes.Remove(docOrShadowRoot); + } + } + } +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp new file mode 100644 index 0000000000..28ed8bcbb4 --- /dev/null +++ b/accessible/generic/DocAccessible.cpp @@ -0,0 +1,2847 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LocalAccessible-inl.h" +#include "AccIterator.h" +#include "AccAttributes.h" +#include "CachedTableAccessible.h" +#include "DocAccessible-inl.h" +#include "EventTree.h" +#include "HTMLImageMapAccessible.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccUtils.h" +#include "nsEventShell.h" +#include "nsIIOService.h" +#include "nsLayoutUtils.h" +#include "nsTextEquivUtils.h" +#include "mozilla/a11y/Role.h" +#include "TreeWalker.h" +#include "xpcAccessibleDocument.h" + +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIContentInlines.h" +#include "nsIEditingSession.h" +#include "nsIFrame.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsImageFrame.h" +#include "nsViewManager.h" +#include "nsIScrollableFrame.h" +#include "nsIURI.h" +#include "nsIWebNavigation.h" +#include "nsFocusManager.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Components.h" // for mozilla::components +#include "mozilla/EditorBase.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PerfStats.h" +#include "mozilla/PresShell.h" +#include "nsAccessibilityService.h" +#include "mozilla/a11y/DocAccessibleChild.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/DocumentType.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/UserActivation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Static member initialization + +static nsStaticAtom* const kRelationAttrs[] = { + nsGkAtoms::aria_labelledby, nsGkAtoms::aria_describedby, + nsGkAtoms::aria_details, nsGkAtoms::aria_owns, + nsGkAtoms::aria_controls, nsGkAtoms::aria_flowto, + nsGkAtoms::aria_errormessage, nsGkAtoms::_for, + nsGkAtoms::control, nsGkAtoms::popovertarget}; + +static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/desctructor + +DocAccessible::DocAccessible(dom::Document* aDocument, + PresShell* aPresShell) + : // XXX don't pass a document to the LocalAccessible constructor so that + // we don't set mDoc until our vtable is fully setup. If we set mDoc + // before setting up the vtable we will call LocalAccessible::AddRef() + // but not the overrides of it for subclasses. It is important to call + // those overrides to avoid confusing leak checking machinary. + HyperTextAccessible(nullptr, nullptr), + // XXX aaronl should we use an algorithm for the initial cache size? + mAccessibleCache(kDefaultCacheLength), + mNodeToAccessibleMap(kDefaultCacheLength), + mDocumentNode(aDocument), + mLoadState(eTreeConstructionPending), + mDocFlags(0), + mViewportCacheDirty(false), + mLoadEventType(0), + mPrevStateBits(0), + mPresShell(aPresShell), + mIPCDoc(nullptr) { + mGenericTypes |= eDocument; + mStateFlags |= eNotNodeMapEntry; + mDoc = this; + + MOZ_ASSERT(mPresShell, "should have been given a pres shell"); + mPresShell->SetDocAccessible(this); +} + +DocAccessible::~DocAccessible() { + NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, + LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) + for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) { + for (const auto& providers : hashEntry->Values()) { + for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + cb, "content of dependent ids hash entry of document accessible"); + + const auto& provider = (*providers)[provIdx]; + cb.NoteXPCOMChild(provider->mContent); + } + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates) + for (const auto& ar : tmp->mARIAOwnsHash.Values()) { + for (uint32_t i = 0; i < ar->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item"); + cb.NoteXPCOMChild(ar->ElementAt(i)); + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) + tmp->mDependentIDsHashes.Clear(); + tmp->mNodeToAccessibleMap.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + tmp->mARIAOwnsHash.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible) + +NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible) +NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible) + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag DocAccessible::Name(nsString& aName) const { + aName.Truncate(); + + if (mParent) { + mParent->Name(aName); // Allow owning iframe to override the name + } + if (aName.IsEmpty()) { + // Allow name via aria-labelledby or title attribute + LocalAccessible::Name(aName); + } + if (aName.IsEmpty()) { + Title(aName); // Try title element + } + if (aName.IsEmpty()) { // Last resort: use URL + URL(aName); + } + + return eNameOK; +} + +// LocalAccessible public method +role DocAccessible::NativeRole() const { + nsCOMPtr docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); + if (docShell) { + nsCOMPtr sameTypeRoot; + docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); + int32_t itemType = docShell->ItemType(); + if (sameTypeRoot == docShell) { + // Root of content or chrome tree + if (itemType == nsIDocShellTreeItem::typeChrome) { + return roles::CHROME_WINDOW; + } + + if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } else if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } + + return roles::PANE; // Fall back; +} + +void DocAccessible::Description(nsString& aDescription) const { + if (mParent) mParent->Description(aDescription); + + if (HasOwnContent() && aDescription.IsEmpty()) { + nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + } +} + +// LocalAccessible public method +uint64_t DocAccessible::NativeState() const { + // Document is always focusable. + uint64_t state = + states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl + if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED; + + // Expose stale state until the document is ready (DOM is loaded and tree is + // constructed). + if (!HasLoadState(eReady)) state |= states::STALE; + + // Expose state busy until the document and all its subdocuments is completely + // loaded. + if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY; + + nsIFrame* frame = GetFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors( + nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { + state |= states::INVISIBLE | states::OFFSCREEN; + } + + RefPtr editorBase = GetEditor(); + state |= editorBase ? states::EDITABLE : states::READONLY; + + return state; +} + +uint64_t DocAccessible::NativeInteractiveState() const { + // Document is always focusable. + return states::FOCUSABLE; +} + +bool DocAccessible::NativelyUnavailable() const { return false; } + +// LocalAccessible public method +void DocAccessible::ApplyARIAState(uint64_t* aState) const { + // Grab states from content element. + if (mContent) LocalAccessible::ApplyARIAState(aState); + + // Allow iframe/frame etc. to have final state override via ARIA. + if (mParent) mParent->ApplyARIAState(aState); +} + +Accessible* DocAccessible::FocusedChild() { + // Return an accessible for the current global focus, which does not have to + // be contained within the current document. + return FocusMgr()->FocusedAccessible(); +} + +void DocAccessible::TakeFocus() const { + // Focus the document. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + RefPtr newFocus; + dom::AutoHandlingUserInputStatePusher inputStatePusher(true); + fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, + nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus)); +} + +// HyperTextAccessible method +already_AddRefed DocAccessible::GetEditor() const { + // Check if document is editable (designMode="on" case). Otherwise check if + // the html:body (for HTML document case) or document element is editable. + if (!mDocumentNode->IsInDesignMode() && + (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) { + return nullptr; + } + + nsCOMPtr docShell = mDocumentNode->GetDocShell(); + if (!docShell) { + return nullptr; + } + + nsCOMPtr editingSession; + docShell->GetEditingSession(getter_AddRefs(editingSession)); + if (!editingSession) return nullptr; // No editing session interface + + RefPtr htmlEditor = + editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow()); + if (!htmlEditor) { + return nullptr; + } + + bool isEditable = false; + htmlEditor->GetIsDocumentEditable(&isEditable); + if (isEditable) { + return htmlEditor.forget(); + } + + return nullptr; +} + +// DocAccessible public method + +void DocAccessible::URL(nsAString& aURL) const { + aURL.Truncate(); + nsCOMPtr container = mDocumentNode->GetContainer(); + nsCOMPtr webNav(do_GetInterface(container)); + if (MOZ_UNLIKELY(!webNav)) { + return; + } + + nsCOMPtr uri; + webNav->GetCurrentURI(getter_AddRefs(uri)); + if (MOZ_UNLIKELY(!uri)) { + return; + } + // Let's avoid treating too long URI in the main process for avoiding + // memory fragmentation as far as possible. + if (uri->SchemeIs("data") || uri->SchemeIs("blob")) { + return; + } + + nsCOMPtr io = mozilla::components::IO::Service(); + if (NS_WARN_IF(!io)) { + return; + } + nsCOMPtr exposableURI; + if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) || + MOZ_UNLIKELY(!exposableURI)) { + return; + } + nsAutoCString theURL; + if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) { + CopyUTF8toUTF16(theURL, aURL); + } +} + +void DocAccessible::Title(nsString& aTitle) const { + mDocumentNode->GetTitle(aTitle); +} + +void DocAccessible::MimeType(nsAString& aType) const { + mDocumentNode->GetContentType(aType); +} + +void DocAccessible::DocType(nsAString& aType) const { + dom::DocumentType* docType = mDocumentNode->GetDoctype(); + if (docType) docType->GetPublicId(aType); +} + +void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc, + uint64_t aNewDomain) { + if (!mIPCDoc) { + return; + } + // These strong references aren't necessary because WithEntryHandle is + // guaranteed to run synchronously. However, static analysis complains without + // them. + RefPtr self = this; + RefPtr acc = aAcc; + size_t arrayIndex = + mQueuedCacheUpdatesHash.WithEntryHandle(aAcc, [self, acc](auto&& entry) { + if (entry.HasEntry()) { + // This LocalAccessible has already been queued. Return its index in + // the queue array so we can update its queued domains. + return entry.Data(); + } + // Add this LocalAccessible to the queue array. + size_t index = self->mQueuedCacheUpdatesArray.Length(); + self->mQueuedCacheUpdatesArray.EmplaceBack(std::make_pair(acc, 0)); + // Also add it to the hash map so we can avoid processing the same + // LocalAccessible twice. + return entry.Insert(index); + }); + auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex]; + MOZ_ASSERT(arrayAcc == aAcc); + domain |= aNewDomain; + Controller()->ScheduleProcessing(); +} + +void DocAccessible::QueueCacheUpdateForDependentRelations( + LocalAccessible* aAcc) { + if (!mIPCDoc || !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() || + aAcc->IsDefunct()) { + return; + } + nsAutoString ID; + aAcc->DOMNodeID(ID); + if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) { + // We call this function when we've noticed an ID change, or when an acc + // is getting bound to its document. We need to ensure any existing accs + // that depend on this acc's ID have their rel cache entries updated. + for (const auto& provider : *list) { + LocalAccessible* relatedAcc = GetAccessible(provider->mContent); + if (!relatedAcc || relatedAcc->IsDefunct() || + !relatedAcc->IsInDocument() || + mInsertedAccessibles.Contains(relatedAcc)) { + continue; + } + QueueCacheUpdate(relatedAcc, CacheDomain::Relations); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void DocAccessible::Init() { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("document initialize", mDocumentNode, this); + } +#endif + + // Initialize notification controller. + mNotificationController = new NotificationController(this, mPresShell); + + // Mark the DocAccessible as loaded if its DOM document is already loaded at + // this point. This can happen for one of three reasons: + // 1. A11y was started late. + // 2. DOM loading for a document (probably an in-process iframe) completed + // before its Accessible container was created. + // 3. The PresShell for the document was created after DOM loading completed. + // In that case, we tried to create the DocAccessible when DOM loading + // completed, but we can't create a DocAccessible without a PresShell, so + // this failed. The DocAccessible was subsequently created due to a layout + // notification. + if (mDocumentNode->GetReadyStateEnum() == + dom::Document::READYSTATE_COMPLETE) { + mLoadState |= eDOMLoaded; + // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a + // doc load complete event. If it happened due to reason 3, we need to fire + // doc load complete because clients (especially tests) might be waiting + // for the document to load using this event. We can't distinguish why this + // happened at this point, so just fire it regardless. It won't do any + // harm even if it isn't necessary. We set mLoadEventType here and it will + // be fired in ProcessLoad as usual. + mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; + } else if (mDocumentNode->IsInitialDocument()) { + // The initial about:blank document will never finish loading, so we can + // immediately mark it loaded to avoid waiting for its load. + mLoadState |= eDOMLoaded; + } + + AddEventListeners(); +} + +void DocAccessible::Shutdown() { + if (!mPresShell) { // already shutdown + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("document shutdown", mDocumentNode, this); + } +#endif + + // Mark the document as shutdown before AT is notified about the document + // removal from its container (valid for root documents on ATK and due to + // some reason for MSAA, refer to bug 757392 for details). + mStateFlags |= eIsDefunct; + + if (mNotificationController) { + mNotificationController->Shutdown(); + mNotificationController = nullptr; + } + + RemoveEventListeners(); + + // mParent->RemoveChild clears mParent, but we need to know whether we were a + // child later, so use a flag. + const bool isChild = !!mParent; + if (mParent) { + DocAccessible* parentDocument = mParent->Document(); + if (parentDocument) parentDocument->RemoveChildDocument(this); + + mParent->RemoveChild(this); + MOZ_ASSERT(!mParent, "Parent has to be null!"); + } + + mPresShell->SetDocAccessible(nullptr); + mPresShell = nullptr; // Avoid reentrancy + + // Walk the array backwards because child documents remove themselves from the + // array as they are shutdown. + int32_t childDocCount = mChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + mChildDocuments[idx]->Shutdown(); + } + + mChildDocuments.Clear(); + // mQueuedCacheUpdates* can contain a reference to this document (ex. if the + // doc is scrollable and we're sending a scroll position update). Clear the + // map here to avoid creating ref cycles. + mQueuedCacheUpdatesArray.Clear(); + mQueuedCacheUpdatesHash.Clear(); + + // XXX thinking about ordering? + if (mIPCDoc) { + MOZ_ASSERT(IPCAccessibilityActive()); + mIPCDoc->Shutdown(); + MOZ_ASSERT(!mIPCDoc); + } + + mDependentIDsHashes.Clear(); + mNodeToAccessibleMap.Clear(); + + mAnchorJumpElm = nullptr; + mInvalidationList.Clear(); + mPendingUpdates.Clear(); + + for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) { + LocalAccessible* accessible = iter.Data(); + MOZ_ASSERT(accessible); + if (accessible) { + // This might have been focused with FocusManager::ActiveItemChanged. In + // that case, we must notify FocusManager so that it clears the active + // item. Otherwise, it will hold on to a defunct Accessible. Normally, + // this happens in UnbindFromDocument, but we don't call that when the + // whole document shuts down. + if (FocusMgr()->WasLastFocused(accessible)) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("doc shutdown", accessible); + } +#endif + } + if (!accessible->IsDefunct()) { + // Unlink parent to avoid its cleaning overhead in shutdown. + accessible->mParent = nullptr; + accessible->Shutdown(); + } + } + iter.Remove(); + } + + HyperTextAccessible::Shutdown(); + + MOZ_ASSERT(GetAccService()); + GetAccService()->NotifyOfDocumentShutdown( + this, mDocumentNode, + // Make sure we don't shut down AccService while a parent document is + // still shutting down. The parent will allow service shutdown when it + // reaches this point. + /* aAllowServiceShutdown */ !isChild); + mDocumentNode = nullptr; +} + +nsIFrame* DocAccessible::GetFrame() const { + nsIFrame* root = nullptr; + if (mPresShell) { + root = mPresShell->GetRootFrame(); + } + + return root; +} + +nsINode* DocAccessible::GetNode() const { return mDocumentNode; } + +// DocAccessible protected member +nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const { + *aRelativeFrame = GetFrame(); + + dom::Document* document = mDocumentNode; + dom::Document* parentDoc = nullptr; + + nsRect bounds; + while (document) { + PresShell* presShell = document->GetPresShell(); + if (!presShell) { + return nsRect(); + } + + nsRect scrollPort; + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable(); + if (sf) { + scrollPort = sf->GetScrollPortRect(); + } else { + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) return nsRect(); + + scrollPort = rootFrame->GetRect(); + } + + if (parentDoc) { // After first time thru loop + // XXXroc bogus code! scrollPort is relative to the viewport of + // this document, but we're intersecting rectangles derived from + // multiple documents and assuming they're all in the same coordinate + // system. See bug 514117. + bounds.IntersectRect(scrollPort, bounds); + } else { // First time through loop + bounds = scrollPort; + } + + document = parentDoc = document->GetInProcessParentDocument(); + } + + return bounds; +} + +// DocAccessible protected member +nsresult DocAccessible::AddEventListeners() { + SelectionMgr()->AddDocSelectionListener(mPresShell); + + // Add document observer. + mDocumentNode->AddObserver(this); + return NS_OK; +} + +// DocAccessible protected member +nsresult DocAccessible::RemoveEventListeners() { + // Remove listeners associated with content documents + NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); + + if (mDocumentNode) { + mDocumentNode->RemoveObserver(this); + } + + if (mScrollWatchTimer) { + mScrollWatchTimer->Cancel(); + mScrollWatchTimer = nullptr; + NS_RELEASE_THIS(); // Kung fu death grip + } + + SelectionMgr()->RemoveDocSelectionListener(mPresShell); + return NS_OK; +} + +void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) { + DocAccessible* docAcc = reinterpret_cast(aClosure); + + if (docAcc) { + // Dispatch a scroll-end for all entries in table. They have not + // been scrolled in at least `kScrollEventInterval`. + for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done(); + iter.Next()) { + docAcc->DispatchScrollingEvent(iter.Key(), + nsIAccessibleEvent::EVENT_SCROLLING_END); + iter.Remove(); + } + + if (docAcc->mScrollWatchTimer) { + docAcc->mScrollWatchTimer = nullptr; + NS_RELEASE(docAcc); // Release kung fu death grip + } + } +} + +void DocAccessible::HandleScroll(nsINode* aTarget) { + nsINode* target = aTarget; + LocalAccessible* targetAcc = GetAccessible(target); + if (!targetAcc && target->IsInNativeAnonymousSubtree()) { + // The scroll event for textareas comes from a native anonymous div. We need + // the closest non-anonymous ancestor to get the right Accessible. + target = target->GetClosestNativeAnonymousSubtreeRootParentOrHost(); + targetAcc = GetAccessible(target); + } + // Regardless of our scroll timer, we need to send a cache update + // to ensure the next Bounds() query accurately reflects our position + // after scrolling. + if (targetAcc) { + QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition); + } + + const uint32_t kScrollEventInterval = 100; + // If we haven't dispatched a scrolling event for a target in at least + // kScrollEventInterval milliseconds, dispatch one now. + mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) { + const TimeStamp now = TimeStamp::Now(); + + if (!lastDispatch || + (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) { + // We can't fire events on a document whose tree isn't constructed yet. + if (HasLoadState(eTreeConstructed)) { + DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING); + } + lastDispatch.InsertOrUpdate(now); + } + }); + + // If timer callback is still pending, push it 100ms into the future. + // When scrolling ends and we don't fire this callback anymore, the + // timer callback will fire and dispatch an EVENT_SCROLLING_END. + if (mScrollWatchTimer) { + mScrollWatchTimer->SetDelay(kScrollEventInterval); + } else { + NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer), + ScrollTimerCallback, this, kScrollEventInterval, + nsITimer::TYPE_ONE_SHOT, + "a11y::DocAccessible::ScrollPositionDidChange"); + if (mScrollWatchTimer) { + NS_ADDREF_THIS(); // Kung fu death grip + } + } +} + +std::pair DocAccessible::ComputeScrollData( + LocalAccessible* aAcc) { + nsPoint scrollPoint; + nsRect scrollRange; + + if (nsIFrame* frame = aAcc->GetFrame()) { + nsIScrollableFrame* sf = aAcc == this + ? mPresShell->GetRootScrollFrameAsScrollable() + : frame->GetScrollTargetFrame(); + + // If there is no scrollable frame, it's likely a scroll in a popup, like + // in an + // iframe or shadow DOM. The root document itself doesn't receive these. + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr nstarget = window ? window->GetParentTarget() : nullptr; + + if (nstarget) { + for (const char *const *e = kEventTypes, *const *e_end = + ArrayEnd(kEventTypes); + e < e_end; ++e) { + nsresult rv = nstarget->AddEventListener(NS_ConvertASCIItoUTF16(*e), this, + true, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return DocAccessible::AddEventListeners(); +} + +nsresult RootAccessible::RemoveEventListeners() { + nsPIDOMWindowOuter* window = mDocumentNode->GetWindow(); + nsCOMPtr target = window ? window->GetParentTarget() : nullptr; + if (target) { + for (const char *const *e = kEventTypes, *const *e_end = + ArrayEnd(kEventTypes); + e < e_end; ++e) { + target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true); + } + } + + // Do this before removing clearing caret accessible, so that it can use + // shutdown the caret accessible's selection listener + DocAccessible::RemoveEventListeners(); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// public + +void RootAccessible::DocumentActivated(DocAccessible* aDocument) {} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +RootAccessible::HandleEvent(Event* aDOMEvent) { + MOZ_ASSERT(aDOMEvent); + if (IsDefunct()) { + // Even though we've been shut down, RemoveEventListeners might not have + // removed the event handlers on the window's parent target if GetWindow + // returned null, so we might still get events here in this case. We should + // just ignore these events. + return NS_OK; + } + + nsCOMPtr origTargetNode = + do_QueryInterface(aDOMEvent->GetOriginalTarget()); + if (!origTargetNode) return NS_OK; + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDOMEvents)) { + nsAutoString eventType; + aDOMEvent->GetType(eventType); + logging::DOMEvent("handled", origTargetNode, eventType); + } +#endif + + DocAccessible* document = + GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc()); + + if (document) { + nsAutoString eventType; + aDOMEvent->GetType(eventType); + if (eventType.EqualsLiteral("scroll")) { + // We don't put this in the notification queue for 2 reasons: + // 1. We will flood the queue with repetitive events. + // 2. Since this doesn't necessarily touch layout, we are not + // guaranteed to have a WillRefresh tick any time soon. + document->HandleScroll(origTargetNode); + } else { + // Root accessible exists longer than any of its descendant documents so + // that we are guaranteed notification is processed before root accessible + // is destroyed. + // For shadow DOM, GetOriginalTarget on the Event returns null if we + // process the event async, so we must pass the target node as well. + document->HandleNotification( + this, &RootAccessible::ProcessDOMEvent, aDOMEvent, origTargetNode); + } + } + + return NS_OK; +} + +// RootAccessible protected +void RootAccessible::ProcessDOMEvent(Event* aDOMEvent, nsINode* aTarget) { + MOZ_ASSERT(aDOMEvent); + MOZ_ASSERT(aTarget); + + nsAutoString eventType; + aDOMEvent->GetType(eventType); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDOMEvents)) { + logging::DOMEvent("processed", aTarget, eventType); + } +#endif + + if (eventType.EqualsLiteral("popuphiding")) { + HandlePopupHidingEvent(aTarget); + return; + } + + DocAccessible* targetDocument = + GetAccService()->GetDocAccessible(aTarget->OwnerDoc()); + if (!targetDocument) { + // Document has ceased to exist. + return; + } + + if (eventType.EqualsLiteral("popupshown") && + aTarget->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) { + targetDocument->ContentInserted(aTarget->AsContent(), + aTarget->GetNextSibling()); + return; + } + + LocalAccessible* accessible = + targetDocument->GetAccessibleOrContainer(aTarget); + if (!accessible) return; + + if (accessible->IsDoc() && eventType.EqualsLiteral("DOMTitleChanged")) { + targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, + accessible); + return; + } + + XULTreeAccessible* treeAcc = accessible->AsXULTree(); + if (treeAcc) { + if (eventType.EqualsLiteral("TreeRowCountChanged")) { + HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc); + return; + } + + if (eventType.EqualsLiteral("TreeInvalidated")) { + HandleTreeInvalidatedEvent(aDOMEvent, treeAcc); + return; + } + } + + if (eventType.EqualsLiteral("RadioStateChange")) { + uint64_t state = accessible->State(); + bool isEnabled = (state & (states::CHECKED | states::SELECTED)) != 0; + + if (accessible->NeedsDOMUIEvent()) { + RefPtr accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } + + if (isEnabled) { + FocusMgr()->ActiveItemChanged(accessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("RadioStateChange", accessible); + } +#endif + } + + return; + } + + if (eventType.EqualsLiteral("CheckboxStateChange")) { + if (accessible->NeedsDOMUIEvent()) { + uint64_t state = accessible->State(); + bool isEnabled = !!(state & states::CHECKED); + + RefPtr accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } + return; + } + + LocalAccessible* treeItemAcc = nullptr; + // If it's a tree element, need the currently selected item. + if (treeAcc) { + treeItemAcc = accessible->CurrentItem(); + if (treeItemAcc) accessible = treeItemAcc; + } + + if (treeItemAcc && eventType.EqualsLiteral("OpenStateChange")) { + uint64_t state = accessible->State(); + bool isEnabled = (state & states::EXPANDED) != 0; + + RefPtr accEvent = + new AccStateChangeEvent(accessible, states::EXPANDED, isEnabled); + nsEventShell::FireEvent(accEvent); + return; + } + + nsINode* targetNode = accessible->GetNode(); + if (treeItemAcc && eventType.EqualsLiteral("select")) { + // XXX: We shouldn't be based on DOM select event which doesn't provide us + // any context info. We should integrate into nsTreeSelection instead. + // If multiselect tree, we should fire selectionadd or selection removed + if (FocusMgr()->HasDOMFocus(targetNode)) { + nsCOMPtr multiSel = + targetNode->AsElement()->AsXULMultiSelectControl(); + if (!multiSel) { + // This shouldn't be possible. All XUL trees should have + // nsIDOMXULMultiSelectControlElement, and the tree is focused, so it + // shouldn't be dying. Nevertheless, this sometimes happens in the wild + // (bug 1597043). + MOZ_ASSERT_UNREACHABLE( + "XUL tree doesn't have nsIDOMXULMultiSelectControlElement"); + return; + } + nsAutoString selType; + multiSel->GetSelType(selType); + if (selType.IsEmpty() || !selType.EqualsLiteral("single")) { + // XXX: We need to fire EVENT_SELECTION_ADD and EVENT_SELECTION_REMOVE + // for each tree item. Perhaps each tree item will need to cache its + // selection state and fire an event after a DOM "select" event when + // that state changes. XULTreeAccessible::UpdateTreeSelection(); + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN, + accessible); + return; + } + + RefPtr selChangeEvent = new AccSelChangeEvent( + treeAcc, treeItemAcc, AccSelChangeEvent::eSelectionAdd); + nsEventShell::FireEvent(selChangeEvent); + return; + } + } else if (eventType.EqualsLiteral("AlertActive")) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_ALERT, accessible); + } else if (eventType.EqualsLiteral("popupshown")) { + HandlePopupShownEvent(accessible); + } else if (eventType.EqualsLiteral("DOMMenuInactive")) { + if (accessible->Role() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + accessible); + } + if (auto* focus = FocusMgr()->FocusedLocalAccessible()) { + // Intentionally use the content tree, because Linux strips menupopups + // from the a11y tree so accessible might be an arbitrary ancestor. + if (focus->GetContent() && + focus->GetContent()->IsShadowIncludingInclusiveDescendantOf( + aTarget)) { + // Move the focus to the topmost menu active content if any. The + // menu item in the parent menu will not fire a DOMMenuItemActive + // event if it's already active. + LocalAccessible* newActiveAccessible = nullptr; + if (auto* pm = nsXULPopupManager::GetInstance()) { + if (auto* content = pm->GetTopActiveMenuItemContent()) { + newActiveAccessible = + accessible->Document()->GetAccessible(content); + } + } + FocusMgr()->ActiveItemChanged(newActiveAccessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuInactive", + newActiveAccessible); + } +#endif + } + } + } else if (eventType.EqualsLiteral("DOMMenuItemActive")) { + RefPtr event = + new AccStateChangeEvent(accessible, states::ACTIVE, true); + nsEventShell::FireEvent(event); + FocusMgr()->ActiveItemChanged(accessible); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuItemActive", accessible); + } +#endif + } else if (eventType.EqualsLiteral("DOMMenuItemInactive")) { + RefPtr event = + new AccStateChangeEvent(accessible, states::ACTIVE, false); + nsEventShell::FireEvent(event); + + // Process DOMMenuItemInactive event for autocomplete only because this is + // unique widget that may acquire focus from autocomplete popup while popup + // stays open and has no active item. In case of XUL tree autocomplete + // popup this event is fired for tree accessible. + LocalAccessible* widget = + accessible->IsWidget() ? accessible : accessible->ContainerWidget(); + if (widget && widget->IsAutoCompletePopup()) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuItemInactive", accessible); + } +#endif + } + } else if (eventType.EqualsLiteral( + "DOMMenuBarActive")) { // Always from user input + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_START, accessible, + eFromUserInput); + + // Notify of active item change when menubar gets active and if it has + // current item. This is a case of mouseover (set current menuitem) and + // mouse click (activate the menubar). If menubar doesn't have current item + // (can be a case of menubar activation from keyboard) then ignore this + // notification because later we'll receive DOMMenuItemActive event after + // current menuitem is set. + LocalAccessible* activeItem = accessible->CurrentItem(); + if (activeItem) { + FocusMgr()->ActiveItemChanged(activeItem); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuBarActive", accessible); + } +#endif + } + } else if (eventType.EqualsLiteral( + "DOMMenuBarInactive")) { // Always from user input + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_END, accessible, + eFromUserInput); + + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("DOMMenuBarInactive", accessible); + } +#endif + } else if (accessible->NeedsDOMUIEvent() && + eventType.EqualsLiteral("ValueChange")) { + uint32_t event = accessible->HasNumericValue() + ? nsIAccessibleEvent::EVENT_VALUE_CHANGE + : nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE; + targetDocument->FireDelayedEvent(event, accessible); + } +#ifdef DEBUG_DRAGDROPSTART + else if (eventType.EqualsLiteral("mouseover")) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_DRAGDROP_START, + accessible); + } +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void RootAccessible::Shutdown() { + // Called manually or by LocalAccessible::LastRelease() + if (HasShutdown()) { + return; + } + DocAccessibleWrap::Shutdown(); +} + +Relation RootAccessible::RelationByType(RelationType aType) const { + if (!mDocumentNode || aType != RelationType::EMBEDS) { + return DocAccessibleWrap::RelationByType(aType); + } + + if (RemoteAccessible* remoteDoc = GetPrimaryRemoteTopLevelContentDoc()) { + return Relation(remoteDoc); + } + + if (nsIDocShell* docShell = mDocumentNode->GetDocShell()) { + nsCOMPtr owner; + docShell->GetTreeOwner(getter_AddRefs(owner)); + if (owner) { + nsCOMPtr contentShell; + owner->GetPrimaryContentShell(getter_AddRefs(contentShell)); + if (contentShell) { + return Relation(nsAccUtils::GetDocAccessibleFor(contentShell)); + } + } + } + + return Relation(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Protected members + +void RootAccessible::HandlePopupShownEvent(LocalAccessible* aAccessible) { + roles::Role role = aAccessible->Role(); + + if (role == roles::MENUPOPUP) { + // Don't fire menupopup events for combobox and autocomplete lists. + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, + aAccessible); + return; + } + + if (role == roles::COMBOBOX_LIST) { + // Fire expanded state change event for comboboxes and autocompeletes. + LocalAccessible* combobox = aAccessible->LocalParent(); + if (!combobox) return; + + if (combobox->IsCombobox()) { + RefPtr event = + new AccStateChangeEvent(combobox, states::EXPANDED, true); + nsEventShell::FireEvent(event); + } + + // If aria-activedescendant is present, redirect focus. + // This is needed for parent process