Skip to content

Commit b0078c2

Browse files
Merge 0863cac into ac31a79
2 parents ac31a79 + 0863cac commit b0078c2

9 files changed

Lines changed: 961 additions & 21 deletions

File tree

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
@@ -105,6 +105,10 @@ class BlockQuote(Ia2Web):
105105
role = controlTypes.ROLE_BLOCKQUOTE
106106

107107

108+
class Treegrid(Ia2Web):
109+
role = controlTypes.ROLE_TABLE
110+
111+
108112
class Article(Ia2Web):
109113
role = controlTypes.ROLE_ARTICLE
110114

@@ -215,6 +219,8 @@ def findExtraOverlayClasses(obj, clsList, baseClass=Ia2Web, documentClass=None):
215219
xmlRoles = obj.IA2Attributes.get("xml-roles", "").split(" ")
216220
if iaRole == IAccessibleHandler.IA2_ROLE_SECTION and obj.IA2Attributes.get("tag", None) == "blockquote":
217221
clsList.append(BlockQuote)
222+
elif iaRole == oleacc.ROLE_SYSTEM_OUTLINE and "treegrid" in xmlRoles:
223+
clsList.append(Treegrid)
218224
elif iaRole == oleacc.ROLE_SYSTEM_DOCUMENT and xmlRoles[0] == "article":
219225
clsList.append(Article)
220226
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/robot/chromeTests.py

Lines changed: 55 additions & 0 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 (
@@ -21,6 +22,9 @@
2122
_asserts: _AssertsLib = _getLib("AssertsLib")
2223

2324

25+
testDir = os.path.dirname(__file__)
26+
27+
2428
def checkbox_labelled_by_inner_element():
2529
_chrome.prepareChrome(
2630
r"""
@@ -222,3 +226,54 @@ def test_pr11606():
222226
actualSpeech,
223227
"bullet link A link B"
224228
)
229+
230+
def test_ariaTreeGrid_browseMode():
231+
"""
232+
Ensure that ARIA treegrids are accessible as a standard table in browse mode.
233+
"""
234+
testFile = os.path.join(testDir, "testData", "ariaTreegrid", "treegrid-1.html")
235+
_chrome.prepareChrome(
236+
f"""
237+
<iframe src="{testFile}" />
238+
"""
239+
)
240+
# Jump to the heading in the aria treegrid iframe
241+
actualSpeech = _chrome.getSpeechAfterKey("h")
242+
_asserts.strings_match(
243+
actualSpeech,
244+
"frame ARIA treegrid Example heading level 1"
245+
)
246+
# Jump to the ARIA treegrid with the next table quicknav command.
247+
# The browse mode caret will be inside the table on the caption before the first row.
248+
actualSpeech = _chrome.getSpeechAfterKey("t")
249+
_asserts.strings_match(
250+
actualSpeech,
251+
"Inbox table clickable with 5 rows and 3 columns Inbox"
252+
)
253+
# Move past the caption onto row 1 with downArrow
254+
actualSpeech = _chrome.getSpeechAfterKey("downArrow")
255+
_asserts.strings_match(
256+
actualSpeech,
257+
"row 1 Subject column 1 Subject"
258+
)
259+
# Navigate to row 2 column 1 with NVDA table navigation command
260+
actualSpeech = _chrome.getSpeechAfterKey("control+alt+downArrow")
261+
_asserts.strings_match(
262+
actualSpeech,
263+
"expanded level 1 row 2 Treegrids are awesome"
264+
)
265+
# Press enter to activate NVDA focus mode and focus the current row
266+
actualSpeech = _chrome.getSpeechAfterKey("enter")
267+
_asserts.strings_match(
268+
actualSpeech,
269+
"\n".join([
270+
# focus mode turns on
271+
"Focus mode ",
272+
# focus enters the document inside the iframe
273+
"document ",
274+
# Focus enters the ARIA treegrid (table)
275+
"Inbox table ",
276+
# Focus lands on row 2
277+
"level 1 Treegrids are awesome Want to learn how to use them? aaron at thegoogle dot rocks expanded",
278+
])
279+
)

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
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#treegrid {
2+
width: 100%;
3+
white-space: nowrap;
4+
border-collapse: collapse;
5+
table-layout: fixed;
6+
}
7+
8+
#treegrid tr {
9+
display: table-row;
10+
cursor: default;
11+
}
12+
13+
/* Extra space between columns for readability */
14+
#treegrid th,
15+
#treegrid td {
16+
padding-bottom: 3px;
17+
overflow-x: hidden;
18+
text-overflow: ellipsis;
19+
}
20+
21+
#treegrid tbody td {
22+
cursor: default;
23+
}
24+
25+
#treegrid-col1,
26+
#treegrid-col3 {
27+
width: 30%;
28+
}
29+
30+
#treegrid th {
31+
text-align: left;
32+
background-color: #eee;
33+
}
34+
35+
#treegrid a {
36+
padding-left: 0.25ch;
37+
padding-right: 0.25ch;
38+
}
39+
40+
#treegrid tr:focus,
41+
#treegrid td:focus,
42+
#treegrid a:focus {
43+
outline: 2px solid hsl(216, 94%, 70%);
44+
background-color: hsl(216, 80%, 97%);
45+
}
46+
47+
#treegrid tr > td:not(:first-child),
48+
#treegrid tr > th:not(:first-child) {
49+
padding-left: 3ch;
50+
}
51+
52+
#treegrid a:focus {
53+
border-bottom: none;
54+
}
55+
56+
/* Hide collapsed rows */
57+
#treegrid tr.hidden {
58+
display: none;
59+
}
60+
61+
/* Indents */
62+
#treegrid tr[aria-level="2"] > td:first-child {
63+
padding-left: 2.5ch;
64+
}
65+
66+
#treegrid tr[aria-level="3"] > td:first-child {
67+
padding-left: 5ch;
68+
}
69+
70+
#treegrid tr[aria-level="4"] > td:first-child {
71+
padding-left: 7.5ch;
72+
}
73+
74+
#treegrid tr[aria-level="5"] > td:first-child {
75+
padding-left: 10ch;
76+
}
77+
78+
/* Collapse/expand icons */
79+
#treegrid tr > td:first-child::before {
80+
font-family: monospace;
81+
content: " ";
82+
display: inline-block;
83+
width: 2ch;
84+
height: 11px;
85+
transition: transform 0.3s;
86+
transform-origin: 5px 5px;
87+
}
88+
89+
#treegrid tr[aria-expanded] > td:first-child::before,
90+
#treegrid td[aria-expanded]:first-child::before {
91+
cursor: pointer;
92+
93+
/* Load both right away so there is no lag when we need the other */
94+
background-image: url("expand-icon.svg"), url("expand-icon-highlighted.svg");
95+
background-repeat: no-repeat;
96+
}
97+
98+
#treegrid tr[aria-expanded="true"] > td:first-child::before,
99+
#treegrid td[aria-expanded="true"]:first-child::before {
100+
transform: rotate(90deg);
101+
}
102+
103+
#treegrid tr[aria-expanded]:focus > td:first-child::before,
104+
#treegrid tr[aria-expanded] > td:focus:first-child::before,
105+
#treegrid tr:focus > td[aria-expanded]:first-child::before,
106+
#treegrid tr > td[aria-expanded]:focus:first-child::before {
107+
background-image: url("expand-icon-highlighted.svg");
108+
}

0 commit comments

Comments
 (0)