Check for duplicates
Description
In the flyout, every block's focusable element gets aria-owns listing all of its connection IDs — including connections that never get a DOM element. Output connections don't render a highlight in the flyout, so for every value block the aria-owns list contains an IDREF that resolves to nothing:
<path class="blocklyPath" id="blockly-r" role="option"
aria-label="true, has input"
aria-owns="d|giK=N5?S7lC$FzH9-4_connection_blockly-s">
<!-- no element with that id exists anywhere in the document -->
With the standard Logic category open, 4 of the 13 connection refs dangle — one per value block (logic_compare, logic_operation, logic_negate, logic_boolean), each being that block's output connection. The statement block (controls_if) is clean, which matches the cause exactly.
Root cause — packages/blockly/core/block_flyout_inflater.ts (line 95 on v13.1.1, unchanged on main as of 2026-07-26) owns every connection unconditionally:
const ownedConnectionIds = block.getConnections_(true).map((c) => c.id);
while rendered_connection.ts already documents that "output connections don't have highlights", and RenderedConnection.canBeFocused() already answers "does a DOM element exist for this connection". Suggested fix (one line, using the existing predicate):
const ownedConnectionIds = block
.getConnections_(true)
.filter((c) => c.canBeFocused())
.map((c) => c.id);
axe-core reports this as aria-valid-attr-value (Invalid ARIA attribute value: aria-owns="…") on 4.10.3, 4.11.4 and 4.12.1, and Lighthouse (Snapshot mode with the flyout open, desktop, no screen emulation) fails the same audit. Severity, honestly stated: browsers/AT ignore unresolvable IDREFs, so the practical impact is robustness/validator noise rather than a user-facing barrier — but it fails any axe-based CI an embedding app runs, and the fix looks trivial.
Worth knowing why this hasn't been reported before: the flyout renders lazily, so a pristine page load has zero axe violations — the dangling refs only exist once a category's flyout has been opened. Any load-time audit (default Lighthouse included) reports the page clean.
Aside, explicitly not claimed as a violation: axe also flags aria-required-children on the flyout's g[role="listbox"] for the role="figure" connection highlights. We believe that one is an axe attribution gap (the figures are aria-owns'd by the options, and axe's handling of aria-owns reparenting is documented as incomplete — dequelabs/axe-core#214, #4498). Since the flyout figures are unfocusable and unlabeled, hiding them in the flyout (as the inflater already does for non-top-level blocks) would silence that checker noise too, if you want it.
Reproduction steps
Self-contained page (stock Blockly from unpkg, one category, standard blocks):
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>repro</title>
<style>html,body{height:100%;margin:0}#workspace{height:100%}</style></head>
<body>
<div id="workspace"></div>
<script src="https://unpkg.com/blockly@13.1.1/blockly.min.js"></script>
<script>
Blockly.inject('workspace', { toolbox: { kind: 'categoryToolbox', contents: [{
kind: 'category', name: 'Logic', colour: '210', contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
]}]}});
</script>
</body>
</html>
- Open the page, click the Logic category (the flyout must be open — the refs don't exist before that).
- In the DevTools console:
[...document.querySelectorAll('g[role="listbox"] [aria-owns]')].flatMap(el =>
el.getAttribute('aria-owns').split(/\s+/)
.filter(id => !document.getElementById(id))
.map(id => ({block: el.getAttribute('aria-label'), missing: id})))
Expected: []. Actual: 4 rows — one per value block, each a …_connection_… id with no element.
Alternatively run axe-core (any of 4.10–4.12) on the page with the flyout open: aria-valid-attr-value, critical, on the logic_boolean block's path.
Priority
Work effort: looks like a quick fix — one line in block_flyout_inflater.ts, using the existing RenderedConnection.canBeFocused() predicate; no behaviour change for connections that do render. (Maintainers may of course prefer a different remedy, e.g. rendering output-connection highlights in the flyout.)
Stack trace
No response
Screenshots
No response
Browsers
Chrome (also verified via headless Chromium and Lighthouse 13.3)
Check for duplicates
Description
In the flyout, every block's focusable element gets
aria-ownslisting all of its connection IDs — including connections that never get a DOM element. Output connections don't render a highlight in the flyout, so for every value block thearia-ownslist contains an IDREF that resolves to nothing:With the standard Logic category open, 4 of the 13 connection refs dangle — one per value block (
logic_compare,logic_operation,logic_negate,logic_boolean), each being that block's output connection. The statement block (controls_if) is clean, which matches the cause exactly.Root cause —
packages/blockly/core/block_flyout_inflater.ts(line 95 on v13.1.1, unchanged onmainas of 2026-07-26) owns every connection unconditionally:while
rendered_connection.tsalready documents that "output connections don't have highlights", andRenderedConnection.canBeFocused()already answers "does a DOM element exist for this connection". Suggested fix (one line, using the existing predicate):axe-core reports this as
aria-valid-attr-value(Invalid ARIA attribute value: aria-owns="…") on 4.10.3, 4.11.4 and 4.12.1, and Lighthouse (Snapshot mode with the flyout open, desktop, no screen emulation) fails the same audit. Severity, honestly stated: browsers/AT ignore unresolvable IDREFs, so the practical impact is robustness/validator noise rather than a user-facing barrier — but it fails any axe-based CI an embedding app runs, and the fix looks trivial.Worth knowing why this hasn't been reported before: the flyout renders lazily, so a pristine page load has zero axe violations — the dangling refs only exist once a category's flyout has been opened. Any load-time audit (default Lighthouse included) reports the page clean.
Aside, explicitly not claimed as a violation: axe also flags
aria-required-childrenon the flyout'sg[role="listbox"]for therole="figure"connection highlights. We believe that one is an axe attribution gap (the figures arearia-owns'd by the options, and axe's handling ofaria-ownsreparenting is documented as incomplete — dequelabs/axe-core#214, #4498). Since the flyout figures are unfocusable and unlabeled, hiding them in the flyout (as the inflater already does for non-top-level blocks) would silence that checker noise too, if you want it.Reproduction steps
Self-contained page (stock Blockly from unpkg, one category, standard blocks):
Expected:
[]. Actual: 4 rows — one per value block, each a…_connection_…id with no element.Alternatively run axe-core (any of 4.10–4.12) on the page with the flyout open:
aria-valid-attr-value, critical, on thelogic_booleanblock's path.Priority
Work effort: looks like a quick fix — one line in
block_flyout_inflater.ts, using the existingRenderedConnection.canBeFocused()predicate; no behaviour change for connections that do render. (Maintainers may of course prefer a different remedy, e.g. rendering output-connection highlights in the flyout.)Stack trace
No response
Screenshots
No response
Browsers
Chrome (also verified via headless Chromium and Lighthouse 13.3)