-
-
Save stevefaulkner/a0f1f13c620a83e9584a4caae3c9808d to your computer and use it in GitHub Desktop.
aria-activedescendant check
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| let VERBOSE = false; // set true for detailed object dumps | |
| const fmt = (el) => { | |
| if (!el) return '—'; | |
| const id = el.id ? `#${el.id}` : ''; | |
| const role = el.getAttribute?.('role') || ''; | |
| const tag = el.tagName?.toLowerCase?.() || ''; | |
| return [tag+id, role?`(role=${role})`:''].filter(Boolean).join(' '); | |
| }; | |
| const isVisible = (el) => !!(el && el.getClientRects().length && | |
| (el.offsetWidth || el.offsetHeight) && | |
| getComputedStyle(el).visibility !== 'hidden' && | |
| getComputedStyle(el).display !== 'none'); | |
| const relatedContainers = (owner) => { | |
| const owns = (owner.getAttribute('aria-owns')||'').split(/\s+/).filter(Boolean); | |
| const ctrls = (owner.getAttribute('aria-controls')||'').split(/\s+/).filter(Boolean); | |
| const out = new Set(); | |
| for (const id of [...owns, ...ctrls]) { | |
| const el = document.getElementById(id); | |
| if (el) out.add(el); | |
| } | |
| return [...out]; | |
| }; | |
| function evaluate() { | |
| const owner = document.activeElement; | |
| const attr = owner?.getAttribute?.('aria-activedescendant') || ''; | |
| const info = { | |
| focused: fmt(owner), | |
| ownerRole: owner?.getAttribute?.('role') || '—', | |
| ariaActivedescendant: attr || '—', | |
| resolvesTo: '—', | |
| targetRole: '—', | |
| relation: '—', | |
| status: '—' | |
| }; | |
| let ok = false; | |
| let line = ''; | |
| if (owner && attr) { | |
| const target = document.getElementById(attr); | |
| info.resolvesTo = fmt(target); | |
| info.targetRole = target?.getAttribute?.('role') || '—'; | |
| if (!target) { | |
| info.status = '⚠️ no target'; | |
| } else { | |
| const containers = relatedContainers(owner); | |
| const isDesc = owner.contains(target); | |
| const inOwned = containers.some(c => c.contains(target) || c === target); | |
| info.relation = isDesc ? 'descendant' : inOwned ? 'owned/controlled' : 'unrelated'; | |
| const reasons = []; | |
| if (!isDesc && !inOwned) reasons.push('not descendant/owned'); | |
| if (!isVisible(target)) reasons.push('target hidden'); | |
| if (!isVisible(owner)) reasons.push('owner hidden'); | |
| ok = reasons.length === 0; | |
| info.status = ok ? '✅ OK' : '⚠️ ' + reasons.join('; '); | |
| } | |
| } else { | |
| info.status = owner ? '⚠️ missing aria-activedescendant' : '⚠️ no focus'; | |
| } | |
| // Single-line summary | |
| line = `${ok ? '✅' : '⚠️'} owner=${info.focused} activedescendant="${info.ariaActivedescendant}" target=${info.resolvesTo} rel=${info.relation} role(owner/target)=${info.ownerRole}/${info.targetRole} :: ${info.status}`; | |
| if (VERBOSE) { | |
| console.log('[aria-activedescendant]', line, info); | |
| } else { | |
| console.log('[aria-activedescendant]', line); | |
| } | |
| } | |
| const mo = new MutationObserver((muts) => { | |
| for (const m of muts) { | |
| if (m.type === 'attributes' || m.type === 'childList') { evaluate(); break; } | |
| } | |
| }); | |
| mo.observe(document.documentElement, { | |
| attributes: true, | |
| subtree: true, | |
| childList: true, | |
| attributeFilter: ['aria-activedescendant','aria-owns','aria-controls','role','id','style','class'], | |
| }); | |
| ['focus','blur','keydown','keyup'].forEach(ev => { | |
| document.addEventListener(ev, () => setTimeout(evaluate, 0), true); | |
| }); | |
| // API | |
| window.__stopAadInspector = () => { mo.disconnect(); console.log('[aria-activedescendant] stopped'); }; | |
| window.__aadVerbose = (on = true) => { VERBOSE = !!on; console.log(`[aria-activedescendant] verbose=${VERBOSE}`); }; | |
| console.log('[aria-activedescendant] started. Use __stopAadInspector() to stop, __aadVerbose(true|false) to toggle details.'); | |
| evaluate(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
note: this script and documentation was vomited out by ChatGPT
aria-activedescendant Console Inspector --- Documentation
What it is
A zero-install snippet you paste into your browser's DevTools Console to
verify
aria-activedescendantwiring on any page. It watches focusand relevant DOM changes, then prints a single-line status showing
whether the focused element's
aria-activedescendantresolves to avalid target and how that target is related.
Quick start
combobox/listbox)
You'll see log lines like:
What it reports
Each log line contains:
tag#id (role=…).aria-activedescendanton the owner (or
—if absent).(or
—if missing).descendant→ target is inside the owner\owned/controlled→ target is inside any element referenced byowner's
aria-ownsoraria-controls\unrelated→ none of the above✅ OKor⚠️ …with reasons joined by;(e.g.,
not descendant/owned,target hidden,owner hidden,no target,missing aria-activedescendant).How it works (high level)
document (including
aria-activedescendant,aria-owns,aria-controls,role,id,style,class) and re-evaluateswhen any change occurs.\
aria-activedescendantvalue\aria-owns/aria-controlscontainer vs unrelated\Console API
Stop the inspector:
Toggle verbose mode (prints the concise line plus a full info
object):
Typical use cases
drive focus via
aria-activedescendant.visible, and is correctly related to its owner.
aria-activedescendantTips & caveats
not verify assistive technology announcements or user agent
mapping.\
(including
styleandclass) may be chatty. If needed, you can:__aadVerbose(false)reduced
attributeFilter(e.g., onlyaria-activedescendantand
role).\aria-ownsoraria-controls. That's helpful in practice, thoughnote that
aria-controlsis association, not parent/childsemantics.
Troubleshooting
after pasting.\
__aadVerbose(false)(default is already concise).\__stopAadInspector()to stop, then re-run a trimmed scriptwith fewer observed attributes.\
—: Check that the owner'saria-activedescendantvalue exactly matches an element ID onthe page.
Removal
At any time:
That's it---you've got a lightweight way to verify
aria-activedescendantlive as you navigate.