Skip to content

Commit f4e739c

Browse files
Merge e41690b into 3abcc51
2 parents 3abcc51 + e41690b commit f4e739c

12 files changed

Lines changed: 152 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/ChromeLib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def getSpeechAfterKey(key) -> str:
125125
spy.wait_for_speech_to_finish()
126126
nextSpeechIndex = spy.get_next_speech_index()
127127
spy.emulateKeyPress(key)
128+
spy.wait_for_speech_to_finish()
128129
speech = spy.get_speech_at_index_until_now(nextSpeechIndex)
129130
return speech
130131

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: 68 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,62 @@ 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 first heading in the 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+
# Tab to the first link.
251+
# This ensures that focus is totally within the iframe
252+
# so as to not cause focus to hit the iframe's document
253+
# when entering focus mode on the treegrid later.
254+
actualSpeech = _chrome.getSpeechAfterKey("tab")
255+
_asserts.strings_match(
256+
actualSpeech,
257+
"issue 790. link"
258+
)
259+
# Jump to the ARIA treegrid with the next table quicknav command.
260+
# The browse mode caret will be inside the table on the caption before the first row.
261+
actualSpeech = _chrome.getSpeechAfterKey("t")
262+
_asserts.strings_match(
263+
actualSpeech,
264+
"Inbox table clickable with 5 rows and 3 columns Inbox"
265+
)
266+
# Move past the caption onto row 1 with downArrow
267+
actualSpeech = _chrome.getSpeechAfterKey("downArrow")
268+
_asserts.strings_match(
269+
actualSpeech,
270+
"row 1 Subject column 1 Subject"
271+
)
272+
# Navigate to row 2 column 1 with NVDA table navigation command
273+
actualSpeech = _chrome.getSpeechAfterKey("control+alt+downArrow")
274+
_asserts.strings_match(
275+
actualSpeech,
276+
"expanded level 1 row 2 Treegrids are awesome"
277+
)
278+
# Press enter to activate NVDA focus mode and focus the current row
279+
actualSpeech = _chrome.getSpeechAfterKey("enter")
280+
_asserts.strings_match(
281+
actualSpeech,
282+
"\n".join([
283+
# focus mode turns on
284+
"Focus mode",
285+
# Focus enters the ARIA treegrid (table)
286+
"Inbox table",
287+
# Focus lands on row 2
288+
"level 1 Treegrids are awesome Want to learn how to use them? aaron at thegoogle dot rocks expanded",
289+
])
290+
)

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

0 commit comments

Comments
 (0)