Skip to content

Commit 129dd0b

Browse files
Merge 9ed6f12 into 8e17d44
2 parents 8e17d44 + 9ed6f12 commit 129dd0b

10 files changed

Lines changed: 145 additions & 48 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@
4343
[submodule "include/javaAccessBridge32"]
4444
path = include/javaAccessBridge32
4545
url = https://github.com/nvaccess/javaAccessBridge32-bin.git
46+
[submodule "include/w3c-aria-practices"]
47+
path = include/w3c-aria-practices
48+
url = https://github.com/w3c/aria-practices

include/w3c-aria-practices

Submodule w3c-aria-practices added at 78bb9a0

nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ This license can be found at:
3333

3434
using namespace std;
3535

36+
map<wstring,wstring> createMapOfIA2AttributesFromPacc(IAccessible2* pacc) {
37+
map<wstring,wstring> IA2AttribsMap;
38+
CComBSTR IA2Attributes;
39+
if(pacc->get_attributes(&IA2Attributes) == S_OK) {
40+
IA2AttribsToMap(IA2Attributes.m_str,IA2AttribsMap);
41+
}
42+
return IA2AttribsMap;
43+
}
44+
45+
bool hasXmlRoleAttribContainingValue(const map<wstring,wstring>& attribsMap, const wstring roleName) {
46+
const auto attribsMapIt = attribsMap.find(L"xml-roles");
47+
return attribsMapIt != attribsMap.end() && attribsMapIt->second.find(roleName) != wstring::npos;
48+
}
49+
3650
CComPtr<IAccessible2> GeckoVBufBackend_t::getLabelElement(IAccessible2_2* element) {
3751
IUnknown** ppUnk=nullptr;
3852
long nTargets=0;
@@ -422,6 +436,16 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
422436
nhAssert(parentNode); //new node must have been created
423437
previousNode=NULL;
424438

439+
//get IA2Attributes -- IAccessible2 attributes;
440+
map<wstring,wstring>::const_iterator IA2AttribsMapIt;
441+
auto IA2AttribsMap = createMapOfIA2AttributesFromPacc(pacc);
442+
// Add all IA2 attributes on the node
443+
for(const auto& [key, val]: IA2AttribsMap) {
444+
wstring attribName = L"IAccessible2::attribute_";
445+
attribName += key;
446+
parentNode->addAttribute(attribName, val);
447+
}
448+
425449
//Get role -- IAccessible2 role
426450
long role=0;
427451
BSTR roleString=NULL;
@@ -438,6 +462,15 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
438462
else if(varRole.vt==VT_BSTR)
439463
roleString=varRole.bstrVal;
440464
}
465+
466+
// Specifically force the role of ARIA treegrids from outline to table.
467+
// We do this very early on in the rendering so that all our table logic applies.
468+
if(role == ROLE_SYSTEM_OUTLINE) {
469+
if(hasXmlRoleAttribContainingValue(IA2AttribsMap, L"treegrid")) {
470+
role = ROLE_SYSTEM_TABLE;
471+
}
472+
}
473+
441474
//Add role as an attrib
442475
if(roleString)
443476
s<<roleString;
@@ -495,22 +528,6 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
495528
} else
496529
parentNode->addAttribute(L"keyboardShortcut",L"");
497530

