aria-api

access ARIA information from JavaScript
git clone https://git.ce9e.org/aria-api.git

commit
93ad11dbcad85a30f7c6eec93db08e33d6060397
parent
df71d4a90daaf0c9e9594788275fadf8440260cd
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2024-06-22 20:58
bump version to 0.7.0

Diffstat

M CHANGES.md 19 +++++++++++++++++++
M dist/aria.js 164 +++++++++++++++++++++++++++++++++++++++++++------------------
M package.json 2 +-

3 files changed, 137 insertions, 48 deletions


diff --git a/CHANGES.md b/CHANGES.md

@@ -1,3 +1,21 @@
   -1     1 0.7.0 (2024-06-22)
   -1     2 ------------------
   -1     3 
   -1     4 -	add support for SVG-AAM
   -1     5 -	add support for WAI-ARIA 1.3
   -1     6 -	fix: when deciding if an element is hidden, treat `visibility: collapsed` the
   -1     7 	same as `visibility: hidden`
   -1     8 -	getName
   -1     9 	-	do not trim non-breaking spaces
   -1    10 	-	add support for `open-quote`, `close-quote`, `attr(…)` and fallback values
   -1    11 		in pseudo content
   -1    12 	-	treat `placeholder` as tooltip
   -1    13 	-	only add whitespace for online elements, not for inline-block
   -1    14 	-	fallback to tooltip for any element
   -1    15 	-	only ignore `aria-labelledby` when recursion was caused by
   -1    16 		`aria-labelledby` or `aria-describedby`
   -1    17 
   -1    18 
    1    19 0.6.0 (2024-02-04)
    2    20 ------------------
    3    21 
@@ -11,6 +29,7 @@
   11    29 -	getName
   12    30 	-	increase priority of embedded input element
   13    31 
   -1    32 
   14    33 0.5.0 (2023-06-07)
   15    34 ------------------
   16    35 

diff --git a/dist/aria.js b/dist/aria.js

