Repository: nvaccess/nvda

Issue:
ARIA Tabs not accessible as forms controls
ARIA Tabs are not accessible using the shortcut "F" or in the lists of form fields.

### Steps to reproduce:
Open in either Chrome or Firefox the following page : 
https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-1/tabs.html

### Actual behavior:
Tabs are not accessible using the shortcut "F" or in the lists of form fields

### Expected behavior:
Tabs are focused with the "F" key and are listed in the form fields

### System configuration
#### NVDA installed/portable/running from source:
NVDA installed 

#### NVDA version:
2019.2.1

#### Windows version:
Windows 10 version 1803 - OS Build 17134.1069 

#### Name and version of other software in use when reproducing the issue:
Firefox 69.0.3 64 Bits (FR-fr locale)
Chrome Version 77.0.3865.120 (Official build) (64 bits) (FR-fr locale)

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your PC?
Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
No


Discussion:
I think this makes sense. Marking this good for new dev. The places to be are:

* Gecko_ia2._searchableAttribsForNodeType
* mshtml._searchableAttribsForNodeType
* UIABrowseModeDocument._iterNodesByType
I already have a few things ready in this scope. I might try to file a PR quite soon.
My concern is not much technical (the implementation is, as you wisely noted, quite simple) but rather ergonomic: Tab headers and panels are quite specific controls/containers.

For one thing, we believe (here at Accessolutions) that, despite past counter-arguments, tab panels should be considered as containing-regions: (shift+)comma would move to their boundaries, but navigating in/out-side of them would also be announced.

Also, we'd need to have a way to announce the current tab when in a panel.
We've thought about adding the info to what is reported by NVDA+tab, but this would limit to cases where the focus can indeed go inside.
For WebModules, we usually add this info to what is reported by NVDA+t, but many could argue this is just a hack.

Related: How should we handle moving from inside the panel back to the selected tab header?
If we stick on "f" for tab headers, we could consider to target the selected tab rather than the first/last.
Then, a solution could be: first press shift+comma to move to the beginning of the panel then press shift+f to reach the selected header.

What do you think?
In JAWS, all tabs are exposed via the "F" shortcut; but the selected one is not announced (which is an issue). Since tab selection behaves like radio button selection, I think each tab should be accessed via the "F" key, like with radio buttons: wether the current tab is selected should also be announced. 
> In JAWS, all tabs are exposed via the "F" shortcut; but the selected one is not announced (which is an issue). Since tab selection behaves like radio button selection, I think each tab should be accessed via the "F" key, like with radio buttons: wether the current tab is selected should be announced.

I'm glad you mention radio buttons, as I feel there handling can at times be problematic too.
Quick nav considers all items, while tab only stops at the first/last one.
I really feel that tab headers lists and radio buttons groups should both be handled by quick-nav as a whole: When jumping to the whole, the selected item if any should IMHO be the one we reach.
When there are numerous elements in the list, it can be tedious if not error prone to find the selected one - especially as we do not currently have a quick-nav toward it.
Tab headers lists are easier to handle in that respect than radio buttons groups are as, if i'm not wrong, there always ought to be a selected tab header within the list.

Please also see https://github.com/nvaccess/nvda/issues/9227#issuecomment-547291343 for related considerations.

Resolving patch:
diff --git a/source/UIABrowseMode.py b/source/UIABrowseMode.py
--- a/source/UIABrowseMode.py
+++ b/source/UIABrowseMode.py
@@ -1,7 +1,7 @@
-#A part of NonVisual Desktop Access (NVDA)
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
-#Copyright (C) 2015-2017 NV Access Limited, Babbage B.V.
+# A part of NonVisual Desktop Access (NVDA)
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
+# Copyright (C) 2015-2020 NV Access Limited, Babbage B.V., Accessolutions, Julien Cochuyt
 
 from ctypes import byref
 from comtypes import COMError
@@ -11,10 +11,10 @@
 import UIAHandler
 from UIAUtils import (
 	createUIAMultiPropertyCondition,
-	isUIAElementInWalker,
 	getDeepestLastChildUIAElementInWalker,
 	getUIATextAttributeValueFromRange,
-	iterUIARangeByUnit
+	isUIAElementInWalker,
+	iterUIARangeByUnit,
 )
 import documentBase
 import treeInterceptorHandler
