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 "nsAccessibilityService.h"
10 #include "nsAccUtils.h"
11 #include "DocAccessible.h"
12 #include "nsEventShell.h"
13 #include "nsTextEquivUtils.h"
18 #include "mozilla/dom/HTMLOptionElement.h"
19 #include "mozilla/dom/HTMLOptGroupElement.h"
20 #include "mozilla/dom/HTMLSelectElement.h"
21 #include "nsComboboxControlFrame.h"
22 #include "nsContainerFrame.h"
23 #include "nsListControlFrame.h"
25 using namespace mozilla::a11y
;
26 using namespace mozilla::dom
;
28 ////////////////////////////////////////////////////////////////////////////////
29 // HTMLSelectListAccessible
30 ////////////////////////////////////////////////////////////////////////////////
32 HTMLSelectListAccessible::HTMLSelectListAccessible(nsIContent
* aContent
,
34 : AccessibleWrap(aContent
, aDoc
) {
35 mGenericTypes
|= eListControl
| eSelect
;
38 ////////////////////////////////////////////////////////////////////////////////
39 // HTMLSelectListAccessible: LocalAccessible public
41 uint64_t HTMLSelectListAccessible::NativeState() const {
42 uint64_t state
= AccessibleWrap::NativeState();
43 if (mContent
->AsElement()->HasAttr(kNameSpaceID_None
, nsGkAtoms::multiple
)) {
44 state
|= states::MULTISELECTABLE
| states::EXTSELECTABLE
;
50 role
HTMLSelectListAccessible::NativeRole() const { return roles::LISTBOX
; }
52 ////////////////////////////////////////////////////////////////////////////////
53 // HTMLSelectListAccessible: SelectAccessible
55 bool HTMLSelectListAccessible::SelectAll() {
56 return mContent
->AsElement()->HasAttr(kNameSpaceID_None
, nsGkAtoms::multiple
)
57 ? AccessibleWrap::SelectAll()
61 bool HTMLSelectListAccessible::UnselectAll() {
62 return mContent
->AsElement()->HasAttr(kNameSpaceID_None
, nsGkAtoms::multiple
)
63 ? AccessibleWrap::UnselectAll()
67 ////////////////////////////////////////////////////////////////////////////////
68 // HTMLSelectListAccessible: Widgets
70 bool HTMLSelectListAccessible::IsWidget() const { return true; }
72 bool HTMLSelectListAccessible::IsActiveWidget() const {
73 return FocusMgr()->HasDOMFocus(mContent
);
76 bool HTMLSelectListAccessible::AreItemsOperable() const { return true; }
78 LocalAccessible
* HTMLSelectListAccessible::CurrentItem() const {
79 nsListControlFrame
* listControlFrame
= do_QueryFrame(GetFrame());
80 if (listControlFrame
) {
81 nsCOMPtr
<nsIContent
> activeOptionNode
=
82 listControlFrame
->GetCurrentOption();
83 if (activeOptionNode
) {
84 DocAccessible
* document
= Document();
85 if (document
) return document
->GetAccessible(activeOptionNode
);
91 void HTMLSelectListAccessible::SetCurrentItem(const LocalAccessible
* aItem
) {
92 if (!aItem
->GetContent()->IsElement()) return;
94 aItem
->GetContent()->AsElement()->SetAttr(
95 kNameSpaceID_None
, nsGkAtoms::selected
, u
"true"_ns
, true);
98 bool HTMLSelectListAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
99 return aEl
->IsAnyOfHTMLElements(nsGkAtoms::option
, nsGkAtoms::optgroup
);
102 bool HTMLSelectListAccessible::AttributeChangesState(nsAtom
* aAttribute
) {
103 return aAttribute
== nsGkAtoms::multiple
||
104 LocalAccessible::AttributeChangesState(aAttribute
);
107 ////////////////////////////////////////////////////////////////////////////////
108 // HTMLSelectOptionAccessible
109 ////////////////////////////////////////////////////////////////////////////////
111 HTMLSelectOptionAccessible::HTMLSelectOptionAccessible(nsIContent
* aContent
,
113 : HyperTextAccessibleWrap(aContent
, aDoc
) {}
115 ////////////////////////////////////////////////////////////////////////////////
116 // HTMLSelectOptionAccessible: LocalAccessible public
118 role
HTMLSelectOptionAccessible::NativeRole() const {
119 if (GetCombobox()) return roles::COMBOBOX_OPTION
;
121 return roles::OPTION
;
124 ENameValueFlag
HTMLSelectOptionAccessible::NativeName(nsString
& aName
) const {
125 if (auto* option
= dom::HTMLOptionElement::FromNode(mContent
)) {
126 option
->GetAttr(nsGkAtoms::label
, aName
);
127 if (!aName
.IsEmpty()) {
130 option
->GetText(aName
);
131 return eNameFromSubtree
;
133 if (auto* group
= dom::HTMLOptGroupElement::FromNode(mContent
)) {
134 group
->GetLabel(aName
);
135 return aName
.IsEmpty() ? eNameOK
: eNameFromSubtree
;
137 MOZ_ASSERT_UNREACHABLE("What content do we have?");
138 return eNameFromSubtree
;
141 void HTMLSelectOptionAccessible::DOMAttributeChanged(
142 int32_t aNameSpaceID
, nsAtom
* aAttribute
, int32_t aModType
,
143 const nsAttrValue
* aOldValue
, uint64_t aOldState
) {
144 HyperTextAccessibleWrap::DOMAttributeChanged(aNameSpaceID
, aAttribute
,
145 aModType
, aOldValue
, aOldState
);
147 if (aAttribute
== nsGkAtoms::label
) {
148 dom::Element
* elm
= Elm();
149 if (!nsAccUtils::HasARIAAttr(elm
, nsGkAtoms::aria_labelledby
) &&
150 !nsAccUtils::HasARIAAttr(elm
, nsGkAtoms::aria_label
)) {
151 mDoc
->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE
, this);
156 uint64_t HTMLSelectOptionAccessible::NativeState() const {
157 // As a HTMLSelectOptionAccessible we can have the following states:
158 // SELECTABLE, SELECTED, FOCUSED, FOCUSABLE, OFFSCREEN
159 // Upcall to LocalAccessible, but skip HyperTextAccessible impl
160 // because we don't want EDITABLE or SELECTABLE_TEXT
161 uint64_t state
= LocalAccessible::NativeState();
163 LocalAccessible
* select
= GetSelect();
164 if (!select
) return state
;
166 uint64_t selectState
= select
->State();
167 if (selectState
& states::INVISIBLE
) return state
;
170 HTMLOptionElement
* option
= HTMLOptionElement::FromNode(mContent
);
171 bool selected
= option
&& option
->Selected();
172 if (selected
) state
|= states::SELECTED
;
174 if (selectState
& states::OFFSCREEN
) {
175 state
|= states::OFFSCREEN
;
176 } else if (selectState
& states::COLLAPSED
) {
177 // <select> is COLLAPSED: add OFFSCREEN, if not the currently
180 state
|= states::OFFSCREEN
;
181 // Ensure the invisible state is removed. Otherwise, group info will skip
182 // this option. Furthermore, this gets cached and this doesn't get
183 // invalidated even once the select is expanded.
184 state
&= ~states::INVISIBLE
;
186 // Clear offscreen and invisible for currently showing option
187 state
&= ~(states::OFFSCREEN
| states::INVISIBLE
);
188 state
|= selectState
& states::OPAQUE1
;
191 // XXX list frames are weird, don't rely on LocalAccessible's general
192 // visibility implementation unless they get reimplemented in layout
193 state
&= ~states::OFFSCREEN
;
194 // <select> is not collapsed: compare bounds to calculate OFFSCREEN
195 LocalAccessible
* listAcc
= LocalParent();
197 LayoutDeviceIntRect optionRect
= Bounds();
198 LayoutDeviceIntRect listRect
= listAcc
->Bounds();
199 if (optionRect
.Y() < listRect
.Y() ||
200 optionRect
.YMost() > listRect
.YMost()) {
201 state
|= states::OFFSCREEN
;
209 uint64_t HTMLSelectOptionAccessible::NativeInteractiveState() const {
210 return NativelyUnavailable() ? states::UNAVAILABLE
211 : states::FOCUSABLE
| states::SELECTABLE
;
214 nsRect
HTMLSelectOptionAccessible::RelativeBounds(
215 nsIFrame
** aBoundingFrame
) const {
216 LocalAccessible
* combobox
= GetCombobox();
217 if (combobox
&& (combobox
->State() & states::COLLAPSED
)) {
218 return combobox
->RelativeBounds(aBoundingFrame
);
221 return HyperTextAccessibleWrap::RelativeBounds(aBoundingFrame
);
224 void HTMLSelectOptionAccessible::ActionNameAt(uint8_t aIndex
,
226 if (aIndex
== eAction_Select
) aName
.AssignLiteral("select");
229 bool HTMLSelectOptionAccessible::HasPrimaryAction() const { return true; }
231 void HTMLSelectOptionAccessible::SetSelected(bool aSelect
) {
232 HTMLOptionElement
* option
= HTMLOptionElement::FromNode(mContent
);
233 if (option
) option
->SetSelected(aSelect
);
236 ////////////////////////////////////////////////////////////////////////////////
237 // HTMLSelectOptionAccessible: Widgets
239 LocalAccessible
* HTMLSelectOptionAccessible::ContainerWidget() const {
240 LocalAccessible
* parent
= LocalParent();
241 if (parent
&& parent
->IsHTMLOptGroup()) {
242 parent
= parent
->LocalParent();
245 return parent
&& parent
->IsListControl() ? parent
: nullptr;
248 ////////////////////////////////////////////////////////////////////////////////
249 // HTMLSelectOptGroupAccessible
250 ////////////////////////////////////////////////////////////////////////////////
252 role
HTMLSelectOptGroupAccessible::NativeRole() const {
253 return roles::GROUPING
;
256 uint64_t HTMLSelectOptGroupAccessible::NativeInteractiveState() const {
257 return NativelyUnavailable() ? states::UNAVAILABLE
: 0;
260 bool HTMLSelectOptGroupAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
261 return aEl
->IsCharacterData() || aEl
->IsHTMLElement(nsGkAtoms::option
);
264 bool HTMLSelectOptGroupAccessible::HasPrimaryAction() const { return false; }
266 ////////////////////////////////////////////////////////////////////////////////
267 // HTMLComboboxAccessible
268 ////////////////////////////////////////////////////////////////////////////////
270 HTMLComboboxAccessible::HTMLComboboxAccessible(nsIContent
* aContent
,
272 : AccessibleWrap(aContent
, aDoc
) {
273 mType
= eHTMLComboboxType
;
274 mGenericTypes
|= eCombobox
;
275 mStateFlags
|= eNoKidsFromDOM
;
277 if ((nsComboboxControlFrame
*)do_QueryFrame(GetFrame())) {
278 mListAccessible
= new HTMLComboboxListAccessible(mParent
, mContent
, mDoc
);
279 Document()->BindToDocument(mListAccessible
, nullptr);
280 AppendChild(mListAccessible
);
284 ////////////////////////////////////////////////////////////////////////////////
285 // HTMLComboboxAccessible: LocalAccessible
287 role
HTMLComboboxAccessible::NativeRole() const { return roles::COMBOBOX
; }
289 bool HTMLComboboxAccessible::RemoveChild(LocalAccessible
* aChild
) {
290 MOZ_ASSERT(aChild
== mListAccessible
);
291 if (AccessibleWrap::RemoveChild(aChild
)) {
292 mListAccessible
= nullptr;
298 void HTMLComboboxAccessible::Shutdown() {
299 MOZ_ASSERT(!mDoc
|| mDoc
->IsDefunct() || !mListAccessible
);
300 if (mListAccessible
) {
301 mListAccessible
->Shutdown();
302 mListAccessible
= nullptr;
305 AccessibleWrap::Shutdown();
308 uint64_t HTMLComboboxAccessible::NativeState() const {
309 // As a HTMLComboboxAccessible we can have the following states:
310 // FOCUSED, FOCUSABLE, HASPOPUP, EXPANDED, COLLAPSED
311 // Get focus status from base class
312 uint64_t state
= LocalAccessible::NativeState();
314 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(GetFrame());
315 if (comboFrame
&& comboFrame
->IsDroppedDown()) {
316 state
|= states::EXPANDED
;
318 state
|= states::COLLAPSED
;
321 state
|= states::HASPOPUP
;
325 void HTMLComboboxAccessible::Description(nsString
& aDescription
) const {
326 aDescription
.Truncate();
327 // First check to see if combo box itself has a description, perhaps through
328 // tooltip (title attribute) or via aria-describedby
329 LocalAccessible::Description(aDescription
);
330 if (!aDescription
.IsEmpty()) return;
332 // Otherwise use description of selected option.
333 LocalAccessible
* option
= SelectedOption();
334 if (option
) option
->Description(aDescription
);
337 void HTMLComboboxAccessible::Value(nsString
& aValue
) const {
338 // Use accessible name of selected option.
339 LocalAccessible
* option
= SelectedOption();
340 if (option
) option
->Name(aValue
);
343 bool HTMLComboboxAccessible::HasPrimaryAction() const { return true; }
345 void HTMLComboboxAccessible::ActionNameAt(uint8_t aIndex
, nsAString
& aName
) {
346 if (aIndex
!= HTMLComboboxAccessible::eAction_Click
) return;
348 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(GetFrame());
349 if (!comboFrame
) return;
351 if (comboFrame
->IsDroppedDown()) {
352 aName
.AssignLiteral("close");
354 aName
.AssignLiteral("open");
358 bool HTMLComboboxAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
362 ////////////////////////////////////////////////////////////////////////////////
363 // HTMLComboboxAccessible: Widgets
365 bool HTMLComboboxAccessible::IsWidget() const { return true; }
367 bool HTMLComboboxAccessible::IsActiveWidget() const {
368 return FocusMgr()->HasDOMFocus(mContent
);
371 bool HTMLComboboxAccessible::AreItemsOperable() const {
372 nsComboboxControlFrame
* comboboxFrame
= do_QueryFrame(GetFrame());
373 return comboboxFrame
&& comboboxFrame
->IsDroppedDown();
376 LocalAccessible
* HTMLComboboxAccessible::CurrentItem() const {
377 return AreItemsOperable() ? mListAccessible
->CurrentItem() : nullptr;
380 void HTMLComboboxAccessible::SetCurrentItem(const LocalAccessible
* aItem
) {
381 if (AreItemsOperable()) mListAccessible
->SetCurrentItem(aItem
);
384 ////////////////////////////////////////////////////////////////////////////////
385 // HTMLComboboxAccessible: protected
387 LocalAccessible
* HTMLComboboxAccessible::SelectedOption() const {
388 HTMLSelectElement
* select
= HTMLSelectElement::FromNode(mContent
);
389 int32_t selectedIndex
= select
->SelectedIndex();
391 if (selectedIndex
>= 0) {
392 HTMLOptionElement
* option
= select
->Item(selectedIndex
);
394 DocAccessible
* document
= Document();
395 if (document
) return document
->GetAccessible(option
);
402 ////////////////////////////////////////////////////////////////////////////////
403 // HTMLComboboxListAccessible
404 ////////////////////////////////////////////////////////////////////////////////
406 HTMLComboboxListAccessible::HTMLComboboxListAccessible(LocalAccessible
* aParent
,
407 nsIContent
* aContent
,
409 : HTMLSelectListAccessible(aContent
, aDoc
) {
410 mStateFlags
|= eSharedNode
;
413 ////////////////////////////////////////////////////////////////////////////////
414 // HTMLComboboxAccessible: LocalAccessible
416 role
HTMLComboboxListAccessible::NativeRole() const {
417 return roles::COMBOBOX_LIST
;
420 uint64_t HTMLComboboxListAccessible::NativeState() const {
421 // As a HTMLComboboxListAccessible we can have the following states:
422 // FOCUSED, FOCUSABLE, FLOATING, INVISIBLE
423 // Get focus status from base class
424 uint64_t state
= LocalAccessible::NativeState();
426 nsComboboxControlFrame
* comboFrame
= do_QueryFrame(mParent
->GetFrame());
427 if (comboFrame
&& comboFrame
->IsDroppedDown()) {
428 state
|= states::FLOATING
;
430 state
|= states::INVISIBLE
;
436 nsRect
HTMLComboboxListAccessible::RelativeBounds(
437 nsIFrame
** aBoundingFrame
) const {
438 *aBoundingFrame
= nullptr;
440 LocalAccessible
* comboAcc
= LocalParent();
441 if (!comboAcc
) return nsRect();
443 if (0 == (comboAcc
->State() & states::COLLAPSED
)) {
444 return HTMLSelectListAccessible::RelativeBounds(aBoundingFrame
);
447 // Get the first option.
448 nsIContent
* content
= mContent
->GetFirstChild();
449 if (!content
) return nsRect();
451 nsIFrame
* frame
= content
->GetPrimaryFrame();
453 *aBoundingFrame
= nullptr;
457 *aBoundingFrame
= frame
->GetParent();
458 return (*aBoundingFrame
)->GetRect();
461 bool HTMLComboboxListAccessible::IsAcceptableChild(nsIContent
* aEl
) const {
462 return aEl
->IsAnyOfHTMLElements(nsGkAtoms::option
, nsGkAtoms::optgroup
);
465 ////////////////////////////////////////////////////////////////////////////////
466 // HTMLComboboxListAccessible: Widgets
468 bool HTMLComboboxListAccessible::IsActiveWidget() const {
469 return mParent
&& mParent
->IsActiveWidget();
472 bool HTMLComboboxListAccessible::AreItemsOperable() const {
473 return mParent
&& mParent
->AreItemsOperable();