498-
//get IA2Attributes -- IAccessible2 attributes;
499-
BSTR IA2Attributes;
500-
map<wstring,wstring> IA2AttribsMap;
501-
if(pacc->get_attributes(&IA2Attributes)==S_OK) {
502-
IA2AttribsToMap(IA2Attributes,IA2AttribsMap);
503-
SysFreeString(IA2Attributes);
504-
// Add each IA2 attribute as an attrib.
505-
for(map<wstring,wstring>::const_iterator it=IA2AttribsMap.begin();it!=IA2AttribsMap.end();++it) {
506-
s<<L"IAccessible2::attribute_"<<it->first;
507-
parentNode->addAttribute(s.str(),it->second);
508-
s.str(L"");
509-
}
510-
} else
511-
LOG_DEBUG(L"pacc->get_attributes failed");
512-
map<wstring,wstring>::const_iterator IA2AttribsMapIt;
513-
514531
//Check IA2Attributes, and or the role etc to work out if this object is a block element
515532
bool isBlockElement=TRUE;
516533
if(IA2States&IA2_STATE_MULTI_LINE) {
@@ -673,9 +690,10 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
673690
} else {
674691
// If a node has children, it's visible.
675692
isVisible = width > 0 && height > 0 || childCount > 0;
676-
if ((role == ROLE_SYSTEM_LIST && !(states & STATE_SYSTEM_READONLY))
677-
|| role == ROLE_SYSTEM_OUTLINE
678-
) {
693+
// Only render the selected item for interactive lists.
694+
if (role == ROLE_SYSTEM_LIST && !(states & STATE_SYSTEM_READONLY)) {
695+
renderSelectedItemOnly = true;
696+
} else if(role == ROLE_SYSTEM_OUTLINE) {
679697
renderSelectedItemOnly = true;
680698
}
681699
if (IA2TextIsUnneededSpace

source/NVDAObjects/IAccessible/ia2Web.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ class BlockQuote(Ia2Web):
112112
role = controlTypes.ROLE_BLOCKQUOTE
113113

114114

115+
class Treegrid(Ia2Web):
116+
role = controlTypes.ROLE_TABLE
117+
118+
115119
class Article(Ia2Web):
116120
role = controlTypes.ROLE_ARTICLE
117121

@@ -222,6 +226,8 @@ def findExtraOverlayClasses(obj, clsList, baseClass=Ia2Web, documentClass=None):
222226
xmlRoles = obj.IA2Attributes.get("xml-roles", "").split(" ")
223227
if iaRole == IAccessibleHandler.IA2_ROLE_SECTION and obj.IA2Attributes.get("tag", None) == "blockquote":
224228
clsList.append(BlockQuote)
229+
elif iaRole == oleacc.ROLE_SYSTEM_OUTLINE and "treegrid" in xmlRoles:
230+
clsList.append(Treegrid)
225231
elif iaRole == oleacc.ROLE_SYSTEM_DOCUMENT and xmlRoles[0] == "article":
226232
clsList.append(Article)
227233
elif xmlRoles[0] == "region" and obj.name:

source/speech/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,18 @@ def getControlFieldSpeech( # noqa: C901
19101910
out = []
19111911
if ariaCurrent:
19121912
out.extend(ariaCurrentSequence)
1913+
# Speak expanded / collapsed / level for treeview items (in ARIA treegrids)
1914+
if role == controlTypes.ROLE_TREEVIEWITEM:
1915+
if controlTypes.STATE_EXPANDED in states:
1916+
out.extend(
1917+
getPropertiesSpeech(reason=reason, states={controlTypes.STATE_EXPANDED}, _role=role)
1918+
)
1919+
elif controlTypes.STATE_COLLAPSED in states:
1920+
out.extend(
1921+
getPropertiesSpeech(reason=reason, states={controlTypes.STATE_COLLAPSED}, _role=role)
1922+
)
1923+
if levelSequence:
1924+
out.extend(levelSequence)
19131925
if role == controlTypes.ROLE_GRAPHIC and content:
19141926
out.append(content)
19151927
types.logBadSequenceTypes(out)

source/virtualBuffers/gecko_ia2.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ def _normalizeControlField(self,attrs):
9494
# This is a named link destination, not a link which can be activated. The user doesn't care about these.
9595
role=controlTypes.ROLE_TEXTFRAME
9696
level=attrs.get('IAccessible2::attribute_level',"")
97-
98-
xmlRoles=attrs.get("IAccessible2::attribute_xml-roles", "").split(" ")
97+
xmlRoles = attrs.get("IAccessible2::attribute_xml-roles", "").split(" ")
9998
landmark = next((xr for xr in xmlRoles if xr in aria.landmarkRoles), None)
10099
if landmark and role != controlTypes.ROLE_LANDMARK and landmark != xmlRoles[0]:
101100
# Ignore the landmark role

tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,6 @@ def _onNvdaSpeech(self, speechSequence=None):
7777
self._lastSpeechTime_requiresLock = _timer()
7878
self._nvdaSpeech_requiresLock.append(speechSequence)
7979

80-
@staticmethod
81-
def _flattenCommandsSeparatingWithNewline(commandArray):
82-
"""
83-
Flatten many collections of speech sequences into a single speech sequence. Each original speech sequence
84-
is separated by a newline string.
85-
@param commandArray: is a collection of speechSequences
86-
@return: speechSequence
87-
"""
88-
f = [c for commands in commandArray for newlineJoined in [commands, [u"\n"]] for c in newlineJoined]
89-
return f
90-
9180
@staticmethod
9281
def _getJoinedBaseStringsFromCommands(speechCommandArray) -> str:
9382
baseStrings = [c for c in speechCommandArray if isinstance(c, str)]
@@ -103,11 +92,10 @@ def get_speech_at_index_until_now(self, speechIndex: int) -> str:
10392
@return: The speech joined together, see L{_getJoinedBaseStringsFromCommands}
10493
"""
10594
with threading.Lock():
106-
speechCommands = self._flattenCommandsSeparatingWithNewline(
107-
self._nvdaSpeech_requiresLock[speechIndex:]
108-
)
109-
joined = self._getJoinedBaseStringsFromCommands(speechCommands)
110-
return joined
95+
speechCommands = [
96+
self._getJoinedBaseStringsFromCommands(x) for x in self._nvdaSpeech_requiresLock[speechIndex:]
97+
]
98+
return "\n".join(x for x in speechCommands if x and not x.isspace())
11199

112100
def get_last_speech_index(self) -> int:
113101
with threading.Lock():

tests/system/robot/chromeTests.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""Logic for NVDA + Google Chrome tests
77
"""
88

9+
import os
910
from robot.libraries.BuiltIn import BuiltIn
1011
# imported methods start with underscore (_) so they don't get imported into robot files as keywords
1112
from SystemTestSpy import (
@@ -15,12 +16,18 @@
1516
# Imported for type information
1617
from ChromeLib import ChromeLib as _ChromeLib
1718
from AssertsLib import AssertsLib as _AssertsLib
19+
import NvdaLib as _NvdaLib
1820

1921
_builtIn: BuiltIn = BuiltIn()
2022
_chrome: _ChromeLib = _getLib("ChromeLib")
2123
_asserts: _AssertsLib = _getLib("AssertsLib")
2224

2325

26+
ARIAExamplesDir = os.path.join(
27+
_NvdaLib._locations.repoRoot, "include", "w3c-aria-practices", "examples"
28+
)
29+
30+
2431
def checkbox_labelled_by_inner_element():
2532
_chrome.prepareChrome(
2633
r"""
@@ -97,7 +104,7 @@ def announce_list_item_when_moving_by_word_or_character():
97104
_asserts.strings_match(
98105
actualSpeech,
99106
"\n".join([
100-
"list item level 1 ",
107+
"list item level 1",
101108
"b"
102109
])
103110
)
@@ -204,7 +211,7 @@ def test_pr11606():
204211
_asserts.strings_match(
205212
actualSpeech,
206213
"\n".join([
207-
"out of link ",
214+
"out of link",
208215
"space"
209216
])
210217
)
@@ -222,3 +229,57 @@ def test_pr11606():
222229
actualSpeech,
223230
"bullet link A link B"
224231
)
232+
233+
234+
def test_ariaTreeGrid_browseMode():
235+
"""
236+
Ensure that ARIA treegrids are accessible as a standard table in browse mode.
237+
"""
238+
testFile = os.path.join(ARIAExamplesDir, "treegrid", "treegrid-1.html")
239+
_chrome.prepareChrome(
240+
f"""
241+
<iframe src="{testFile}" />
242+
"""
243+
)
244+
# Jump to the heading in the aria treegrid iframe
245+
actualSpeech = _chrome.getSpeechAfterKey("h")
246+
_asserts.strings_match(
247+
actualSpeech,
248+
"frame main landmark Treegrid Email Inbox Example heading level 1"
249+
)
250+
# Jump to the ARIA treegrid with the next table quicknav command.
251+
# The browse mode caret will be inside the table on the caption before the first row.
252+
actualSpeech = _chrome.getSpeechAfterKey("t")
253+
_asserts.strings_match(
254+
actualSpeech,
255+
"Inbox table clickable with 5 rows and 3 columns Inbox"
256+
)
257+
# Move past the caption onto row 1 with downArrow
258+
actualSpeech = _chrome.getSpeechAfterKey("downArrow")
259+
_asserts.strings_match(
260+
actualSpeech,
261+
"row 1 Subject column 1 Subject"
262+
)
263+
# Navigate to row 2 column 1 with NVDA table navigation command
264+
actualSpeech = _chrome.getSpeechAfterKey("control+alt+downArrow")
265+
_asserts.strings_match(
266+
actualSpeech,
267+
"expanded level 1 row 2 Treegrids are awesome"
268+
)
269+
# Press enter to activate NVDA focus mode and focus the current row
270+
actualSpeech = _chrome.getSpeechAfterKey("enter")
271+
_asserts.strings_match(
272+
actualSpeech,
273+
"\n".join([
274+
# focus mode turns on
275+
"Focus mode",
276+
# focus enters the document inside the iframe
277+
"Treegrid Email Inbox Example WAI ARIA Authoring Practices 1.2 document",
278+
# Focus enters the main landmark
279+
"main landmark",
280+
# Focus enters the ARIA treegrid (table)
281+
"Inbox table",
282+
# Focus lands on row 2
283+
"level 1 Treegrids are awesome Want to learn how to use them? aaron at thegoogle dot rocks expanded",
284+
])
285+
)

tests/system/robot/chromeTests.robot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ i7562
3535
pr11606
3636
[Documentation] Announce the correct line when placed at the end of a link at the end of a list item in a contenteditable
3737
test_pr11606
38+
ARIA treegrid
39+
[Documentation] Ensure that ARIA treegrids are accessible as a standard table in browse mode.
40+
test_ariaTreeGrid_browseMode

tests/system/robot/startupShutdownNVDA.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ def quits_from_keyboard():
4848

4949
_asserts.strings_match(
5050
actualSpeech,
51-
"Exit NVDA dialog \n"
52-
"What would you like to do? combo box Exit collapsed Alt plus d"
51+
"\n".join([
52+
"Exit NVDA dialog",
53+
"What would you like to do? combo box Exit collapsed Alt plus d"
54+
])
5355
)
5456
_builtIn.sleep(1) # the dialog is not always receiving the enter keypress, wait a little longer for it
5557
spy.emulateKeyPress("enter", blockUntilProcessed=False)
@@ -65,13 +67,17 @@ def read_welcome_dialog():
6567

6668
_asserts.strings_match(
6769
actualSpeech,
68-
"Welcome to NVDA dialog Welcome to NVDA! Most commands for controlling NVDA require you to hold "
69-
"down the NVDA key while pressing other keys. By default, the numpad Insert and main Insert keys "
70-
"may both be used as the NVDA key. You can also configure NVDA to use the Caps Lock as the NVDA "
71-
"key. Press NVDA plus n at any time to activate the NVDA menu. From this menu, you can configure "
72-
"NVDA, get help and access other NVDA functions. \n"
73-
"Options grouping \n"
74-
"Keyboard layout: combo box desktop collapsed Alt plus k"
70+
"\n".join([
71+
(
72+
"Welcome to NVDA dialog Welcome to NVDA! Most commands for controlling NVDA require you to hold "
73+
"down the NVDA key while pressing other keys. By default, the numpad Insert and main Insert keys "
74+
"may both be used as the NVDA key. You can also configure NVDA to use the Caps Lock as the NVDA "
75+
"key. Press NVDA plus n at any time to activate the NVDA menu. From this menu, you can configure "
76+
"NVDA, get help and access other NVDA functions."
77+
),
78+
"Options grouping",
79+
"Keyboard layout: combo box desktop collapsed Alt plus k"
80+
])
7581
)
7682
_builtIn.sleep(1) # the dialog is not always receiving the enter keypress, wait a little longer for it
7783
spy.emulateKeyPress("enter")

0 commit comments

Comments
 (0)