INTERACTIVE_ROLES = {
"button", "checkbox", "combobox", "link", "listbox", "menuitem",
"menuitemcheckbox", "menuitemradio", "option", "radio", "searchbox",
"slider", "spinbutton", "switch", "tab", "textbox", "treeitem"
}
CONTENT_ROLES = {
"heading", "main", "navigation", "region", "search"
}
LANDMARKS = {"main", "navigation", "region", "search", "form", "heading"}
JS_A11Y_EXTRACTOR = """
() => {
const INTERACTIVE_ROLES = new Set([
"button", "link", "checkbox", "menuitem", "option", "radio", "switch", "tab",
"treeitem", "textbox", "searchbox", "spinbutton", "combobox", "listbox", "slider"
]);
const results = [];
const isVisible = (el) => {
if (!el.getClientRects().length) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
if (style.pointerEvents === 'none') return false;
return true;
};
const getAriaLabelledBy = (el) => {
const ids = el.getAttribute('aria-labelledby');
if (!ids) return '';
return ids
.split(' ')
.map(id => {
const target = document.getElementById(id);
return target ? target.innerText : '';
})
.filter(Boolean)
.join(' ');
};
const isInteractiveNode = (node, style, role) => {
const tagName = node.tagName;
const isInput = ['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName);
const isButtonOrLink = tagName === 'BUTTON' || tagName === 'A' || style.cursor === 'pointer';
const hasOnClick = typeof node.onclick === 'function' || node.getAttribute('onclick');
const hasTabIndex = node.tabIndex >= 0;
const hasRole = role && role !== '';
return (
INTERACTIVE_ROLES.has(role) ||
isInput ||
isButtonOrLink ||
hasOnClick ||
hasTabIndex ||
hasRole
);
};
const walk = (root) => {
const children = root.children || [];
for (const node of children) {
if (node.nodeType !== 1) continue;
if (!isVisible(node)) continue;
const rect = node.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) continue;
const style = window.getComputedStyle(node);
const tagName = node.tagName;
const role = (node.getAttribute('role') || '').toLowerCase();
const nameCandidates = [
node.getAttribute('aria-label'),
getAriaLabelledBy(node),
node.placeholder,
node.value,
node.innerText
];
const name = (nameCandidates.find(x => x && x.toString().trim()) || tagName).toString().substring(0, 100).trim();
const interactive = isInteractiveNode(node, style, role);
if (interactive) {
results.push({
tagName: tagName,
role: role || tagName.toLowerCase(),
name: name,
placeholder: node.placeholder || '',
rect: {
x: Math.round(rect.x), y: Math.round(rect.y),
width: Math.round(rect.width), height: Math.round(rect.height)
}
});
}
// Traverse shadow root if present
if (node.shadowRoot) {
walk(node.shadowRoot);
}
// Continue traversal
walk(node);
}
};
walk(document.body);
return results;
}
"""