/* -*- 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 "HTMLFormControlAccessible.h" #include "CacheConstants.h" #include "DocAccessible-inl.h" #include "LocalAccessible-inl.h" #include "nsAccUtils.h" #include "nsEventShell.h" #include "nsTextEquivUtils.h" #include "Relation.h" #include "mozilla/a11y/Role.h" #include "States.h" #include "TextLeafAccessible.h" #include "nsContentList.h" #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/HTMLTextAreaElement.h" #include "mozilla/dom/HTMLFormControlsCollection.h" #include "nsIFormControl.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Preferences.h" #include "mozilla/TextEditor.h" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::a11y; //////////////////////////////////////////////////////////////////////////////// // HTMLFormAccessible //////////////////////////////////////////////////////////////////////////////// role HTMLFormAccessible::NativeRole() const { return NameIsEmpty() ? roles::FORM : roles::FORM_LANDMARK; } void HTMLFormAccessible::DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue, uint64_t aOldState) { HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue, aOldState); if (aAttribute == nsGkAtoms::autocomplete) { dom::HTMLFormElement* formEl = dom::HTMLFormElement::FromNode(mContent); HTMLFormControlsCollection* controls = formEl->Elements(); uint32_t length = controls->Length(); for (uint32_t i = 0; i < length; i++) { if (LocalAccessible* acc = mDoc->GetAccessible(controls->Item(i))) { if (acc->IsTextField() && !acc->IsPassword()) { if (!acc->Elm()->HasAttr(nsGkAtoms::list_) && !acc->Elm()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocomplete, nsGkAtoms::OFF, eIgnoreCase)) { RefPtr stateChangeEvent = new AccStateChangeEvent(acc, states::SUPPORTS_AUTOCOMPLETION); mDoc->FireDelayedEvent(stateChangeEvent); } } } } } } //////////////////////////////////////////////////////////////////////////////// // HTMLRadioButtonAccessible //////////////////////////////////////////////////////////////////////////////// uint64_t HTMLRadioButtonAccessible::NativeState() const { uint64_t state = AccessibleWrap::NativeState(); state |= states::CHECKABLE; HTMLInputElement* input = HTMLInputElement::FromNode(mContent); if (input && input->Checked()) state |= states::CHECKED; return state; } void HTMLRadioButtonAccessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) { Unused << ComputeGroupAttributes(aPosInSet, aSetSize); } void HTMLRadioButtonAccessible::DOMAttributeChanged( int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue, uint64_t aOldState) { if (aAttribute == nsGkAtoms::name) { // If our name changed, it's possible our MEMBER_OF relation // also changed. Push a cache update for Relations. mDoc->QueueCacheUpdate(this, CacheDomain::Relations); } else { // Otherwise, handle this attribute change the way our parent // class wants us to handle it. RadioButtonAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue, aOldState); } } Relation HTMLRadioButtonAccessible::ComputeGroupAttributes( int32_t* aPosInSet, int32_t* aSetSize) const { Relation rel = Relation(); int32_t namespaceId = mContent->NodeInfo()->NamespaceID(); nsAutoString tagName; mContent->NodeInfo()->GetName(tagName); nsAutoString type; mContent->AsElement()->GetAttr(nsGkAtoms::type, type); nsAutoString name; mContent->AsElement()->GetAttr(nsGkAtoms::name, name); RefPtr inputElms; nsCOMPtr formControlNode(do_QueryInterface(mContent)); if (dom::Element* formElm = formControlNode->GetForm()) { inputElms = NS_GetContentList(formElm, namespaceId, tagName); } else { inputElms = NS_GetContentList(mContent->OwnerDoc(), namespaceId, tagName); } NS_ENSURE_TRUE(inputElms, rel); uint32_t inputCount = inputElms->Length(false); // Compute posinset and setsize. int32_t indexOf = 0; int32_t count = 0; for (uint32_t index = 0; index < inputCount; index++) { nsIContent* inputElm = inputElms->Item(index, false); if (inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, type, eCaseMatters) && inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, name, eCaseMatters) && mDoc->HasAccessible(inputElm)) { count++; rel.AppendTarget(mDoc->GetAccessible(inputElm)); if (inputElm == mContent) indexOf = count; } } *aPosInSet = indexOf; *aSetSize = count; return rel; } Relation HTMLRadioButtonAccessible::RelationByType(RelationType aType) const { if (aType == RelationType::MEMBER_OF) { int32_t unusedPos, unusedSetSize; return ComputeGroupAttributes(&unusedPos, &unusedSetSize); } return LocalAccessible::RelationByType(aType); } //////////////////////////////////////////////////////////////////////////////// // HTMLButtonAccessible //////////////////////////////////////////////////////////////////////////////// HTMLButtonAccessible::HTMLButtonAccessible(nsIContent* aContent, DocAccessible* aDoc) : HyperTextAccessible(aContent, aDoc) { mGenericTypes |= eButton; } bool HTMLButtonAccessible::HasPrimaryAction() const { return true; } void HTMLButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { if (aIndex == eAction_Click) aName.AssignLiteral("press"); } uint64_t HTMLButtonAccessible::NativeState() const { uint64_t state = HyperTextAccessible::NativeState(); dom::Element* elm = Elm(); if (auto* popover = elm->GetEffectivePopoverTargetElement()) { LocalAccessible* popoverAcc = mDoc->GetAccessible(popover); if (!popoverAcc || !popoverAcc->IsAncestorOf(this)) { if (popover->IsPopoverOpen()) { state |= states::EXPANDED; } else { state |= states::COLLAPSED; } } } ElementState elmState = mContent->AsElement()->State(); if (elmState.HasState(ElementState::DEFAULT)) state |= states::DEFAULT; return state; } role HTMLButtonAccessible::NativeRole() const { return roles::PUSHBUTTON; } ENameValueFlag HTMLButtonAccessible::NativeName(nsString& aName) const { // No need to check @value attribute for buttons since this attribute results // in native anonymous text node and the name is calculated from subtree. // The same magic works for @alt and @value attributes in case of type="image" // element that has no valid @src (note if input@type="image" has an image // then neither @alt nor @value attributes are used to generate a visual label // and thus we need to obtain the accessible name directly from attribute // value). Also the same algorithm works in case of default labels for // type="submit"/"reset"/"image" elements. ENameValueFlag nameFlag = LocalAccessible::NativeName(aName); if (!aName.IsEmpty() || !mContent->IsHTMLElement(nsGkAtoms::input) || !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::image, eCaseMatters)) { return nameFlag; } if (!mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName)) { mContent->AsElement()->GetAttr(nsGkAtoms::value, aName); } aName.CompressWhitespace(); return eNameOK; } void HTMLButtonAccessible::DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue, uint64_t aOldState) { HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue, aOldState); if (aAttribute == nsGkAtoms::value) { dom::Element* elm = Elm(); if (elm->IsHTMLElement(nsGkAtoms::input) || (elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::image, eCaseMatters) && !elm->HasAttr(nsGkAtoms::alt))) { if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_labelledby) && !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label)) { mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); } } } } //////////////////////////////////////////////////////////////////////////////// // HTMLButtonAccessible: Widgets bool HTMLButtonAccessible::IsWidget() const { return true; } //////////////////////////////////////////////////////////////////////////////// // HTMLTextFieldAccessible //////////////////////////////////////////////////////////////////////////////// HTMLTextFieldAccessible::HTMLTextFieldAccessible(nsIContent* aContent, DocAccessible* aDoc) : HyperTextAccessible(aContent, aDoc) { mType = mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::password, eIgnoreCase) ? eHTMLTextPasswordFieldType : eHTMLTextFieldType; } role HTMLTextFieldAccessible::NativeRole() const { if (mType == eHTMLTextPasswordFieldType) { return roles::PASSWORD_TEXT; } if (mContent->AsElement()->HasAttr(nsGkAtoms::list_)) { return roles::EDITCOMBOBOX; } return roles::ENTRY; } already_AddRefed HTMLTextFieldAccessible::NativeAttributes() { RefPtr attributes = HyperTextAccessible::NativeAttributes(); // Expose type for text input elements as it gives some useful context, // especially for mobile. if (const nsAttrValue* attr = mContent->AsElement()->GetParsedAttr(nsGkAtoms::type)) { RefPtr inputType = attr->GetAsAtom(); if (inputType) { if (!ARIARoleMap() && inputType == nsGkAtoms::search) { attributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::searchbox); } attributes->SetAttribute(nsGkAtoms::textInputType, inputType); } } // If this element has the placeholder attribute set, // and if that is not identical to the name, expose it as an object attribute. nsString placeholderText; if (mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholderText)) { nsAutoString name; Name(name); if (!name.Equals(placeholderText)) { attributes->SetAttribute(nsGkAtoms::placeholder, std::move(placeholderText)); } } return attributes.forget(); } ENameValueFlag HTMLTextFieldAccessible::Name(nsString& aName) const { ENameValueFlag nameFlag = LocalAccessible::Name(aName); if (!aName.IsEmpty()) return nameFlag; // text inputs and textareas might have useful placeholder text mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, aName); return eNameOK; } void HTMLTextFieldAccessible::Value(nsString& aValue) const { aValue.Truncate(); if (NativeState() & states::PROTECTED) { // Don't return password text! return; } HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(mContent); if (textArea) { textArea->GetValue(aValue); return; } HTMLInputElement* input = HTMLInputElement::FromNode(mContent); if (input) { // Pass NonSystem as the caller type, to be safe. We don't expect to have a // file input here. input->GetValue(aValue, CallerType::NonSystem); } } bool HTMLTextFieldAccessible::AttributeChangesState(nsAtom* aAttribute) { if (aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::list_ || aAttribute == nsGkAtoms::autocomplete) { return true; } return LocalAccessible::AttributeChangesState(aAttribute); } void HTMLTextFieldAccessible::ApplyARIAState(uint64_t* aState) const { HyperTextAccessible::ApplyARIAState(aState); aria::MapToState(aria::eARIAAutoComplete, mContent->AsElement(), aState); } uint64_t HTMLTextFieldAccessible::NativeState() const { uint64_t state = HyperTextAccessible::NativeState(); // Text fields are always editable, even if they are also read only or // disabled. state |= states::EDITABLE; // can be focusable, focused, protected. readonly, unavailable, selected if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::password, eIgnoreCase)) { state |= states::PROTECTED; } if (mContent->AsElement()->HasAttr(nsGkAtoms::readonly)) { state |= states::READONLY; } // Is it an or a