Skip to content

Fix broken ARIA reference on SVG icons - #4369

Open
kunalKumar-13 wants to merge 2 commits into
plone:masterfrom
kunalKumar-13:fix/3394-svg-icon-aria
Open

Fix broken ARIA reference on SVG icons#4369
kunalKumar-13 wants to merge 2 commits into
plone:masterfrom
kunalKumar-13:fix/3394-svg-icon-aria

Conversation

@kunalKumar-13

Copy link
Copy Markdown

Closes #3394.

Implements the approach @jensens set out on the issue in March. Quoting it so the reasoning is visible here:

All SVG icons get aria-hidden="true" — screen readers skip them. Keep <title> for browser hover tooltip when tag_alt is provided, but not for screen reader labeling. Remove the broken aria-labelledby.

Every icons.tag() call site renders the icon next to its own text label, so the icons carry no information of their own and are decorative in the ARIA sense.

The bug

_add_aria_title set aria-labelledby="title", referencing id="title". That id was never set on the <title> element, and _strip_id removes every id from the tree afterwards anyway — so the reference could never resolve. This is what the WAVE checker flagged in the original report.

One extra thing this surfaced

Writing the tests turned up a second, latent problem in the same function. The title was built with etree.Element("title"), which lands in no namespace, while the lookup on the line above searches for {svg-ns}title. So a title created by this code could never be found again, and a second pass over the same tree appended a duplicate <title>.

It is now created in the SVG namespace. The serialised output is unchanged — lxml emits <title>Bug</title> either way, which is why this was invisible in practice — so this is not a rendering change, only a correctness one. There is a test pinning both the idempotency and the serialised bytes.

Happy to split that into its own PR if you would rather keep this one to the ARIA change alone.

Tests

Six new cases in SVGAriaTest, exercising _add_aria_title directly so they need no Plone layer:

  • icon is aria-hidden="true", with and without an alt text
  • aria-labelledby is not emitted, and an existing one is removed
  • <title> kept when an alt is given, absent when it is not
  • a second pass does not duplicate the title
  • the serialised output contains aria-hidden, no aria-labelledby, and <title>Bug</title>

I ran those six locally against this branch (Python 3.13) and they pass; black --check is clean on both files. I have not run the full integration suite locally.

Note on the earlier discussion

@agitator and @thomasmassmann discussed role="presentation" and unique ids back in 2022. This follows @jensens' later proposal instead, which supersedes that — the icons are decorative, so no id-based labelling is needed at all. Happy to revisit if that is not the intent.

The icon resolver emitted aria-labelledby="title", which pointed at an id
that was never set on the title element — and _strip_id removes every id
from the tree in any case, so the reference could never resolve.

Every icons.tag() call site renders the icon next to its own text label, so
the icons carry no information of their own and are decorative in the ARIA
sense. They are now marked aria-hidden="true" and skipped by screen readers,
per the approach jensens set out on the issue.

The <title> tag is still emitted when an alt text is given, for the browser
hover tooltip. It is now created in the SVG namespace: a bare
etree.Element("title") landed in no namespace, so the lookup on the line
above could never find a title this code had created and a second pass
appended a duplicate. The serialised output is unchanged.

Closes plone#3394
Copilot AI lite review requested due to automatic review settings September 2, 2026 18:13
@mister-roboto

Copy link
Copy Markdown

@kunalKumar-13 thanks for creating this Pull Request and helping to improve Plone!

TL;DR: Finish pushing changes, pass all other checks, then paste a comment:

@jenkins-plone-org please run jobs

To ensure that these changes do not break other parts of Plone, the Plone test suite matrix needs to pass, but it takes 30-60 min. Other CI checks are usually much faster and the Plone Jenkins resources are limited, so when done pushing changes and all other checks pass either start all Jenkins PR jobs yourself, or simply add the comment above in this PR to start all the jobs automatically.

Happy hacking!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

_add_aria_title currently derives the SVG namespace from the default namespace mapping, which can mis-handle SVGs using a prefixed namespace and undermine the idempotency/correctness fix.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes an invalid ARIA relationship emitted by the SVG icon resolver and adjusts SVG title handling to be idempotent and consistent with the intended accessibility approach (icons are decorative and should be hidden from assistive technology).

Changes:

  • Set aria-hidden="true" on all SVG icons and remove any aria-labelledby attribute.
  • Keep emitting an SVG <title> only for hover tooltips when tag_alt/title is provided, and avoid duplicating it on repeated modification passes.
  • Add focused unit tests for _add_aria_title behavior (no Plone layer required) and a changelog entry for #3394.
File summaries
File Description
src/Products/CMFPlone/browser/icons.py Removes broken aria-labelledby, adds aria-hidden="true", and makes <title> creation/lookup idempotent.
src/Products/CMFPlone/tests/test_icons.py Adds unit tests covering ARIA/title behavior and serialization expectations.
news/3394.bugfix.md Documents the accessibility fix and the idempotency/namespace correction.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Products/CMFPlone/browser/icons.py Outdated
Comment on lines +32 to +40
ns = root.nsmap.get(None, "")
# set title tag
# A title tag is still useful: browsers show it as a hover tooltip. It is
# not used for screen reader labelling, which aria-hidden now suppresses.
title = root.find(f"{{{ns}}}title")
if title is None:
title = etree.Element("title")
root.append(title)
# Build it in the SVG namespace. A bare etree.Element("title") lands in
# no namespace, so the lookup above could never find a title this code
# had created and a second pass would append a duplicate one.
title = etree.SubElement(root, f"{{{ns}}}title" if ns else "title")
An SVG declaring its namespace with a prefix (<svg:svg xmlns:svg="...">)
has no default nsmap entry, so nsmap.get(None) fell back to no namespace
and the title would again be created outside the SVG namespace — the exact
problem this change set out to fix. etree.QName(root).namespace reads the
namespace off the root element itself and handles both forms.

Test added for the prefixed-namespace case.
@kunalKumar-13

Copy link
Copy Markdown
Author

Good catch from the Copilot review, and it was a real gap — pushed in c472ef9.

root.nsmap.get(None, "") reads the default namespace mapping. An SVG that declares its namespace with a prefix (<svg:svg xmlns:svg="http://www.w3.org/2000/svg">) has no default entry, so that returned "" and the title would have been created outside the SVG namespace again — precisely the problem this change was meant to fix. It now uses etree.QName(root).namespace, which reads the namespace off the root element itself and handles both forms. There is a test for the prefixed case.

All seven unit tests pass locally on Python 3.13 and black --check is clean on both files.

@kunalKumar-13

Copy link
Copy Markdown
Author

@jenkins-plone-org please run jobs

1 similar comment
@kunalKumar-13

Copy link
Copy Markdown
Author

@jenkins-plone-org please run jobs

@jensens jensens left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jensens

jensens commented Sep 3, 2026

Copy link
Copy Markdown
Member

@jenkins-plone-org please run jobs

@kunalKumar-13

Copy link
Copy Markdown
Author

Gentle nudge on this one — everything it was waiting on has come back green:

  • Plone Contributors Agreement verifier — passed
  • Changelog verifier — passed
  • Plone Jenkins CI pull-request-6.3-3.14 and 6.3-3.10 — both passed after the run you kicked off on 3 September

@jensens it is approved and mergeable_state is clean, so I do not think anything is outstanding on my side. Is there anything else you would like changed before it goes in, or is it just waiting for someone with the merge button?

No rush at all — flagging it only in case it slipped off the list rather than because it is blocked on a decision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broken ARIA reference in SVG icon

4 participants