@@ -417,8 +417,26 @@ def _iterNodesByType(self,nodeType,direction="next",pos=None):
 			condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_EditControlTypeId,UIAHandler.UIA_ValueIsReadOnlyPropertyId:False},{UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_ComboBoxControlTypeId,UIAHandler.UIA_IsTextPatternAvailablePropertyId:True})
 			return UIAControlQuicknavIterator(nodeType,self,pos,condition,direction)
 		elif nodeType=="formField":
-			condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_EditControlTypeId,UIAHandler.UIA_ValueIsReadOnlyPropertyId:False},{UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_ListControlTypeId,UIAHandler.UIA_IsKeyboardFocusablePropertyId:True},{UIAHandler.UIA_ControlTypePropertyId:[UIAHandler.UIA_CheckBoxControlTypeId,UIAHandler.UIA_RadioButtonControlTypeId,UIAHandler.UIA_ComboBoxControlTypeId,UIAHandler.UIA_ButtonControlTypeId]})
-			return UIAControlQuicknavIterator(nodeType,self,pos,condition,direction)
+			condition = createUIAMultiPropertyCondition(
+				{
+					UIAHandler.UIA_ControlTypePropertyId: UIAHandler.UIA_EditControlTypeId,
+					UIAHandler.UIA_ValueIsReadOnlyPropertyId: False
+				},
+				{
+					UIAHandler.UIA_ControlTypePropertyId: UIAHandler.UIA_ListControlTypeId,
+					UIAHandler.UIA_IsKeyboardFocusablePropertyId: True
+				},
+				{
+					UIAHandler.UIA_ControlTypePropertyId: [
+						UIAHandler.UIA_ButtonControlTypeId,
+						UIAHandler.UIA_CheckBoxControlTypeId,
+						UIAHandler.UIA_ComboBoxControlTypeId,
+						UIAHandler.UIA_RadioButtonControlTypeId,
+						UIAHandler.UIA_TabItemControlTypeId,
+					]
+				},
+			)
+			return UIAControlQuicknavIterator(nodeType, self, pos, condition, direction)
 		elif nodeType == "landmark":
 			condition = UIAHandler.handler.clientObject.createNotCondition(
 				UIAHandler.handler.clientObject.createPropertyCondition(
diff --git a/source/virtualBuffers/MSHTML.py b/source/virtualBuffers/MSHTML.py
--- a/source/virtualBuffers/MSHTML.py
+++ b/source/virtualBuffers/MSHTML.py
@@ -1,8 +1,8 @@
-#virtualBuffers/MSHTML.py
-#A part of NonVisual Desktop Access (NVDA)
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
-#Copyright (C) 2009-2017 NV Access Limited, Babbage B.V.
+# virtualBuffers/MSHTML.py
+# A part of NonVisual Desktop Access (NVDA)
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
+# Copyright (C) 2009-2020 NV Access Limited, Babbage B.V., Accessolutions, Julien Cochuyt
 
 from comtypes import COMError
 import eventHandler
@@ -244,14 +244,29 @@ def _searchableAttribsForNodeType(self,nodeType):
 			attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LINK],"IAccessible::state_%d"%oleacc.STATE_SYSTEM_LINKED:[1],"IAccessible::state_%d"%oleacc.STATE_SYSTEM_TRAVERSED:[None]}
 		elif nodeType=="formField":
 			attrs=[
-				{"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON,oleacc.ROLE_SYSTEM_RADIOBUTTON,oleacc.ROLE_SYSTEM_CHECKBUTTON,oleacc.ROLE_SYSTEM_OUTLINE],"IAccessible::state_%s"%oleacc.STATE_SYSTEM_READONLY:[None]},
-				{"IAccessible::role":[oleacc.ROLE_SYSTEM_COMBOBOX]},
+				{
+					"IAccessible::role": [
+						oleacc.ROLE_SYSTEM_CHECKBUTTON,
+						oleacc.ROLE_SYSTEM_OUTLINE,
+						oleacc.ROLE_SYSTEM_PUSHBUTTON,
+						oleacc.ROLE_SYSTEM_PAGETAB,
+						oleacc.ROLE_SYSTEM_RADIOBUTTON,
+					],
+					f"IAccessible::state_{oleacc.STATE_SYSTEM_READONLY}": [None],
+				},
+				{"IAccessible::role": [oleacc.ROLE_SYSTEM_COMBOBOX]},
 				# Focusable edit fields (input type=text, including readonly ones)
-				{"IAccessible::role":[oleacc.ROLE_SYSTEM_TEXT],"IAccessible::state_%s"%oleacc.STATE_SYSTEM_FOCUSABLE:[1]},
+				{
+					"IAccessible::role": [oleacc.ROLE_SYSTEM_TEXT],
+					f"IAccessible::state_{oleacc.STATE_SYSTEM_FOCUSABLE}": [1],
+				},
 				# Any top-most content editable element (E.g. an editable div for rhich text editing) 
-				{"IHTMLElement::isContentEditable":[1],"parent::IHTMLElement::isContentEditable":[0,None]},
-				{"IHTMLDOMNode::nodeName":["SELECT"]},
-				{"HTMLAttrib::role":["listbox"]},
+				{
+					"IHTMLElement::isContentEditable": [1],
+					"parent::IHTMLElement::isContentEditable": [0, None],
+				},
+				{"IHTMLDOMNode::nodeName": ["SELECT"]},
+				{"HTMLAttrib::role": ["listbox"]},
 			]
 		elif nodeType=="button":
 			attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON]}
diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py
--- a/source/virtualBuffers/gecko_ia2.py
+++ b/source/virtualBuffers/gecko_ia2.py
@@ -1,8 +1,8 @@
-#virtualBuffers/gecko_ia2.py
-#A part of NonVisual Desktop Access (NVDA)
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
-# Copyright (C) 2008-2020 NV Access Limited, Babbage B.V., Mozilla Corporation
+# virtualBuffers/gecko_ia2.py
+# A part of NonVisual Desktop Access (NVDA)
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
+# Copyright (C) 2008-2020 NV Access Limited, Babbage B.V., Mozilla Corporation, Accessolutions, Julien Cochuyt
 
 import weakref
 from . import VirtualBuffer, VirtualBufferTextInfo, VBufStorage_findMatch_word, VBufStorage_findMatch_notEmpty
@@ -325,9 +325,31 @@ def _searchableAttribsForNodeType(self,nodeType):
 			attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LINK],"IAccessible::state_%d"%oleacc.STATE_SYSTEM_LINKED:[1],"IAccessible::state_%d"%oleacc.STATE_SYSTEM_TRAVERSED:[None]}
 		elif nodeType=="formField":
 			attrs=[
-				{"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON,oleacc.ROLE_SYSTEM_BUTTONMENU,oleacc.ROLE_SYSTEM_RADIOBUTTON,oleacc.ROLE_SYSTEM_CHECKBUTTON,oleacc.ROLE_SYSTEM_COMBOBOX,oleacc.ROLE_SYSTEM_LIST,oleacc.ROLE_SYSTEM_OUTLINE,IAccessibleHandler.IA2_ROLE_TOGGLE_BUTTON],"IAccessible::state_%s"%oleacc.STATE_SYSTEM_READONLY:[None]},
-				{"IAccessible::role":[oleacc.ROLE_SYSTEM_COMBOBOX,oleacc.ROLE_SYSTEM_TEXT],"IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[1]},
-				{"IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[1],"parent::IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[None]},
+				{
+					"IAccessible::role": [
+						oleacc.ROLE_SYSTEM_BUTTONMENU,
+						oleacc.ROLE_SYSTEM_CHECKBUTTON,
+						oleacc.ROLE_SYSTEM_COMBOBOX,
+						oleacc.ROLE_SYSTEM_LIST,
+						oleacc.ROLE_SYSTEM_OUTLINE,
+						oleacc.ROLE_SYSTEM_PUSHBUTTON,
+						oleacc.ROLE_SYSTEM_RADIOBUTTON,
+						oleacc.ROLE_SYSTEM_PAGETAB,
+						IAccessibleHandler.IA2_ROLE_TOGGLE_BUTTON,
+					],
+					f"IAccessible::state_{oleacc.STATE_SYSTEM_READONLY}": [None],
+				},
+				{
+					"IAccessible::role": [
+						oleacc.ROLE_SYSTEM_COMBOBOX,
+						oleacc.ROLE_SYSTEM_TEXT
+					],
+					f"IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [1],
+				},
+				{
+					f"IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [1],
+					f"parent::IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [None],
+				},
 			]
 		elif nodeType=="list":
 			attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LIST]}


Test patch: