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/. */
6 #include "HTMLSelectAccessible.h"
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"
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"
24 using namespace mozilla::a11y
;
25 using namespace mozilla::dom
;
27 ////////////////////////////////////////////////////////////////////////////////
28 // HTMLSelectListAccessible
29 ////////////////////////////////////////////////////////////////////////////////
31 HTMLSelectListAccessible::HTMLSelectListAccessible(nsIContent
* aContent
,
33 : AccessibleWrap(aContent
, aDoc
) {
34 mGenericTypes
|= eListControl
| eSelect
;
37 ////////////////////////////////////////////////////////////////////////////////
38 // HTMLSelectListAccessible: LocalAccessible public
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
;
49 role
HTMLSelectListAccessible::NativeRole() const { return roles::LISTBOX
; }
51 ////////////////////////////////////////////////////////////////////////////////
52 // HTMLSelectListAccessible: SelectAccessible
54 bool HTMLSelectListAccessible::SelectAll() {
55 return mContent
->AsElement()->HasAttr(nsGkAtoms::multiple
)
56 ? AccessibleWrap::SelectAll()
60 bool HTMLSelectListAccessible::UnselectAll() {
61 return mContent
->AsElement()->HasAttr(nsGkAtoms::multiple
)
62 ? AccessibleWrap::UnselectAll()
66 ////////////////////////////////////////////////////////////////////////////////
67 // HTMLSelectListAccessible: Widgets
69 bool HTMLSelectListAccessible::IsWidget() const { return true; }
71 bool HTMLSelectListAccessible::IsActiveWidget() const {
72 return FocusMgr()->HasDOMFocus(mContent
);
75 bool HTMLSelectListAccessible::AreItemsOperable() const { return true; }
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
);
90 void HTMLSelectListAccessible::SetCurrentItem(const LocalAccessible
* aItem
) {
91 if (!aItem
->GetContent()->IsElement()) return;
93 aItem
->GetContent()->AsElement()->SetAttr(
94 kNameSpaceID_None
, nsGkAtoms::selected
, u
"true"_ns
, true);
97 bool HTMLSelectListAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
98 return aEl
->IsAnyOfHTMLElements(nsGkAtoms::option
, nsGkAtoms::optgroup
);
101 bool HTMLSelectListAccessible::AttributeChangesState(nsAtom
* aAttribute
) {
102 return aAttribute
== nsGkAtoms::multiple
||
103 LocalAccessible::AttributeChangesState(aAttribute
);
106 ////////////////////////////////////////////////////////////////////////////////
107 // HTMLSelectOptionAccessible
108 ////////////////////////////////////////////////////////////////////////////////
110 HTMLSelectOptionAccessible::HTMLSelectOptionAccessible(nsIContent
* aContent
,
112 : HyperTextAccessible(aContent
, aDoc
) {}
114 ////////////////////////////////////////////////////////////////////////////////
115 // HTMLSelectOptionAccessible: LocalAccessible public
117 role
HTMLSelectOptionAccessible::NativeRole() const {
118 if (GetCombobox()) return roles::COMBOBOX_OPTION
;
120 return roles::OPTION
;
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()) {
129 option
->GetText(aName
);
130 return eNameFromSubtree
;
132 if (auto* group
= dom::HTMLOptGroupElement::FromNode(mContent
)) {
133 group
->GetLabel(aName
);
134 return aName
.IsEmpty() ? eNameOK
: eNameFromSubtree
;
136 MOZ_ASSERT_UNREACHABLE("What content do we have?");
137 return eNameFromSubtree
;
140 void HTMLSelectOptionAccessible::DOMAttributeChanged(
141 int32_t aNameSpaceID
, nsAtom
* aAttribute
, int32_t aModType
,
142 const nsAttrValue
* aOldValue
, uint64_t aOldState
) {
143 HyperTextAccessible::DOMAttributeChanged(aNameSpaceID
, aAttribute
, aModType
,
144 aOldValue
, aOldState
);
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);
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();
162 LocalAccessible
* select
= GetSelect();
163 if (!select
) return state
;
165 uint64_t selectState
= select
->State();
166 if (selectState
& states::INVISIBLE
) return state
;
169 HTMLOptionElement
* option
= HTMLOptionElement::FromNode(mContent
);
170 bool selected
= option
&& option
->Selected();
171 if (selected
) state
|= states::SELECTED
;
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
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
;
185 // Clear offscreen and invisible for currently showing option
186 state
&= ~(states::OFFSCREEN
| states::INVISIBLE
);
187 state
|= selectState
& states::OPAQUE1
;
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();
196 LayoutDeviceIntRect optionRect
= Bounds();
197 LayoutDeviceIntRect listRect
= listAcc
->Bounds();
198 if (optionRect
.Y() < listRect
.Y() ||
199 optionRect
.YMost() > listRect
.YMost()) {
200 state
|= states::OFFSCREEN
;
208 uint64_t HTMLSelectOptionAccessible::NativeInteractiveState() const {
209 return NativelyUnavailable() ? states::UNAVAILABLE
210 : states::FOCUSABLE
| states::SELECTABLE
;
213 nsRect
HTMLSelectOptionAccessible::RelativeBounds(
214 nsIFrame
** aBoundingFrame
) const {
215 LocalAccessible
* combobox
= GetCombobox();
216 if (combobox
&& (combobox
->State() & states::COLLAPSED
)) {
217 return combobox
->RelativeBounds(aBoundingFrame
);
220 return HyperTextAccessible::RelativeBounds(aBoundingFrame
);
223 void HTMLSelectOptionAccessible::ActionNameAt(uint8_t aIndex
,
225 if (aIndex
== eAction_Select
) aName
.AssignLiteral("select");
228 bool HTMLSelectOptionAccessible::HasPrimaryAction() const { return true; }
230 void HTMLSelectOptionAccessible::SetSelected(bool aSelect
) {
231 HTMLOptionElement
* option
= HTMLOptionElement::FromNode(mContent
);
232 if (option
) option
->SetSelected(aSelect
);
235 ////////////////////////////////////////////////////////////////////////////////
236 // HTMLSelectOptionAccessible: Widgets
238 LocalAccessible
* HTMLSelectOptionAccessible::ContainerWidget() const {
239 LocalAccessible
* parent
= LocalParent();
240 if (parent
&& parent
->IsHTMLOptGroup()) {
241 parent
= parent
->LocalParent();
244 return parent
&& parent
->IsListControl() ? parent
: nullptr;
247 ////////////////////////////////////////////////////////////////////////////////
248 // HTMLSelectOptGroupAccessible
249 ////////////////////////////////////////////////////////////////////////////////
251 role
HTMLSelectOptGroupAccessible::NativeRole() const {
252 return roles::GROUPING
;
255 uint64_t HTMLSelectOptGroupAccessible::NativeInteractiveState() const {
256 return NativelyUnavailable() ? states::UNAVAILABLE
: 0;
259 bool HTMLSelectOptGroupAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
260 return aEl
->IsCharacterData() || aEl
->IsHTMLElement(nsGkAtoms::option
);
263 bool HTMLSelectOptGroupAccessible::HasPrimaryAction() const { return false; }
265 ////////////////////////////////////////////////////////////////////////////////
266 // HTMLComboboxAccessible
267 ////////////////////////////////////////////////////////////////////////////////
269 HTMLComboboxAccessible::HTMLComboboxAccessible(nsIContent
* aContent
,
271 : AccessibleWrap(aContent
, aDoc
) {
272 mType
= eHTMLComboboxType
;
273 mGenericTypes
|= eCombobox
;
274 mStateFlags
|= eNoKidsFromDOM
;
276 if ((nsComboboxControlFrame
*)do_QueryFrame(GetFrame())) {
277 mListAccessible
= new HTMLComboboxListAccessible(mParent
, mContent
, mDoc
);
278 Document()->BindToDocument(mListAccessible
, nullptr);
279 AppendChild(mListAccessible
);
283 ////////////////////////////////////////////////////////////////////////////////
284 // HTMLComboboxAccessible: LocalAccessible
286 role
HTMLComboboxAccessible::NativeRole() const { return roles::COMBOBOX
; }
288 bool HTMLComboboxAccessible::RemoveChild(LocalAccessible
* aChild
) {
289 MOZ_ASSERT(aChild
== mListAccessible
);
290 if (AccessibleWrap::RemoveChild(aChild
)) {
291 mListAccessible
= nullptr;
297 void HTMLComboboxAccessible::Shutdown() {
298 MOZ_ASSERT(!mDoc
|| mDoc
->IsDefunct() || !mListAccessible
);
299 if (mListAccessible
) {
300 mListAccessible
->Shutdown();
301 mListAccessible
= nullptr;
304 AccessibleWrap::Shutdown();
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();
313 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(GetFrame());
314 if (comboFrame
&& comboFrame
->IsDroppedDown()) {
315 state
|= states::EXPANDED
;
317 state
|= states::COLLAPSED
;
320 state
|= states::HASPOPUP
;
324 void HTMLComboboxAccessible::Description(nsString
& aDescription
) const {
325 aDescription
.Truncate();
326 // First check to see if combo box itself has a description, perhaps through
327 // tooltip (title attribute) or via aria-describedby
328 LocalAccessible::Description(aDescription
);
329 if (!aDescription
.IsEmpty()) return;
331 // Otherwise use description of selected option.
332 LocalAccessible
* option
= SelectedOption();
333 if (option
) option
->Description(aDescription
);
336 void HTMLComboboxAccessible::Value(nsString
& aValue
) const {
337 // Use accessible name of selected option.
338 LocalAccessible
* option
= SelectedOption();
339 if (option
) option
->Name(aValue
);
342 bool HTMLComboboxAccessible::HasPrimaryAction() const { return true; }
344 void HTMLComboboxAccessible::ActionNameAt(uint8_t aIndex
, nsAString
& aName
) {
345 if (aIndex
!= HTMLComboboxAccessible::eAction_Click
) return;
347 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(GetFrame());
348 if (!comboFrame
) return;
350 if (comboFrame
->IsDroppedDown()) {
351 aName
.AssignLiteral("close");
353 aName
.AssignLiteral("open");
357 bool HTMLComboboxAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
361 ////////////////////////////////////////////////////////////////////////////////
362 // HTMLComboboxAccessible: Widgets
364 bool HTMLComboboxAccessible::IsWidget() const { return true; }
366 bool HTMLComboboxAccessible::IsActiveWidget() const {
367 return FocusMgr()->HasDOMFocus(mContent
);
370 bool HTMLComboboxAccessible::AreItemsOperable() const {
371 nsComboboxControlFrame
* comboboxFrame
= do_QueryFrame(GetFrame());
372 return comboboxFrame
&& comboboxFrame
->IsDroppedDown();
375 LocalAccessible
* HTMLComboboxAccessible::CurrentItem() const {
376 return AreItemsOperable() ? mListAccessible
->CurrentItem() : nullptr;
379 void HTMLComboboxAccessible::SetCurrentItem(const LocalAccessible
* aItem
) {
380 if (AreItemsOperable()) mListAccessible
->SetCurrentItem(aItem
);
383 ////////////////////////////////////////////////////////////////////////////////
384 // HTMLComboboxAccessible: protected
386 LocalAccessible
* HTMLComboboxAccessible::SelectedOption() const {
387 HTMLSelectElement
* select
= HTMLSelectElement::FromNode(mContent
);
388 int32_t selectedIndex
= select
->SelectedIndex();
390 if (selectedIndex
>= 0) {
391 HTMLOptionElement
* option
= select
->Item(selectedIndex
);
393 DocAccessible
* document
= Document();
394 if (document
) return document
->GetAccessible(option
);
401 ////////////////////////////////////////////////////////////////////////////////
402 // HTMLComboboxListAccessible
403 ////////////////////////////////////////////////////////////////////////////////
405 HTMLComboboxListAccessible::HTMLComboboxListAccessible(LocalAccessible
* aParent
,
406 nsIContent
* aContent
,
408 : HTMLSelectListAccessible(aContent
, aDoc
) {
409 mStateFlags
|= eSharedNode
;
412 ////////////////////////////////////////////////////////////////////////////////
413 // HTMLComboboxAccessible: LocalAccessible
415 role
HTMLComboboxListAccessible::NativeRole() const {
416 return roles::COMBOBOX_LIST
;
419 uint64_t HTMLComboboxListAccessible::NativeState() const {
420 // As a HTMLComboboxListAccessible we can have the following states:
421 // FOCUSED, FOCUSABLE, FLOATING, INVISIBLE
422 // Get focus status from base class
423 uint64_t state
= LocalAccessible::NativeState();
425 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(mParent
->GetFrame());
426 if (comboFrame
&& comboFrame
->IsDroppedDown()) {
427 state
|= states::FLOATING
;
429 state
|= states::INVISIBLE
;
435 nsRect
HTMLComboboxListAccessible::RelativeBounds(
436 nsIFrame
** aBoundingFrame
) const {
437 *aBoundingFrame
= nullptr;
439 LocalAccessible
* comboAcc
= LocalParent();
440 if (!comboAcc
) return nsRect();
442 if (0 == (comboAcc
->State() & states::COLLAPSED
)) {
443 return HTMLSelectListAccessible::RelativeBounds(aBoundingFrame
);
446 // Get the first option.
447 nsIContent
* content
= mContent
->GetFirstChild();
448 if (!content
) return nsRect();
450 nsIFrame
* frame
= content
->GetPrimaryFrame();
452 *aBoundingFrame
= nullptr;
456 *aBoundingFrame
= frame
->GetParent();
457 return (*aBoundingFrame
)->GetRect();
460 bool HTMLComboboxListAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
461 return aEl
->IsAnyOfHTMLElements(nsGkAtoms::option
, nsGkAtoms::optgroup
);
464 ////////////////////////////////////////////////////////////////////////////////
465 // HTMLComboboxListAccessible: Widgets
467 bool HTMLComboboxListAccessible::IsActiveWidget() const {
468 return mParent
&& mParent
->IsActiveWidget();
471 bool HTMLComboboxListAccessible::AreItemsOperable() const {
472 return mParent
&& mParent
->AreItemsOperable();