@@ -188,7 +188,7 @@ const getAttribute = function(el, key) {
  188   188 			return true;
  189   189 		}
  190   190 		const style = window.getComputedStyle(el);
  191    -1 		if (style.display === 'none' || style.visibility === 'hidden') {
   -1   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',
   -1   260 	'braillelabel': 'string',
   -1   261 	'brailleroledescription': 'string',
  260   262 	'busy': 'bool',
  261   263 	'checked': 'tristate',
  262   264 	'colcount': 'int',
  263   265 	'colindex': 'int',
   -1   266 	'colindextext': 'string',
  264   267 	'colspan': 'int',
  265   268 	'controls': 'id-list',
  266   269 	'current': 'token',
  267   270 	'describedby': 'id-list',
   -1   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',
   -1   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 
   -1   331 const svgSelectors = function(selector) {
   -1   332 	return [
   -1   333 		// `${selector}:has(> title:not(:empty))`,
   -1   334 		// `${selector}:has(> desc:not(:empty))`,
   -1   335 		`${selector}[aria-label]`,
   -1   336 		`${selector}[aria-roledescription]`,
   -1   337 		`${selector}[aria-labelledby]`,
   -1   338 		`${selector}[aria-describedby]`,
   -1   339 		`${selector}[tabindex]`,
   -1   340 		`${selector}[role]`,
   -1   341 	];
   -1   342 };
   -1   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'],
   -1   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 	},
   -1   421 	comment: {
   -1   422 		nameFromContents: true,
   -1   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    -1 			'a:not([href])',
  498    -1 			'area:not([href])',
   -1   519 			'a:not([*|href])',
   -1   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    -1 	'graphics-object': {},
  523    -1 	'graphics-symbol': {},
   -1   544 	'graphics-object': {
   -1   545 		selectors: [
   -1   546 			...svgSelectors('symbol'),
   -1   547 			...svgSelectors('use'),
   -1   548 		],
   -1   549 	},
   -1   550 	'graphics-symbol': {
   -1   551 		selectors: [
   -1   552 			...svgSelectors('circle'),
   -1   553 			...svgSelectors('ellipse'),
   -1   554 			...svgSelectors('line'),
   -1   555 			...svgSelectors('path'),
   -1   556 			...svgSelectors('polygon'),
   -1   557 			...svgSelectors('polyline'),
   -1   558 			...svgSelectors('rect'),
   -1   559 		],
   -1   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',
   -1   575 			...svgSelectors('foreignObject'),
   -1   576 			...svgSelectors('g'),
  538   577 			'text',
   -1   578 			...svgSelectors('textPath'),
   -1   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    -1 	img: {
  550    -1 		selectors: ['img:not([alt=""])', 'graphics-symbol'],
   -1   590 	image: {
   -1   591 		selectors: [
   -1   592 			'img:not([alt=""])',
   -1   593 			'graphics-symbol',
   -1   594 			...svgSelectors('image'),
   -1   595 			...svgSelectors('mesh'),
   -1   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    -1 		selectors: ['a[href]', 'area[href]'],
   -1   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 	},
   -1   674 	mark: {
   -1   675 		selectors: ['mark'],
   -1   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    -1 		childRoles: ['menuitemcheckbox'],
   -1   700 		childRoles: ['menuitemcheckbox', 'menuitemradio'],
  652   701 		nameFromContents: true,
  653   702 	},
  654   703 	menuitemcheckbox: {
  655    -1 		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    -1 			'img',
   -1   810 			'image',
  763   811 			'insertion',
  764   812 			'landmark',
  765   813 			'list',
  766   814 			'listitem',
  767   815 			'log',
   -1   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',
   -1   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',
   -1  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    -1 const getPseudoContent = function(node, selector) {
  989    -1 	const styles = window.getComputedStyle(node, selector);
  990    -1 	const ret = styles.getPropertyValue('content');
  991    -1 	const inline = styles.display.substr(0, 6) === 'inline';
  992    -1 	if (!ret) {
  993    -1 		return '';
  994    -1 	}
  995    -1 	if (ret.substr(0, 1) !== '"') {
  996    -1 		return '';
  997    -1 	} else {
  998    -1 		if (inline) {
  999    -1 			return ret.slice(1, -1);
   -1  1039 const addSpaces = function(text, el, pseudoSelector) {
   -1  1040 	// https://github.com/w3c/accname/issues/3
   -1  1041 	const styles = window.getComputedStyle(el, pseudoSelector);
   -1  1042 	const inline = styles.display === 'inline';
   -1  1043 	return inline ? text : ` ${text} `;
   -1  1044 };
   -1  1045 
   -1  1046 const getPseudoContent = function(el, pseudoSelector) {
   -1  1047 	const styles = window.getComputedStyle(el, pseudoSelector);
   -1  1048 	let tail = styles.getPropertyValue('content').trim();
   -1  1049 	let ret = [];
   -1  1050 
   -1  1051 	let match;
   -1  1052 	while (tail.length) {
   -1  1053 		if (match = tail.match(/^"([^"]*)"/)) {
   -1  1054 			ret.push(match[1]);
   -1  1055 		} else if (match = tail.match(/^([a-z-]+)\(([^)]*)\)/)) {
   -1  1056 			if (match[1] === 'attr') {
   -1  1057 				ret.push(el.getAttribute(match[2]) || '');
   -1  1058 			}
   -1  1059 		} else if (match = tail.match(/^([a-z-]+)/)) {
   -1  1060 			if (match[1] === 'open-quote' || match[1] === 'close-quote') {
   -1  1061 				ret.push('"');
   -1  1062 			}
   -1  1063 		} else if (match = tail.match(/^\//)) {
   -1  1064 			ret = [];
 1000  1065 		} else {
 1001    -1 			return ' ' + ret.slice(1, -1) + ' ';
   -1  1066 			// invalid content, ignore
   -1  1067 			return '';
 1002  1068 		}
   -1  1069 		tail = tail.slice(match[0].length).trim();
 1003  1070 	}
   -1  1071 
   -1  1072 	return addSpaces(ret.join(''), el, pseudoSelector);
 1004  1073 };
 1005  1074 
 1006    -1 const getContent = function(root, visited) {
   -1  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    -1 			} else if (window.getComputedStyle(node).display.substr(0, 6) === 'inline' &&
 1018    -1 					node.tagName.toLowerCase() !== 'input' &&
 1019    -1 					node.tagName.toLowerCase() !== 'img') {  // https://github.com/w3c/accname/issues/3
 1020    -1 				ret += getName(node, true, visited);
 1021  1086 			} else {
 1022    -1 				ret += ' ' + getName(node, true, visited) + ' ';
   -1  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    -1 const getName = function(el, recursive, visited, directReference) {
   -1  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    -1 	if (!recursive && el.matches('[aria-labelledby]')) {
   -1  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    -1 			return label ? getName(label, true, visited, true) : '';
   -1  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    -1 		if (query.matches(el, 'textbox,button')) {
   -1  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    -1 				ret = getName(selected, recursive, visited);
   -1  1134 				ret = getName(selected, recursive, ongoingLabelledBy, visited);
 1070  1135 			} else {
 1071  1136 				ret = el.value || '';
 1072  1137 			}
@@ -1084,14 +1149,11 @@ 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    -1 			return getName(label, true, visited);
   -1  1152 			return getName(label, true, ongoingLabelledBy, visited);
 1088  1153 		});
 1089  1154 		ret = strings.join(' ');
 1090  1155 	}
 1091  1156 	if (!ret.trim()) {
 1092    -1 		ret = el.placeholder || '';
 1093    -1 	}
 1094    -1 	if (!ret.trim()) {
 1095  1157 		ret = el.alt || '';
 1096  1158 	}
 1097  1159 	if (!ret.trim() && el.matches('abbr,acronym') && el.title) {
@@ -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    -1 					ret = getName(descendant, true, visited);
   -1  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 	}
   -1  1178 	if (!ret.trim() && el.matches('a')) {
   -1  1179 		ret = el.getAttribute('xlink:title') || '';
   -1  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    -1 		ret = getContent(el, visited);
   -1  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    -1 	// FIXME: presentation not mentioned in the spec
 1140    -1 	if (!ret.trim() && (directReference || !query.matches(el, 'presentation'))) {
 1141    -1 		ret = el.title || '';
   -1  1204 	if (!ret.trim()) {
   -1  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    -1 	return before + ret + after;
   -1  1216 	return addSpaces(before + ret + after, el);
 1153  1217 };
 1154  1218 
 1155  1219 const getNameTrimmed = function(el) {
 1156    -1 	return getName(el).replace(/\s+/g, ' ').trim();
   -1  1220 	return getName(el)
   -1  1221 		.replace(/[ \n\r\t\f]+/g, ' ')
   -1  1222 		.replace(/^ /, '')
   -1  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    -1 			return label ? getName(label, true) : '';
   -1  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 	}
   -1  1246 	if (!ret.trim() && el.matches('a')) {
   -1  1247 		ret = el.getAttribute('xlink:title') || '';
   -1  1248 	}
 1179  1249 
 1180  1250 	ret = (ret || '').trim().replace(/\s+/g, ' ');
 1181  1251 

diff --git a/package.json b/package.json

@@ -1,6 +1,6 @@
    1     1 {
    2     2   "name": "aria-api",
    3    -1   "version": "0.6.0",
   -1     3   "version": "0.7.0",
    4     4   "description": "Access ARIA information from JavaScript",
    5     5   "main": "index.js",
    6     6   "keywords": [