Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 4; 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 "HTMLSelectAccessible.h"
#include "LocalAccessible-inl.h"
#include "nsAccessibilityService.h"
#include "nsAccUtils.h"
#include "DocAccessible.h"
#include "nsEventShell.h"
#include "nsTextEquivUtils.h"
#include "Role.h"
#include "States.h"
#include "nsCOMPtr.h"
#include "mozilla/dom/HTMLOptionElement.h"
#include "mozilla/dom/HTMLOptGroupElement.h"
#include "mozilla/dom/HTMLSelectElement.h"
#include "nsComboboxControlFrame.h"
#include "nsContainerFrame.h"
#include "nsListControlFrame.h"
using namespace mozilla::a11y;
using namespace mozilla::dom;
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectListAccessible
////////////////////////////////////////////////////////////////////////////////
HTMLSelectListAccessible::HTMLSelectListAccessible(nsIContent* aContent,
DocAccessible* aDoc)
: AccessibleWrap(aContent, aDoc) {
mGenericTypes |= eListControl | eSelect;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectListAccessible: LocalAccessible public
uint64_t HTMLSelectListAccessible::NativeState() const {
uint64_t state = AccessibleWrap::NativeState();
if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)) {
state |= states::MULTISELECTABLE | states::EXTSELECTABLE;
}
return state;
}
role HTMLSelectListAccessible::NativeRole() const { return roles::LISTBOX; }
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectListAccessible: SelectAccessible
bool HTMLSelectListAccessible::SelectAll() {
return mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)
? AccessibleWrap::SelectAll()
: false;
}
bool HTMLSelectListAccessible::UnselectAll() {
return mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)
? AccessibleWrap::UnselectAll()
: false;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectListAccessible: Widgets
bool HTMLSelectListAccessible::IsWidget() const { return true; }
bool HTMLSelectListAccessible::IsActiveWidget() const {
return FocusMgr()->HasDOMFocus(mContent);
}
bool HTMLSelectListAccessible::AreItemsOperable() const { return true; }
LocalAccessible* HTMLSelectListAccessible::CurrentItem() const {
nsListControlFrame* listControlFrame = do_QueryFrame(GetFrame());
if (listControlFrame) {
nsCOMPtr<nsIContent> activeOptionNode =
listControlFrame->GetCurrentOption();
if (activeOptionNode) {
DocAccessible* document = Document();
if (document) return document->GetAccessible(activeOptionNode);
}
}
return nullptr;
}
void HTMLSelectListAccessible::SetCurrentItem(const LocalAccessible* aItem) {
if (!aItem->GetContent()->IsElement()) return;
aItem->GetContent()->AsElement()->SetAttr(
kNameSpaceID_None, nsGkAtoms::selected, u"true"_ns, true);
}
bool HTMLSelectListAccessible::IsAcceptableChild(nsIContent* aEl) const {
return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
}
bool HTMLSelectListAccessible::AttributeChangesState(nsAtom* aAttribute) {
return aAttribute == nsGkAtoms::multiple ||
LocalAccessible::AttributeChangesState(aAttribute);
}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectOptionAccessible
////////////////////////////////////////////////////////////////////////////////
HTMLSelectOptionAccessible::HTMLSelectOptionAccessible(nsIContent* aContent,
DocAccessible* aDoc)
: HyperTextAccessibleWrap(aContent, aDoc) {}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectOptionAccessible: LocalAccessible public
role HTMLSelectOptionAccessible::NativeRole() const {
if (GetCombobox()) return roles::COMBOBOX_OPTION;
return roles::OPTION;
}
ENameValueFlag HTMLSelectOptionAccessible::NativeName(nsString& aName) const {
if (auto* option = dom::HTMLOptionElement::FromNode(mContent)) {
option->GetAttr(nsGkAtoms::label, aName);
if (!aName.IsEmpty()) {
return eNameOK;
}
option->GetText(aName);
return eNameFromSubtree;
}
if (auto* group = dom::HTMLOptGroupElement::FromNode(mContent)) {
group->GetLabel(aName);
return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
}
MOZ_ASSERT_UNREACHABLE("What content do we have?");
return eNameFromSubtree;
}
void HTMLSelectOptionAccessible::DOMAttributeChanged(
int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType,
const nsAttrValue* aOldValue, uint64_t aOldState) {
HyperTextAccessibleWrap::DOMAttributeChanged(aNameSpaceID, aAttribute,
aModType, aOldValue, aOldState);
if (aAttribute == nsGkAtoms::label) {
dom::Element* elm = Elm();
if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_labelledby) &&
!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label)) {
mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
}
}
}
uint64_t HTMLSelectOptionAccessible::NativeState() const {
// As a HTMLSelectOptionAccessible we can have the following states:
// SELECTABLE, SELECTED, FOCUSED, FOCUSABLE, OFFSCREEN
// Upcall to LocalAccessible, but skip HyperTextAccessible impl
// because we don't want EDITABLE or SELECTABLE_TEXT
uint64_t state = LocalAccessible::NativeState();
LocalAccessible* select = GetSelect();
if (!select) return state;
uint64_t selectState = select->State();
if (selectState & states::INVISIBLE) return state;
// Are we selected?
HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent);
bool selected = option && option->Selected();
if (selected) state |= states::SELECTED;
if (selectState & states::OFFSCREEN) {
state |= states::OFFSCREEN;
} else if (selectState & states::COLLAPSED) {
// <select> is COLLAPSED: add OFFSCREEN, if not the currently
// visible option
if (!selected) {
state |= states::OFFSCREEN;
// Ensure the invisible state is removed. Otherwise, group info will skip
// this option. Furthermore, this gets cached and this doesn't get
// invalidated even once the select is expanded.
state &= ~states::INVISIBLE;
} else {
// Clear offscreen and invisible for currently showing option
state &= ~(states::OFFSCREEN | states::INVISIBLE);
state |= selectState & states::OPAQUE1;
}
} else {
// XXX list frames are weird, don't rely on LocalAccessible's general
// visibility implementation unless they get reimplemented in layout
state &= ~states::OFFSCREEN;
// <select> is not collapsed: compare bounds to calculate OFFSCREEN
LocalAccessible* listAcc = LocalParent();
if (listAcc) {
LayoutDeviceIntRect optionRect = Bounds();
LayoutDeviceIntRect listRect = listAcc->Bounds();
if (optionRect.Y() < listRect.Y() ||
optionRect.YMost() > listRect.YMost()) {
state |= states::OFFSCREEN;
}
}
}
return state;
}
uint64_t HTMLSelectOptionAccessible::NativeInteractiveState() const {
return NativelyUnavailable() ? states::UNAVAILABLE
: states::FOCUSABLE | states::SELECTABLE;
}
nsRect HTMLSelectOptionAccessible::RelativeBounds(
nsIFrame** aBoundingFrame) const {
LocalAccessible* combobox = GetCombobox();
if (combobox && (combobox->State() & states::COLLAPSED)) {
return combobox->RelativeBounds(aBoundingFrame);
}
return HyperTextAccessibleWrap::RelativeBounds(aBoundingFrame);
}
void HTMLSelectOptionAccessible::ActionNameAt(uint8_t aIndex,
nsAString& aName) {
if (aIndex == eAction_Select) aName.AssignLiteral("select");
}
bool HTMLSelectOptionAccessible::HasPrimaryAction() const { return true; }
void HTMLSelectOptionAccessible::SetSelected(bool aSelect) {
HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent);
if (option) option->SetSelected(aSelect);
}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectOptionAccessible: Widgets
LocalAccessible* HTMLSelectOptionAccessible::ContainerWidget() const {
LocalAccessible* parent = LocalParent();
if (parent && parent->IsHTMLOptGroup()) {
parent = parent->LocalParent();
}
return parent && parent->IsListControl() ? parent : nullptr;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLSelectOptGroupAccessible
////////////////////////////////////////////////////////////////////////////////
role HTMLSelectOptGroupAccessible::NativeRole() const {
return roles::GROUPING;
}
uint64_t HTMLSelectOptGroupAccessible::NativeInteractiveState() const {
return NativelyUnavailable() ? states::UNAVAILABLE : 0;
}
bool HTMLSelectOptGroupAccessible::IsAcceptableChild(nsIContent* aEl) const {
return aEl->IsCharacterData() || aEl->IsHTMLElement(nsGkAtoms::option);
}
bool HTMLSelectOptGroupAccessible::HasPrimaryAction() const { return false; }
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxAccessible
////////////////////////////////////////////////////////////////////////////////
HTMLComboboxAccessible::HTMLComboboxAccessible(nsIContent* aContent,
DocAccessible* aDoc)
: AccessibleWrap(aContent, aDoc) {
mType = eHTMLComboboxType;
mGenericTypes |= eCombobox;
mStateFlags |= eNoKidsFromDOM;
if ((nsComboboxControlFrame*)do_QueryFrame(GetFrame())) {
mListAccessible = new HTMLComboboxListAccessible(mParent, mContent, mDoc);
Document()->BindToDocument(mListAccessible, nullptr);
AppendChild(mListAccessible);
}
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxAccessible: LocalAccessible
role HTMLComboboxAccessible::NativeRole() const { return roles::COMBOBOX; }
bool HTMLComboboxAccessible::RemoveChild(LocalAccessible* aChild) {
MOZ_ASSERT(aChild == mListAccessible);
if (AccessibleWrap::RemoveChild(aChild)) {
mListAccessible = nullptr;
return true;
}
return false;
}
void HTMLComboboxAccessible::Shutdown() {
MOZ_ASSERT(!mDoc || mDoc->IsDefunct() || !mListAccessible);
if (mListAccessible) {
mListAccessible->Shutdown();
mListAccessible = nullptr;
}
AccessibleWrap::Shutdown();
}
uint64_t HTMLComboboxAccessible::NativeState() const {
// As a HTMLComboboxAccessible we can have the following states:
// FOCUSED, FOCUSABLE, HASPOPUP, EXPANDED, COLLAPSED
// Get focus status from base class
uint64_t state = LocalAccessible::NativeState();
nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame());
if (comboFrame && comboFrame->IsDroppedDown()) {
state |= states::EXPANDED;
} else {
state |= states::COLLAPSED;
}
state |= states::HASPOPUP;
return state;
}
void HTMLComboboxAccessible::Description(nsString& aDescription) const {
aDescription.Truncate();
// First check to see if combo box itself has a description, perhaps through
// tooltip (title attribute) or via aria-describedby
LocalAccessible::Description(aDescription);
if (!aDescription.IsEmpty()) return;
// Otherwise use description of selected option.
LocalAccessible* option = SelectedOption();
if (option) option->Description(aDescription);
}
void HTMLComboboxAccessible::Value(nsString& aValue) const {
// Use accessible name of selected option.
LocalAccessible* option = SelectedOption();
if (option) option->Name(aValue);
}
bool HTMLComboboxAccessible::HasPrimaryAction() const { return true; }
void HTMLComboboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
if (aIndex != HTMLComboboxAccessible::eAction_Click) return;
nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame());
if (!comboFrame) return;
if (comboFrame->IsDroppedDown()) {
aName.AssignLiteral("close");
} else {
aName.AssignLiteral("open");
}
}
bool HTMLComboboxAccessible::IsAcceptableChild(nsIContent* aEl) const {
return false;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxAccessible: Widgets
bool HTMLComboboxAccessible::IsWidget() const { return true; }
bool HTMLComboboxAccessible::IsActiveWidget() const {
return FocusMgr()->HasDOMFocus(mContent);
}
bool HTMLComboboxAccessible::AreItemsOperable() const {
nsComboboxControlFrame* comboboxFrame = do_QueryFrame(GetFrame());
return comboboxFrame && comboboxFrame->IsDroppedDown();
}
LocalAccessible* HTMLComboboxAccessible::CurrentItem() const {
return AreItemsOperable() ? mListAccessible->CurrentItem() : nullptr;
}
void HTMLComboboxAccessible::SetCurrentItem(const LocalAccessible* aItem) {
if (AreItemsOperable()) mListAccessible->SetCurrentItem(aItem);
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxAccessible: protected
LocalAccessible* HTMLComboboxAccessible::SelectedOption() const {
HTMLSelectElement* select = HTMLSelectElement::FromNode(mContent);
int32_t selectedIndex = select->SelectedIndex();
if (selectedIndex >= 0) {
HTMLOptionElement* option = select->Item(selectedIndex);
if (option) {
DocAccessible* document = Document();
if (document) return document->GetAccessible(option);
}
}
return nullptr;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxListAccessible
////////////////////////////////////////////////////////////////////////////////
HTMLComboboxListAccessible::HTMLComboboxListAccessible(LocalAccessible* aParent,
nsIContent* aContent,
DocAccessible* aDoc)
: HTMLSelectListAccessible(aContent, aDoc) {
mStateFlags |= eSharedNode;
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxAccessible: LocalAccessible
role HTMLComboboxListAccessible::NativeRole() const {
return roles::COMBOBOX_LIST;
}
uint64_t HTMLComboboxListAccessible::NativeState() const {
// As a HTMLComboboxListAccessible we can have the following states:
// FOCUSED, FOCUSABLE, FLOATING, INVISIBLE
// Get focus status from base class
uint64_t state = LocalAccessible::NativeState();
nsComboboxControlFrame* comboFrame = do_QueryFrame(mParent->GetFrame());
if (comboFrame && comboFrame->IsDroppedDown()) {
state |= states::FLOATING;
} else {
state |= states::INVISIBLE;
}
return state;
}
nsRect HTMLComboboxListAccessible::RelativeBounds(
nsIFrame** aBoundingFrame) const {
*aBoundingFrame = nullptr;
LocalAccessible* comboAcc = LocalParent();
if (!comboAcc) return nsRect();
if (0 == (comboAcc->State() & states::COLLAPSED)) {
return HTMLSelectListAccessible::RelativeBounds(aBoundingFrame);
}
// Get the first option.
nsIContent* content = mContent->GetFirstChild();
if (!content) return nsRect();
nsIFrame* frame = content->GetPrimaryFrame();
if (!frame) {
*aBoundingFrame = nullptr;
return nsRect();
}
*aBoundingFrame = frame->GetParent();
return (*aBoundingFrame)->GetRect();
}
bool HTMLComboboxListAccessible::IsAcceptableChild(nsIContent* aEl) const {
return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
}
////////////////////////////////////////////////////////////////////////////////
// HTMLComboboxListAccessible: Widgets
bool HTMLComboboxListAccessible::IsActiveWidget() const {
return mParent && mParent->IsActiveWidget();
}
bool HTMLComboboxListAccessible::AreItemsOperable() const {
return mParent && mParent->AreItemsOperable();
}