HTMLSelectAccessible.cpp (16190B)
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "HTMLSelectAccessible.h" 7 8 #include "LocalAccessible-inl.h" 9 #include "DocAccessible-inl.h" 10 #include "nsAccessibilityService.h" 11 #include "nsAccUtils.h" 12 #include "DocAccessible.h" 13 #include "mozilla/a11y/Role.h" 14 #include "States.h" 15 16 #include "nsCOMPtr.h" 17 #include "mozilla/dom/HTMLOptionElement.h" 18 #include "mozilla/dom/HTMLOptGroupElement.h" 19 #include "mozilla/dom/HTMLSelectElement.h" 20 #include "nsComboboxControlFrame.h" 21 #include "nsContainerFrame.h" 22 #include "nsListControlFrame.h" 23 24 using namespace mozilla::a11y; 25 using namespace mozilla::dom; 26 27 //////////////////////////////////////////////////////////////////////////////// 28 // HTMLSelectListAccessible 29 //////////////////////////////////////////////////////////////////////////////// 30 31 HTMLSelectListAccessible::HTMLSelectListAccessible(nsIContent* aContent, 32 DocAccessible* aDoc) 33 : AccessibleWrap(aContent, aDoc) { 34 mGenericTypes |= eListControl | eSelect; 35 } 36 37 //////////////////////////////////////////////////////////////////////////////// 38 // HTMLSelectListAccessible: LocalAccessible public 39 40 uint64_t HTMLSelectListAccessible::NativeState() const { 41 uint64_t state = AccessibleWrap::NativeState(); 42 if (mContent->AsElement()->HasAttr(nsGkAtoms::multiple)) { 43 state |= states::MULTISELECTABLE | states::EXTSELECTABLE; 44 } 45 46 return state; 47 } 48 49 role HTMLSelectListAccessible::NativeRole() const { return roles::LISTBOX; } 50 51 //////////////////////////////////////////////////////////////////////////////// 52 // HTMLSelectListAccessible: SelectAccessible 53 54 bool HTMLSelectListAccessible::SelectAll() { 55 return mContent->AsElement()->HasAttr(nsGkAtoms::multiple) 56 ? AccessibleWrap::SelectAll() 57 : false; 58 } 59 60 bool HTMLSelectListAccessible::UnselectAll() { 61 return mContent->AsElement()->HasAttr(nsGkAtoms::multiple) 62 ? AccessibleWrap::UnselectAll() 63 : false; 64 } 65 66 //////////////////////////////////////////////////////////////////////////////// 67 // HTMLSelectListAccessible: Widgets 68 69 bool HTMLSelectListAccessible::IsWidget() const { return true; } 70 71 bool HTMLSelectListAccessible::IsActiveWidget() const { 72 return FocusMgr()->HasDOMFocus(mContent); 73 } 74 75 bool HTMLSelectListAccessible::AreItemsOperable() const { return true; } 76 77 LocalAccessible* HTMLSelectListAccessible::CurrentItem() const { 78 nsListControlFrame* listControlFrame = do_QueryFrame(GetFrame()); 79 if (listControlFrame) { 80 nsCOMPtr<nsIContent> activeOptionNode = 81 listControlFrame->GetCurrentOption(); 82 if (activeOptionNode) { 83 DocAccessible* document = Document(); 84 if (document) return document->GetAccessible(activeOptionNode); 85 } 86 } 87 return nullptr; 88 } 89 90 void HTMLSelectListAccessible::SetCurrentItem(const LocalAccessible* aItem) { 91 if (!aItem->GetContent()->IsElement()) return; 92 93 aItem->GetContent()->AsElement()->SetAttr( 94 kNameSpaceID_None, nsGkAtoms::selected, u"true"_ns, true); 95 } 96 97 bool HTMLSelectListAccessible::IsAcceptableChild(nsIContent* aEl) const { 98 return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup); 99 } 100 101 bool HTMLSelectListAccessible::AttributeChangesState(nsAtom* aAttribute) { 102 return aAttribute == nsGkAtoms::multiple || 103 LocalAccessible::AttributeChangesState(aAttribute); 104 } 105 106 //////////////////////////////////////////////////////////////////////////////// 107 // HTMLSelectOptionAccessible 108 //////////////////////////////////////////////////////////////////////////////// 109 110 HTMLSelectOptionAccessible::HTMLSelectOptionAccessible(nsIContent* aContent, 111 DocAccessible* aDoc) 112 : HyperTextAccessible(aContent, aDoc) {} 113 114 //////////////////////////////////////////////////////////////////////////////// 115 // HTMLSelectOptionAccessible: LocalAccessible public 116 117 role HTMLSelectOptionAccessible::NativeRole() const { 118 if (GetCombobox()) return roles::COMBOBOX_OPTION; 119 120 return roles::OPTION; 121 } 122 123 ENameValueFlag HTMLSelectOptionAccessible::NativeName(nsString& aName) const { 124 if (auto* option = dom::HTMLOptionElement::FromNode(mContent)) { 125 option->GetAttr(nsGkAtoms::label, aName); 126 if (!aName.IsEmpty()) { 127 return eNameOK; 128 } 129 option->GetText(aName); 130 return eNameFromSubtree; 131 } 132 if (auto* group = dom::HTMLOptGroupElement::FromNode(mContent)) { 133 group->GetLabel(aName); 134 return aName.IsEmpty() ? eNameOK : eNameFromSubtree; 135 } 136 MOZ_ASSERT_UNREACHABLE("What content do we have?"); 137 return eNameFromSubtree; 138 } 139 140 void HTMLSelectOptionAccessible::DOMAttributeChanged( 141 int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType aModType, 142 const nsAttrValue* aOldValue, uint64_t aOldState) { 143 HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, 144 aOldValue, aOldState); 145 146 if (aAttribute == nsGkAtoms::label) { 147 dom::Element* elm = Elm(); 148 if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_labelledby) && 149 !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label)) { 150 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); 151 } 152 } 153 } 154 155 uint64_t HTMLSelectOptionAccessible::NativeState() const { 156 // As a HTMLSelectOptionAccessible we can have the following states: 157 // SELECTABLE, SELECTED, FOCUSED, FOCUSABLE, OFFSCREEN 158 // Upcall to LocalAccessible, but skip HyperTextAccessible impl 159 // because we don't want EDITABLE or SELECTABLE_TEXT 160 uint64_t state = LocalAccessible::NativeState(); 161 162 LocalAccessible* select = GetSelect(); 163 if (!select) return state; 164 165 uint64_t selectState = select->State(); 166 if (selectState & states::INVISIBLE) return state; 167 168 // Are we selected? 169 HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent); 170 bool selected = option && option->Selected(); 171 if (selected) state |= states::SELECTED; 172 173 if (selectState & states::OFFSCREEN) { 174 state |= states::OFFSCREEN; 175 } else if (selectState & states::COLLAPSED) { 176 // <select> is COLLAPSED: add OFFSCREEN, if not the currently 177 // visible option 178 if (!selected) { 179 state |= states::OFFSCREEN; 180 // Ensure the invisible state is removed. Otherwise, group info will skip 181 // this option. Furthermore, this gets cached and this doesn't get 182 // invalidated even once the select is expanded. 183 state &= ~states::INVISIBLE; 184 } else { 185 // Clear offscreen and invisible for currently showing option 186 state &= ~(states::OFFSCREEN | states::INVISIBLE); 187 state |= selectState & states::OPAQUE1; 188 } 189 } else { 190 // XXX list frames are weird, don't rely on LocalAccessible's general 191 // visibility implementation unless they get reimplemented in layout 192 state &= ~states::OFFSCREEN; 193 // <select> is not collapsed: compare bounds to calculate OFFSCREEN 194 LocalAccessible* listAcc = LocalParent(); 195 if (listAcc) { 196 LayoutDeviceIntRect optionRect = Bounds(); 197 LayoutDeviceIntRect listRect = listAcc->Bounds(); 198 if (optionRect.Y() < listRect.Y() || 199 optionRect.YMost() > listRect.YMost()) { 200 state |= states::OFFSCREEN; 201 } 202 } 203 } 204 205 return state; 206 } 207 208 uint64_t HTMLSelectOptionAccessible::NativeInteractiveState() const { 209 return NativelyUnavailable() ? states::UNAVAILABLE 210 : states::FOCUSABLE | states::SELECTABLE; 211 } 212 213 nsRect HTMLSelectOptionAccessible::RelativeBounds( 214 nsIFrame** aBoundingFrame) const { 215 LocalAccessible* combobox = GetCombobox(); 216 if (combobox && (combobox->State() & states::COLLAPSED)) { 217 return combobox->RelativeBounds(aBoundingFrame); 218 } 219 220 return HyperTextAccessible::RelativeBounds(aBoundingFrame); 221 } 222 223 void HTMLSelectOptionAccessible::ActionNameAt(uint8_t aIndex, 224 nsAString& aName) { 225 if (aIndex == eAction_Select) aName.AssignLiteral("select"); 226 } 227 228 bool HTMLSelectOptionAccessible::HasPrimaryAction() const { return true; } 229 230 void HTMLSelectOptionAccessible::SetSelected(bool aSelect) { 231 HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent); 232 if (option) option->SetSelected(aSelect); 233 } 234 235 //////////////////////////////////////////////////////////////////////////////// 236 // HTMLSelectOptionAccessible: Widgets 237 238 LocalAccessible* HTMLSelectOptionAccessible::ContainerWidget() const { 239 LocalAccessible* parent = LocalParent(); 240 if (parent && parent->IsHTMLOptGroup()) { 241 parent = parent->LocalParent(); 242 } 243 244 return parent && parent->IsListControl() ? parent : nullptr; 245 } 246 247 //////////////////////////////////////////////////////////////////////////////// 248 // HTMLSelectOptGroupAccessible 249 //////////////////////////////////////////////////////////////////////////////// 250 251 role HTMLSelectOptGroupAccessible::NativeRole() const { 252 return roles::GROUPING; 253 } 254 255 uint64_t HTMLSelectOptGroupAccessible::NativeInteractiveState() const { 256 return NativelyUnavailable() ? states::UNAVAILABLE : 0; 257 } 258 259 bool HTMLSelectOptGroupAccessible::IsAcceptableChild(nsIContent* aEl) const { 260 return aEl->IsCharacterData() || aEl->IsHTMLElement(nsGkAtoms::option); 261 } 262 263 bool HTMLSelectOptGroupAccessible::HasPrimaryAction() const { return false; } 264 265 //////////////////////////////////////////////////////////////////////////////// 266 // HTMLComboboxAccessible 267 //////////////////////////////////////////////////////////////////////////////// 268 269 HTMLComboboxAccessible::HTMLComboboxAccessible(nsIContent* aContent, 270 DocAccessible* aDoc) 271 : AccessibleWrap(aContent, aDoc) { 272 mType = eHTMLComboboxType; 273 mGenericTypes |= eCombobox; 274 mStateFlags |= eNoKidsFromDOM; 275 276 if ((nsComboboxControlFrame*)do_QueryFrame(GetFrame())) { 277 mListAccessible = new HTMLComboboxListAccessible(mParent, mContent, mDoc); 278 Document()->BindToDocument(mListAccessible, nullptr); 279 AppendChild(mListAccessible); 280 } 281 } 282 283 //////////////////////////////////////////////////////////////////////////////// 284 // HTMLComboboxAccessible: LocalAccessible 285 286 role HTMLComboboxAccessible::NativeRole() const { return roles::COMBOBOX; } 287 288 bool HTMLComboboxAccessible::RemoveChild(LocalAccessible* aChild) { 289 MOZ_ASSERT(aChild == mListAccessible); 290 if (AccessibleWrap::RemoveChild(aChild)) { 291 mListAccessible = nullptr; 292 return true; 293 } 294 return false; 295 } 296 297 void HTMLComboboxAccessible::Shutdown() { 298 MOZ_ASSERT(!mDoc || mDoc->IsDefunct() || !mListAccessible); 299 if (mListAccessible) { 300 mListAccessible->Shutdown(); 301 mListAccessible = nullptr; 302 } 303 304 AccessibleWrap::Shutdown(); 305 } 306 307 uint64_t HTMLComboboxAccessible::NativeState() const { 308 // As a HTMLComboboxAccessible we can have the following states: 309 // FOCUSED, FOCUSABLE, HASPOPUP, EXPANDED, COLLAPSED 310 // Get focus status from base class 311 uint64_t state = LocalAccessible::NativeState(); 312 313 nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame()); 314 if (comboFrame && comboFrame->IsDroppedDown()) { 315 state |= states::EXPANDED; 316 } 317 318 state |= states::HASPOPUP | states::EXPANDABLE; 319 return state; 320 } 321 322 EDescriptionValueFlag HTMLComboboxAccessible::Description( 323 nsString& aDescription) const { 324 aDescription.Truncate(); 325 // First check to see if combo box itself has a description, perhaps through 326 // tooltip (title attribute) or via aria-describedby 327 EDescriptionValueFlag descFlag = LocalAccessible::Description(aDescription); 328 if (!aDescription.IsEmpty()) { 329 return descFlag; 330 } 331 332 // Otherwise use description of selected option. 333 LocalAccessible* option = SelectedOption(); 334 if (option) { 335 return option->Description(aDescription); 336 } 337 338 return eDescriptionOK; 339 } 340 341 void HTMLComboboxAccessible::Value(nsString& aValue) const { 342 // Use accessible name of selected option. 343 LocalAccessible* option = SelectedOption(); 344 if (option) option->Name(aValue); 345 } 346 347 bool HTMLComboboxAccessible::HasPrimaryAction() const { return true; } 348 349 void HTMLComboboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { 350 if (aIndex != HTMLComboboxAccessible::eAction_Click) return; 351 352 nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame()); 353 if (!comboFrame) return; 354 355 if (comboFrame->IsDroppedDown()) { 356 aName.AssignLiteral("close"); 357 } else { 358 aName.AssignLiteral("open"); 359 } 360 } 361 362 bool HTMLComboboxAccessible::IsAcceptableChild(nsIContent* aEl) const { 363 return false; 364 } 365 366 //////////////////////////////////////////////////////////////////////////////// 367 // HTMLComboboxAccessible: Widgets 368 369 bool HTMLComboboxAccessible::IsWidget() const { return true; } 370 371 bool HTMLComboboxAccessible::IsActiveWidget() const { 372 return FocusMgr()->HasDOMFocus(mContent); 373 } 374 375 bool HTMLComboboxAccessible::AreItemsOperable() const { 376 nsComboboxControlFrame* comboboxFrame = do_QueryFrame(GetFrame()); 377 return comboboxFrame && comboboxFrame->IsDroppedDown(); 378 } 379 380 LocalAccessible* HTMLComboboxAccessible::CurrentItem() const { 381 return AreItemsOperable() ? mListAccessible->CurrentItem() : nullptr; 382 } 383 384 void HTMLComboboxAccessible::SetCurrentItem(const LocalAccessible* aItem) { 385 if (AreItemsOperable()) mListAccessible->SetCurrentItem(aItem); 386 } 387 388 //////////////////////////////////////////////////////////////////////////////// 389 // HTMLComboboxAccessible: protected 390 391 LocalAccessible* HTMLComboboxAccessible::SelectedOption() const { 392 HTMLSelectElement* select = HTMLSelectElement::FromNode(mContent); 393 int32_t selectedIndex = select->SelectedIndex(); 394 395 if (selectedIndex >= 0) { 396 HTMLOptionElement* option = select->Item(selectedIndex); 397 if (option) { 398 DocAccessible* document = Document(); 399 if (document) return document->GetAccessible(option); 400 } 401 } 402 403 return nullptr; 404 } 405 406 //////////////////////////////////////////////////////////////////////////////// 407 // HTMLComboboxListAccessible 408 //////////////////////////////////////////////////////////////////////////////// 409 410 HTMLComboboxListAccessible::HTMLComboboxListAccessible(LocalAccessible* aParent, 411 nsIContent* aContent, 412 DocAccessible* aDoc) 413 : HTMLSelectListAccessible(aContent, aDoc) { 414 mStateFlags |= eSharedNode; 415 } 416 417 //////////////////////////////////////////////////////////////////////////////// 418 // HTMLComboboxAccessible: LocalAccessible 419 420 role HTMLComboboxListAccessible::NativeRole() const { 421 return roles::COMBOBOX_LIST; 422 } 423 424 uint64_t HTMLComboboxListAccessible::NativeState() const { 425 // As a HTMLComboboxListAccessible we can have the following states: 426 // FOCUSED, FOCUSABLE, FLOATING, INVISIBLE 427 // Get focus status from base class 428 uint64_t state = LocalAccessible::NativeState(); 429 430 nsComboboxControlFrame* comboFrame = do_QueryFrame(mParent->GetFrame()); 431 if (comboFrame && comboFrame->IsDroppedDown()) { 432 state |= states::FLOATING; 433 } else { 434 state |= states::INVISIBLE; 435 } 436 437 return state; 438 } 439 440 nsRect HTMLComboboxListAccessible::RelativeBounds( 441 nsIFrame** aBoundingFrame) const { 442 *aBoundingFrame = nullptr; 443 444 LocalAccessible* comboAcc = LocalParent(); 445 if (!comboAcc) return nsRect(); 446 447 if (0 == (comboAcc->State() & states::COLLAPSED)) { 448 return HTMLSelectListAccessible::RelativeBounds(aBoundingFrame); 449 } 450 451 // Get the first option. 452 nsIContent* content = mContent->GetFirstChild(); 453 if (!content) return nsRect(); 454 455 nsIFrame* frame = content->GetPrimaryFrame(); 456 if (!frame) { 457 *aBoundingFrame = nullptr; 458 return nsRect(); 459 } 460 461 *aBoundingFrame = frame->GetParent(); 462 return (*aBoundingFrame)->GetRect(); 463 } 464 465 bool HTMLComboboxListAccessible::IsAcceptableChild(nsIContent* aEl) const { 466 return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup); 467 } 468 469 //////////////////////////////////////////////////////////////////////////////// 470 // HTMLComboboxListAccessible: Widgets 471 472 bool HTMLComboboxListAccessible::IsActiveWidget() const { 473 return mParent && mParent->IsActiveWidget(); 474 } 475 476 bool HTMLComboboxListAccessible::AreItemsOperable() const { 477 return mParent && mParent->AreItemsOperable(); 478 }