Accessible names from a simplified tree
Your task
An accessibility checker needs the name a screen reader would announce for some elements, computed from a simplified DOM. tree is the root element. An element is an object with a role and, optionally: id, label (its aria-label), labelledby (its aria-labelledby, ids separated by spaces), alt, title, value (the current text of a textbox), hidden (true when it is hidden from assistive technology) and children, a list of elements and text nodes. A text node is a plain string. Ids are unique.
The computation starts at a target element. It reaches other nodes in two ways, as children (rule 7) and through labelledby (rule 3), and every node reached either way is nested. The target itself is not nested. For each node, the first rule that applies gives its name:
- Hidden. A hidden element gives an empty string. The exception is an element reached directly through a
labelledbyid: it is used even when hidden, and when it is hidden, everything inside it is used too. - Text. A text node gives its text.
- labelledby. If the element has
labelledbyand the computation is not already inside alabelledbyreference, compute the name of each referenced element that exists, in order, and join them with single spaces. If that is not blank, it is the name. Everything computed inside a reference ignoreslabelledby, so a chain is followed one step only and a cycle cannot loop. - Embedded textbox. A nested
textboxgives itsvalue, or an empty string when it has none. Itslabelis ignored. - label. A
labelthat is not blank. - alt. An
imgthat has analtkey gives thatalt, even when it is empty: an emptyaltmarks a decorative image, and no later rule applies to it. - Content. If the element is nested, or its role is one of
button,link,heading,checkbox,tab,menuitem,optionorcell, concatenate the names of its children in order, adding nothing between them. If that is not blank, it is the name. - title. A
titlethat is not blank. - Otherwise, an empty string.
Blank means empty or only whitespace. Return the name of each id in targets, in order, with every run of whitespace (spaces, tabs, newlines) collapsed to one space and none at either end. Targets always exist and are never hidden.
Example. With this tree:
{"role": "generic", "children": [
{"id": "save", "role": "button", "children": [
{"role": "img", "alt": ""}, "Save ",
{"role": "generic", "hidden": true, "children": ["(Ctrl+S) "]}, "draft"]},
{"id": "del", "role": "button", "labelledby": "del file", "children": ["Delete"]},
{"id": "file", "role": "generic", "children": ["report.pdf"]},
{"id": "remind", "role": "checkbox", "children": [
"Remind me ", {"role": "textbox", "label": "Minutes", "value": "10"}, " minutes before"]},
{"id": "q", "role": "textbox", "value": "shoes", "labelledby": "hint"},
{"id": "hint", "role": "generic", "hidden": true, "children": [
"Search the ", {"role": "generic", "hidden": true, "children": ["whole "]}, "store"]}
]}
accessible_names(tree, ["save", "del"])returns["Save draft", "Delete report.pdf"]: the decorative image and the hidden shortcut add nothing, anddelnames itself first, thenfile.accessible_names(tree, ["remind"])returns["Remind me 10 minutes before"]: the embedded textbox reads as its value.accessible_names(tree, ["q", "file"])returns["Search the whole store", ""]:hintis hidden but referenced, so all of it counts, and the textbox's own value is not its name.fileis agenericelement named directly, so rule 7 does not apply to it and it has no name of its own.
Examples
accessible_names(tree: dict, targets: list[str]) -> list[str]accessible_names({"role": "generic", "children": [{"id": "save", "role": "button", "children": [{"role": "img", "alt": ""}, "Save ", {"role": "generic", "hidden": True, "children": ["(Ctrl+S) "]}, "draft"]}, {"id": "del", "role": "button", "labelledby": "del file", "children": ["Delete"]}, {"id": "file", "role": "generic", "children": ["report.pdf"]}, {"id": "remind", "role": "checkbox", "children": ["Remind me ", {"role": "textbox", "label": "Minutes", "value": "10"}, " minutes before"]}, {"id": "q", "role": "textbox", "value": "shoes", "labelledby": "hint"}, {"id": "hint", "role": "generic", "hidden": True, "children": ["Search the ", {"role": "generic", "hidden": True, "children": ["whole "]}, "store"]}]}, ["save", "del"])["Save draft", "Delete report.pdf"]accessible_names({"role": "generic", "children": [{"id": "save", "role": "button", "children": [{"role": "img", "alt": ""}, "Save ", {"role": "generic", "hidden": True, "children": ["(Ctrl+S) "]}, "draft"]}, {"id": "del", "role": "button", "labelledby": "del file", "children": ["Delete"]}, {"id": "file", "role": "generic", "children": ["report.pdf"]}, {"id": "remind", "role": "checkbox", "children": ["Remind me ", {"role": "textbox", "label": "Minutes", "value": "10"}, " minutes before"]}, {"id": "q", "role": "textbox", "value": "shoes", "labelledby": "hint"}, {"id": "hint", "role": "generic", "hidden": True, "children": ["Search the ", {"role": "generic", "hidden": True, "children": ["whole "]}, "store"]}]}, ["remind"])["Remind me 10 minutes before"]accessible_names({"role": "generic", "children": [{"id": "save", "role": "button", "children": [{"role": "img", "alt": ""}, "Save ", {"role": "generic", "hidden": True, "children": ["(Ctrl+S) "]}, "draft"]}, {"id": "del", "role": "button", "labelledby": "del file", "children": ["Delete"]}, {"id": "file", "role": "generic", "children": ["report.pdf"]}, {"id": "remind", "role": "checkbox", "children": ["Remind me ", {"role": "textbox", "label": "Minutes", "value": "10"}, " minutes before"]}, {"id": "q", "role": "textbox", "value": "shoes", "labelledby": "hint"}, {"id": "hint", "role": "generic", "hidden": True, "children": ["Search the ", {"role": "generic", "hidden": True, "children": ["whole "]}, "store"]}]}, ["q", "file"])["Search the whole store", ""]