aria-api 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/aria.js CHANGED
@@ -188,7 +188,7 @@ const getAttribute = function(el, key) {
188
188
  return true;
189
189
  }
190
190
  const style = window.getComputedStyle(el);
191
- if (style.display === 'none' || style.visibility === 'hidden') {
191
+ if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
192
192
  return true;
193
193
  }
194
194
  }
@@ -257,14 +257,18 @@ exports.attributes = {
257
257
  'activedescendant': 'id',
258
258
  'atomic': 'bool',
259
259
  'autocomplete': 'token',
260
+ 'braillelabel': 'string',
261
+ 'brailleroledescription': 'string',
260
262
  'busy': 'bool',
261
263
  'checked': 'tristate',
262
264
  'colcount': 'int',
263
265
  'colindex': 'int',
266
+ 'colindextext': 'string',
264
267
  'colspan': 'int',
265
268
  'controls': 'id-list',
266
269
  'current': 'token',
267
270
  'describedby': 'id-list',
271
+ 'description': 'string',
268
272
  'details': 'id',
269
273
  'disabled': 'bool',
270
274
  'dropeffect': 'token-list',
@@ -294,6 +298,7 @@ exports.attributes = {
294
298
  'roledescription': 'string',
295
299
  'rowcount': 'int',
296
300
  'rowindex': 'int',
301
+ 'rowindextext': 'string',
297
302
  'rowspan': 'int',
298
303
  'selected': 'bool-undefined',
299
304
  'setsize': 'int',
@@ -323,6 +328,19 @@ exports.attributeWeakMapping = {
323
328
  // https://www.w3.org/TR/html/dom.html#sectioning-content-2
324
329
  const scoped = ['article *', 'aside *', 'nav *', 'section *'].join(',');
325
330
 
331
+ const svgSelectors = function(selector) {
332
+ return [
333
+ // `${selector}:has(> title:not(:empty))`,
334
+ // `${selector}:has(> desc:not(:empty))`,
335
+ `${selector}[aria-label]`,
336
+ `${selector}[aria-roledescription]`,
337
+ `${selector}[aria-labelledby]`,
338
+ `${selector}[aria-describedby]`,
339
+ `${selector}[tabindex]`,
340
+ `${selector}[role]`,
341
+ ];
342
+ };
343
+
326
344
  // https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings
327
345
  // https://www.w3.org/TR/wai-aria/roles
328
346
  exports.roles = {
@@ -337,6 +355,7 @@ exports.roles = {
337
355
  application: {},
338
356
  article: {
339
357
  selectors: ['article'],
358
+ childRoles: ['comment'],
340
359
  },
341
360
  banner: {
342
361
  selectors: [`header:not(main *, ${scoped})`],
@@ -399,6 +418,9 @@ exports.roles = {
399
418
  abstract: true,
400
419
  childRoles: ['button', 'link', 'menuitem'],
401
420
  },
421
+ comment: {
422
+ nameFromContents: true,
423
+ },
402
424
  complementary: {
403
425
  selectors: [
404
426
  `aside:not(${scoped})`,
@@ -494,8 +516,8 @@ exports.roles = {
494
516
  },
495
517
  generic: {
496
518
  selectors: [
497
- 'a:not([href])',
498
- 'area:not([href])',
519
+ 'a:not([*|href])',
520
+ 'area:not([*|href])',
499
521
  `aside:not(${scoped}):not([aria-label]):not([aria-labelledby]):not([title])`,
500
522
  'b',
501
523
  'bdi',
@@ -519,8 +541,23 @@ exports.roles = {
519
541
  'graphics-document': {
520
542
  selectors: ['svg'],
521
543
  },
522
- 'graphics-object': {},
523
- 'graphics-symbol': {},
544
+ 'graphics-object': {
545
+ selectors: [
546
+ ...svgSelectors('symbol'),
547
+ ...svgSelectors('use'),
548
+ ],
549
+ },
550
+ 'graphics-symbol': {
551
+ selectors: [
552
+ ...svgSelectors('circle'),
553
+ ...svgSelectors('ellipse'),
554
+ ...svgSelectors('line'),
555
+ ...svgSelectors('path'),
556
+ ...svgSelectors('polygon'),
557
+ ...svgSelectors('polyline'),
558
+ ...svgSelectors('rect'),
559
+ ],
560
+ },
524
561
  grid: {
525
562
  childRoles: ['treegrid'],
526
563
  },
@@ -535,7 +572,11 @@ exports.roles = {
535
572
  'fieldset',
536
573
  'hgroup',
537
574
  'optgroup',
575
+ ...svgSelectors('foreignObject'),
576
+ ...svgSelectors('g'),
538
577
  'text',
578
+ ...svgSelectors('textPath'),
579
+ ...svgSelectors('tspan'),
539
580
  ],
540
581
  childRoles: ['row', 'select', 'toolbar', 'graphics-object'],
541
582
  },
@@ -546,8 +587,13 @@ exports.roles = {
546
587
  'level': 2,
547
588
  },
548
589
  },
549
- img: {
550
- selectors: ['img:not([alt=""])', 'graphics-symbol'],
590
+ image: {
591
+ selectors: [
592
+ 'img:not([alt=""])',
593
+ 'graphics-symbol',
594
+ ...svgSelectors('image'),
595
+ ...svgSelectors('mesh'),
596
+ ],
551
597
  childRoles: ['doc-cover'],
552
598
  },
553
599
  input: {
@@ -595,7 +641,7 @@ exports.roles = {
595
641
  ],
596
642
  },
597
643
  link: {
598
- selectors: ['a[href]', 'area[href]'],
644
+ selectors: ['a[*|href]', 'area[href]'],
599
645
  childRoles: ['doc-backlink', 'doc-biblioref', 'doc-glossref', 'doc-noteref'],
600
646
  nameFromContents: true,
601
647
  },
@@ -625,6 +671,9 @@ exports.roles = {
625
671
  main: {
626
672
  selectors: ['main'],
627
673
  },
674
+ mark: {
675
+ selectors: ['mark'],
676
+ },
628
677
  marquee: {},
629
678
  math: {
630
679
  selectors: ['math'],
@@ -648,11 +697,10 @@ exports.roles = {
648
697
  },
649
698
  },
650
699
  menuitem: {
651
- childRoles: ['menuitemcheckbox'],
700
+ childRoles: ['menuitemcheckbox', 'menuitemradio'],
652
701
  nameFromContents: true,
653
702
  },
654
703
  menuitemcheckbox: {
655
- childRoles: ['menuitemradio'],
656
704
  nameFromContents: true,
657
705
  defaults: {
658
706
  'checked': 'false',
@@ -759,12 +807,13 @@ exports.roles = {
759
807
  'emphasis',
760
808
  'figure',
761
809
  'group',
762
- 'img',
810
+ 'image',
763
811
  'insertion',
764
812
  'landmark',
765
813
  'list',
766
814
  'listitem',
767
815
  'log',
816
+ 'mark',
768
817
  'marquee',
769
818
  'math',
770
819
  'note',
@@ -772,6 +821,7 @@ exports.roles = {
772
821
  'status',
773
822
  'strong',
774
823
  'subscript',
824
+ 'suggestion',
775
825
  'superscript',
776
826
  'table',
777
827
  'tabpanel',
@@ -966,6 +1016,7 @@ for (const role in exports.roles) {
966
1016
  exports.aliases = {
967
1017
  'presentation': 'none',
968
1018
  'directory': 'list',
1019
+ 'img': 'image',
969
1020
  };
970
1021
 
971
1022
  exports.nameFromDescendant = {
@@ -985,25 +1036,43 @@ const constants = require('./constants.js');
985
1036
  const atree = require('./atree.js');
986
1037
  const query = require('./query.js');
987
1038
 
988
- const getPseudoContent = function(node, selector) {
989
- const styles = window.getComputedStyle(node, selector);
990
- const ret = styles.getPropertyValue('content');
991
- const inline = styles.display.substr(0, 6) === 'inline';
992
- if (!ret) {
993
- return '';
994
- }
995
- if (ret.substr(0, 1) !== '"') {
996
- return '';
997
- } else {
998
- if (inline) {
999
- return ret.slice(1, -1);
1039
+ const addSpaces = function(text, el, pseudoSelector) {
1040
+ // https://github.com/w3c/accname/issues/3
1041
+ const styles = window.getComputedStyle(el, pseudoSelector);
1042
+ const inline = styles.display === 'inline';
1043
+ return inline ? text : ` ${text} `;
1044
+ };
1045
+
1046
+ const getPseudoContent = function(el, pseudoSelector) {
1047
+ const styles = window.getComputedStyle(el, pseudoSelector);
1048
+ let tail = styles.getPropertyValue('content').trim();
1049
+ let ret = [];
1050
+
1051
+ let match;
1052
+ while (tail.length) {
1053
+ if (match = tail.match(/^"([^"]*)"/)) {
1054
+ ret.push(match[1]);
1055
+ } else if (match = tail.match(/^([a-z-]+)\(([^)]*)\)/)) {
1056
+ if (match[1] === 'attr') {
1057
+ ret.push(el.getAttribute(match[2]) || '');
1058
+ }
1059
+ } else if (match = tail.match(/^([a-z-]+)/)) {
1060
+ if (match[1] === 'open-quote' || match[1] === 'close-quote') {
1061
+ ret.push('"');
1062
+ }
1063
+ } else if (match = tail.match(/^\//)) {
1064
+ ret = [];
1000
1065
  } else {
1001
- return ' ' + ret.slice(1, -1) + ' ';
1066
+ // invalid content, ignore
1067
+ return '';
1002
1068
  }
1069
+ tail = tail.slice(match[0].length).trim();
1003
1070
  }
1071
+
1072
+ return addSpaces(ret.join(''), el, pseudoSelector);
1004
1073
  };
1005
1074
 
1006
- const getContent = function(root, visited) {
1075
+ const getContent = function(root, ongoingLabelledBy, visited) {
1007
1076
  const children = atree.getChildNodes(root);
1008
1077
 
1009
1078
  let ret = '';
@@ -1014,12 +1083,8 @@ const getContent = function(root, visited) {
1014
1083
  } else if (node.nodeType === node.ELEMENT_NODE) {
1015
1084
  if (node.tagName.toLowerCase() === 'br') {
1016
1085
  ret += '\n';
1017
- } else if (window.getComputedStyle(node).display.substr(0, 6) === 'inline' &&
1018
- node.tagName.toLowerCase() !== 'input' &&
1019
- node.tagName.toLowerCase() !== 'img') { // https://github.com/w3c/accname/issues/3
1020
- ret += getName(node, true, visited);
1021
1086
  } else {
1022
- ret += ' ' + getName(node, true, visited) + ' ';
1087
+ ret += getName(node, true, ongoingLabelledBy, visited);
1023
1088
  }
1024
1089
  }
1025
1090
  }
@@ -1034,7 +1099,7 @@ const allowNameFromContent = function(el) {
1034
1099
  }
1035
1100
  };
1036
1101
 
1037
- const getName = function(el, recursive, visited, directReference) {
1102
+ const getName = function(el, recursive, ongoingLabelledBy, visited, directReference) {
1038
1103
  let ret = '';
1039
1104
 
1040
1105
  visited = visited || [];
@@ -1050,23 +1115,23 @@ const getName = function(el, recursive, visited, directReference) {
1050
1115
  // handled in atree
1051
1116
 
1052
1117
  // B
1053
- if (!recursive && el.matches('[aria-labelledby]')) {
1118
+ if (!ongoingLabelledBy && el.matches('[aria-labelledby]')) {
1054
1119
  const ids = el.getAttribute('aria-labelledby').split(/\s+/);
1055
1120
  const strings = ids.map(id => {
1056
1121
  const label = document.getElementById(id);
1057
- return label ? getName(label, true, visited, true) : '';
1122
+ return label ? getName(label, true, true, visited, true) : '';
1058
1123
  });
1059
1124
  ret = strings.join(' ');
1060
1125
  }
1061
1126
 
1062
1127
  // E (the current draft has this at this high priority)
1063
1128
  if (!ret.trim() && recursive) {
1064
- if (query.matches(el, 'textbox,button')) {
1129
+ if (query.matches(el, 'textbox')) {
1065
1130
  ret = el.value || el.textContent;
1066
1131
  } else if (query.matches(el, 'combobox,listbox')) {
1067
1132
  const selected = query.querySelector(el, ':selected') || query.querySelector(el, 'option');
1068
1133
  if (selected) {
1069
- ret = getName(selected, recursive, visited);
1134
+ ret = getName(selected, recursive, ongoingLabelledBy, visited);
1070
1135
  } else {
1071
1136
  ret = el.value || '';
1072
1137
  }
@@ -1084,13 +1149,10 @@ const getName = function(el, recursive, visited, directReference) {
1084
1149
  // D
1085
1150
  if (!ret.trim() && !recursive && el.labels) {
1086
1151
  const strings = Array.prototype.map.call(el.labels, label => {
1087
- return getName(label, true, visited);
1152
+ return getName(label, true, ongoingLabelledBy, visited);
1088
1153
  });
1089
1154
  ret = strings.join(' ');
1090
1155
  }
1091
- if (!ret.trim()) {
1092
- ret = el.placeholder || '';
1093
- }
1094
1156
  if (!ret.trim()) {
1095
1157
  ret = el.alt || '';
1096
1158
  }
@@ -1102,7 +1164,7 @@ const getName = function(el, recursive, visited, directReference) {
1102
1164
  if (el.matches(selector)) {
1103
1165
  const descendant = el.querySelector(constants.nameFromDescendant[selector]);
1104
1166
  if (descendant) {
1105
- ret = getName(descendant, true, visited);
1167
+ ret = getName(descendant, true, ongoingLabelledBy, visited);
1106
1168
  }
1107
1169
  }
1108
1170
  }
@@ -1113,11 +1175,14 @@ const getName = function(el, recursive, visited, directReference) {
1113
1175
  ret = svgTitle.textContent;
1114
1176
  }
1115
1177
  }
1178
+ if (!ret.trim() && el.matches('a')) {
1179
+ ret = el.getAttribute('xlink:title') || '';
1180
+ }
1116
1181
 
1117
1182
  // F
1118
1183
  // FIXME: menu is not mentioned in the spec
1119
1184
  if (!ret.trim() && (recursive || allowNameFromContent(el) || el.closest('label')) && !query.matches(el, 'menu')) {
1120
- ret = getContent(el, visited);
1185
+ ret = getContent(el, ongoingLabelledBy, visited);
1121
1186
  }
1122
1187
 
1123
1188
  if (!ret.trim() && query.matches(el, 'button')) {
@@ -1136,9 +1201,8 @@ const getName = function(el, recursive, visited, directReference) {
1136
1201
  // handled in getContent
1137
1202
 
1138
1203
  // I
1139
- // FIXME: presentation not mentioned in the spec
1140
- if (!ret.trim() && (directReference || !query.matches(el, 'presentation'))) {
1141
- ret = el.title || '';
1204
+ if (!ret.trim()) {
1205
+ ret = el.title || el.placeholder || '';
1142
1206
  }
1143
1207
 
1144
1208
  // FIXME: not exactly sure about this, but it reduces the number of failing
@@ -1149,11 +1213,14 @@ const getName = function(el, recursive, visited, directReference) {
1149
1213
 
1150
1214
  const before = getPseudoContent(el, ':before');
1151
1215
  const after = getPseudoContent(el, ':after');
1152
- return before + ret + after;
1216
+ return addSpaces(before + ret + after, el);
1153
1217
  };
1154
1218
 
1155
1219
  const getNameTrimmed = function(el) {
1156
- return getName(el).replace(/\s+/g, ' ').trim();
1220
+ return getName(el)
1221
+ .replace(/[ \n\r\t\f]+/g, ' ')
1222
+ .replace(/^ /, '')
1223
+ .replace(/ $/, '');
1157
1224
  };
1158
1225
 
1159
1226
  const getDescription = function(el) {
@@ -1163,7 +1230,7 @@ const getDescription = function(el) {
1163
1230
  const ids = el.getAttribute('aria-describedby').split(/\s+/);
1164
1231
  const strings = ids.map(id => {
1165
1232
  const label = document.getElementById(id);
1166
- return label ? getName(label, true) : '';
1233
+ return label ? getName(label, true, true) : '';
1167
1234
  });
1168
1235
  ret = strings.join(' ');
1169
1236
  } else if (el.matches('[aria-description]')) {
@@ -1176,6 +1243,9 @@ const getDescription = function(el) {
1176
1243
  } else if (el.title) {
1177
1244
  ret = el.title;
1178
1245
  }
1246
+ if (!ret.trim() && el.matches('a')) {
1247
+ ret = el.getAttribute('xlink:title') || '';
1248
+ }
1179
1249
 
1180
1250
  ret = (ret || '').trim().replace(/\s+/g, ' ');
1181
1251
 
package/lib/attrs.js CHANGED
@@ -68,7 +68,7 @@ const getAttribute = function(el, key) {
68
68
  return true;
69
69
  }
70
70
  const style = window.getComputedStyle(el);
71
- if (style.display === 'none' || style.visibility === 'hidden') {
71
+ if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
72
72
  return true;
73
73
  }
74
74
  }
package/lib/constants.js CHANGED
@@ -3,14 +3,18 @@ exports.attributes = {
3
3
  'activedescendant': 'id',
4
4
  'atomic': 'bool',
5
5
  'autocomplete': 'token',
6
+ 'braillelabel': 'string',
7
+ 'brailleroledescription': 'string',
6
8
  'busy': 'bool',
7
9
  'checked': 'tristate',
8
10
  'colcount': 'int',
9
11
  'colindex': 'int',
12
+ 'colindextext': 'string',
10
13
  'colspan': 'int',
11
14
  'controls': 'id-list',
12
15
  'current': 'token',
13
16
  'describedby': 'id-list',
17
+ 'description': 'string',
14
18
  'details': 'id',
15
19
  'disabled': 'bool',
16
20
  'dropeffect': 'token-list',
@@ -40,6 +44,7 @@ exports.attributes = {
40
44
  'roledescription': 'string',
41
45
  'rowcount': 'int',
42
46
  'rowindex': 'int',
47
+ 'rowindextext': 'string',
43
48
  'rowspan': 'int',
44
49
  'selected': 'bool-undefined',
45
50
  'setsize': 'int',
@@ -69,6 +74,19 @@ exports.attributeWeakMapping = {
69
74
  // https://www.w3.org/TR/html/dom.html#sectioning-content-2
70
75
  const scoped = ['article *', 'aside *', 'nav *', 'section *'].join(',');
71
76
 
77
+ const svgSelectors = function(selector) {
78
+ return [
79
+ // `${selector}:has(> title:not(:empty))`,
80
+ // `${selector}:has(> desc:not(:empty))`,
81
+ `${selector}[aria-label]`,
82
+ `${selector}[aria-roledescription]`,
83
+ `${selector}[aria-labelledby]`,
84
+ `${selector}[aria-describedby]`,
85
+ `${selector}[tabindex]`,
86
+ `${selector}[role]`,
87
+ ];
88
+ };
89
+
72
90
  // https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings
73
91
  // https://www.w3.org/TR/wai-aria/roles
74
92
  exports.roles = {
@@ -83,6 +101,7 @@ exports.roles = {
83
101
  application: {},
84
102
  article: {
85
103
  selectors: ['article'],
104
+ childRoles: ['comment'],
86
105
  },
87
106
  banner: {
88
107
  selectors: [`header:not(main *, ${scoped})`],
@@ -145,6 +164,9 @@ exports.roles = {
145
164
  abstract: true,
146
165
  childRoles: ['button', 'link', 'menuitem'],
147
166
  },
167
+ comment: {
168
+ nameFromContents: true,
169
+ },
148
170
  complementary: {
149
171
  selectors: [
150
172
  `aside:not(${scoped})`,
@@ -240,8 +262,8 @@ exports.roles = {
240
262
  },
241
263
  generic: {
242
264
  selectors: [
243
- 'a:not([href])',
244
- 'area:not([href])',
265
+ 'a:not([*|href])',
266
+ 'area:not([*|href])',
245
267
  `aside:not(${scoped}):not([aria-label]):not([aria-labelledby]):not([title])`,
246
268
  'b',
247
269
  'bdi',
@@ -265,8 +287,23 @@ exports.roles = {
265
287
  'graphics-document': {
266
288
  selectors: ['svg'],
267
289
  },
268
- 'graphics-object': {},
269
- 'graphics-symbol': {},
290
+ 'graphics-object': {
291
+ selectors: [
292
+ ...svgSelectors('symbol'),
293
+ ...svgSelectors('use'),
294
+ ],
295
+ },
296
+ 'graphics-symbol': {
297
+ selectors: [
298
+ ...svgSelectors('circle'),
299
+ ...svgSelectors('ellipse'),
300
+ ...svgSelectors('line'),
301
+ ...svgSelectors('path'),
302
+ ...svgSelectors('polygon'),
303
+ ...svgSelectors('polyline'),
304
+ ...svgSelectors('rect'),
305
+ ],
306
+ },
270
307
  grid: {
271
308
  childRoles: ['treegrid'],
272
309
  },
@@ -281,7 +318,11 @@ exports.roles = {
281
318
  'fieldset',
282
319
  'hgroup',
283
320
  'optgroup',
321
+ ...svgSelectors('foreignObject'),
322
+ ...svgSelectors('g'),
284
323
  'text',
324
+ ...svgSelectors('textPath'),
325
+ ...svgSelectors('tspan'),
285
326
  ],
286
327
  childRoles: ['row', 'select', 'toolbar', 'graphics-object'],
287
328
  },
@@ -292,8 +333,13 @@ exports.roles = {
292
333
  'level': 2,
293
334
  },
294
335
  },
295
- img: {
296
- selectors: ['img:not([alt=""])', 'graphics-symbol'],
336
+ image: {
337
+ selectors: [
338
+ 'img:not([alt=""])',
339
+ 'graphics-symbol',
340
+ ...svgSelectors('image'),
341
+ ...svgSelectors('mesh'),
342
+ ],
297
343
  childRoles: ['doc-cover'],
298
344
  },
299
345
  input: {
@@ -341,7 +387,7 @@ exports.roles = {
341
387
  ],
342
388
  },
343
389
  link: {
344
- selectors: ['a[href]', 'area[href]'],
390
+ selectors: ['a[*|href]', 'area[href]'],
345
391
  childRoles: ['doc-backlink', 'doc-biblioref', 'doc-glossref', 'doc-noteref'],
346
392
  nameFromContents: true,
347
393
  },
@@ -371,6 +417,9 @@ exports.roles = {
371
417
  main: {
372
418
  selectors: ['main'],
373
419
  },
420
+ mark: {
421
+ selectors: ['mark'],
422
+ },
374
423
  marquee: {},
375
424
  math: {
376
425
  selectors: ['math'],
@@ -394,11 +443,10 @@ exports.roles = {
394
443
  },
395
444
  },
396
445
  menuitem: {
397
- childRoles: ['menuitemcheckbox'],
446
+ childRoles: ['menuitemcheckbox', 'menuitemradio'],
398
447
  nameFromContents: true,
399
448
  },
400
449
  menuitemcheckbox: {
401
- childRoles: ['menuitemradio'],
402
450
  nameFromContents: true,
403
451
  defaults: {
404
452
  'checked': 'false',
@@ -505,12 +553,13 @@ exports.roles = {
505
553
  'emphasis',
506
554
  'figure',
507
555
  'group',
508
- 'img',
556
+ 'image',
509
557
  'insertion',
510
558
  'landmark',
511
559
  'list',
512
560
  'listitem',
513
561
  'log',
562
+ 'mark',
514
563
  'marquee',
515
564
  'math',
516
565
  'note',
@@ -518,6 +567,7 @@ exports.roles = {
518
567
  'status',
519
568
  'strong',
520
569
  'subscript',
570
+ 'suggestion',
521
571
  'superscript',
522
572
  'table',
523
573
  'tabpanel',
@@ -712,6 +762,7 @@ for (const role in exports.roles) {
712
762
  exports.aliases = {
713
763
  'presentation': 'none',
714
764
  'directory': 'list',
765
+ 'img': 'image',
715
766
  };
716
767
 
717
768
  exports.nameFromDescendant = {
package/lib/name.js CHANGED
@@ -2,25 +2,43 @@ const constants = require('./constants.js');
2
2
  const atree = require('./atree.js');
3
3
  const query = require('./query.js');
4
4
 
5
- const getPseudoContent = function(node, selector) {
6
- const styles = window.getComputedStyle(node, selector);
7
- const ret = styles.getPropertyValue('content');
8
- const inline = styles.display.substr(0, 6) === 'inline';
9
- if (!ret) {
10
- return '';
11
- }
12
- if (ret.substr(0, 1) !== '"') {
13
- return '';
14
- } else {
15
- if (inline) {
16
- return ret.slice(1, -1);
5
+ const addSpaces = function(text, el, pseudoSelector) {
6
+ // https://github.com/w3c/accname/issues/3
7
+ const styles = window.getComputedStyle(el, pseudoSelector);
8
+ const inline = styles.display === 'inline';
9
+ return inline ? text : ` ${text} `;
10
+ };
11
+
12
+ const getPseudoContent = function(el, pseudoSelector) {
13
+ const styles = window.getComputedStyle(el, pseudoSelector);
14
+ let tail = styles.getPropertyValue('content').trim();
15
+ let ret = [];
16
+
17
+ let match;
18
+ while (tail.length) {
19
+ if (match = tail.match(/^"([^"]*)"/)) {
20
+ ret.push(match[1]);
21
+ } else if (match = tail.match(/^([a-z-]+)\(([^)]*)\)/)) {
22
+ if (match[1] === 'attr') {
23
+ ret.push(el.getAttribute(match[2]) || '');
24
+ }
25
+ } else if (match = tail.match(/^([a-z-]+)/)) {
26
+ if (match[1] === 'open-quote' || match[1] === 'close-quote') {
27
+ ret.push('"');
28
+ }
29
+ } else if (match = tail.match(/^\//)) {
30
+ ret = [];
17
31
  } else {
18
- return ' ' + ret.slice(1, -1) + ' ';
32
+ // invalid content, ignore
33
+ return '';
19
34
  }
35
+ tail = tail.slice(match[0].length).trim();
20
36
  }
37
+
38
+ return addSpaces(ret.join(''), el, pseudoSelector);
21
39
  };
22
40
 
23
- const getContent = function(root, visited) {
41
+ const getContent = function(root, ongoingLabelledBy, visited) {
24
42
  const children = atree.getChildNodes(root);
25
43
 
26
44
  let ret = '';
@@ -31,12 +49,8 @@ const getContent = function(root, visited) {
31
49
  } else if (node.nodeType === node.ELEMENT_NODE) {
32
50
  if (node.tagName.toLowerCase() === 'br') {
33
51
  ret += '\n';
34
- } else if (window.getComputedStyle(node).display.substr(0, 6) === 'inline' &&
35
- node.tagName.toLowerCase() !== 'input' &&
36
- node.tagName.toLowerCase() !== 'img') { // https://github.com/w3c/accname/issues/3
37
- ret += getName(node, true, visited);
38
52
  } else {
39
- ret += ' ' + getName(node, true, visited) + ' ';
53
+ ret += getName(node, true, ongoingLabelledBy, visited);
40
54
  }
41
55
  }
42
56
  }
@@ -51,7 +65,7 @@ const allowNameFromContent = function(el) {
51
65
  }
52
66
  };
53
67
 
54
- const getName = function(el, recursive, visited, directReference) {
68
+ const getName = function(el, recursive, ongoingLabelledBy, visited, directReference) {
55
69
  let ret = '';
56
70
 
57
71
  visited = visited || [];
@@ -67,23 +81,23 @@ const getName = function(el, recursive, visited, directReference) {
67
81
  // handled in atree
68
82
 
69
83
  // B
70
- if (!recursive && el.matches('[aria-labelledby]')) {
84
+ if (!ongoingLabelledBy && el.matches('[aria-labelledby]')) {
71
85
  const ids = el.getAttribute('aria-labelledby').split(/\s+/);
72
86
  const strings = ids.map(id => {
73
87
  const label = document.getElementById(id);
74
- return label ? getName(label, true, visited, true) : '';
88
+ return label ? getName(label, true, true, visited, true) : '';
75
89
  });
76
90
  ret = strings.join(' ');
77
91
  }
78
92
 
79
93
  // E (the current draft has this at this high priority)
80
94
  if (!ret.trim() && recursive) {
81
- if (query.matches(el, 'textbox,button')) {
95
+ if (query.matches(el, 'textbox')) {
82
96
  ret = el.value || el.textContent;
83
97
  } else if (query.matches(el, 'combobox,listbox')) {
84
98
  const selected = query.querySelector(el, ':selected') || query.querySelector(el, 'option');
85
99
  if (selected) {
86
- ret = getName(selected, recursive, visited);
100
+ ret = getName(selected, recursive, ongoingLabelledBy, visited);
87
101
  } else {
88
102
  ret = el.value || '';
89
103
  }
@@ -101,13 +115,10 @@ const getName = function(el, recursive, visited, directReference) {
101
115
  // D
102
116
  if (!ret.trim() && !recursive && el.labels) {
103
117
  const strings = Array.prototype.map.call(el.labels, label => {
104
- return getName(label, true, visited);
118
+ return getName(label, true, ongoingLabelledBy, visited);
105
119
  });
106
120
  ret = strings.join(' ');
107
121
  }
108
- if (!ret.trim()) {
109
- ret = el.placeholder || '';
110
- }
111
122
  if (!ret.trim()) {
112
123
  ret = el.alt || '';
113
124
  }
@@ -119,7 +130,7 @@ const getName = function(el, recursive, visited, directReference) {
119
130
  if (el.matches(selector)) {
120
131
  const descendant = el.querySelector(constants.nameFromDescendant[selector]);
121
132
  if (descendant) {
122
- ret = getName(descendant, true, visited);
133
+ ret = getName(descendant, true, ongoingLabelledBy, visited);
123
134
  }
124
135
  }
125
136
  }
@@ -130,11 +141,14 @@ const getName = function(el, recursive, visited, directReference) {
130
141
  ret = svgTitle.textContent;
131
142
  }
132
143
  }
144
+ if (!ret.trim() && el.matches('a')) {
145
+ ret = el.getAttribute('xlink:title') || '';
146
+ }
133
147
 
134
148
  // F
135
149
  // FIXME: menu is not mentioned in the spec
136
150
  if (!ret.trim() && (recursive || allowNameFromContent(el) || el.closest('label')) && !query.matches(el, 'menu')) {
137
- ret = getContent(el, visited);
151
+ ret = getContent(el, ongoingLabelledBy, visited);
138
152
  }
139
153
 
140
154
  if (!ret.trim() && query.matches(el, 'button')) {
@@ -153,9 +167,8 @@ const getName = function(el, recursive, visited, directReference) {
153
167
  // handled in getContent
154
168
 
155
169
  // I
156
- // FIXME: presentation not mentioned in the spec
157
- if (!ret.trim() && (directReference || !query.matches(el, 'presentation'))) {
158
- ret = el.title || '';
170
+ if (!ret.trim()) {
171
+ ret = el.title || el.placeholder || '';
159
172
  }
160
173
 
161
174
  // FIXME: not exactly sure about this, but it reduces the number of failing
@@ -166,11 +179,14 @@ const getName = function(el, recursive, visited, directReference) {
166
179
 
167
180
  const before = getPseudoContent(el, ':before');
168
181
  const after = getPseudoContent(el, ':after');
169
- return before + ret + after;
182
+ return addSpaces(before + ret + after, el);
170
183
  };
171
184
 
172
185
  const getNameTrimmed = function(el) {
173
- return getName(el).replace(/\s+/g, ' ').trim();
186
+ return getName(el)
187
+ .replace(/[ \n\r\t\f]+/g, ' ')
188
+ .replace(/^ /, '')
189
+ .replace(/ $/, '');
174
190
  };
175
191
 
176
192
  const getDescription = function(el) {
@@ -180,7 +196,7 @@ const getDescription = function(el) {
180
196
  const ids = el.getAttribute('aria-describedby').split(/\s+/);
181
197
  const strings = ids.map(id => {
182
198
  const label = document.getElementById(id);
183
- return label ? getName(label, true) : '';
199
+ return label ? getName(label, true, true) : '';
184
200
  });
185
201
  ret = strings.join(' ');
186
202
  } else if (el.matches('[aria-description]')) {
@@ -193,6 +209,9 @@ const getDescription = function(el) {
193
209
  } else if (el.title) {
194
210
  ret = el.title;
195
211
  }
212
+ if (!ret.trim() && el.matches('a')) {
213
+ ret = el.getAttribute('xlink:title') || '';
214
+ }
196
215
 
197
216
  ret = (ret || '').trim().replace(/\s+/g, ' ');
198
217
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aria-api",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Access ARIA information from JavaScript",
5
5
  "main": "index.js",
6
6
  "keywords": [