Fix: notification links must have discernible text (a11y link-name)#128
Fix: notification links must have discernible text (a11y link-name)#128ZebaAfiaShama wants to merge 2 commits into
Conversation
The frontend link (a.notificationx-link) and the announcement-theme link
button rendered with no text when a link is configured but the button text
is hidden/empty and no icon is shown, producing an empty accessible name.
This fails the axe/Lighthouse "link-name" rule ("Links must have discernible
text") and leaves the link unlabeled for screen readers and a11y-tree checks.
Add an aria-label fallback (configured button text -> notification title ->
destination URL) on both Analytics.tsx anchor branches (press_bar and
default) and the announcement Button.js anchor, applied only when no visible
text is rendered. Also mark the decorative press-bar button icon with alt="".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime loads the prebuilt assets/public/js/frontend.js, so the source change alone (Analytics.tsx / Button.js) has no effect until the bundle is rebuilt. Apply the same aria-label fallback (button text -> title -> URL) to the a.notificationx-link anchors in the compiled bundle so the fix is live when this branch is installed as a plugin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Found okay
1. Source-side review — ✅ logic is sound
const isLinkTextRendered = !!(
(config.source === 'press_bar' ? config.link_text : config.link_button) &&
link_text && String(link_text).trim()
);
const accessibleLabel = isLinkTextRendered
? undefined
: (link_text || config?.title || link || undefined);
const accessibleLabel =
(announcement_link_button_text && String(announcement_link_button_text).trim())
? undefined
: (config?.title || link || undefined);Correct — these themes only have button text (no separate body), so the title-then-link fallback is sufficient. Anchor render-guard check. PR body says "the anchor only renders when a destination exists". Confirmed by reading the surrounding JSX: both anchors are wrapped in conditions that require 2. Compiled-bundle review — ✅ patches at the correct call sites,
|
| Metric | master | PR-128 |
|---|---|---|
| Bundle size | 1,277,844 bytes | 1,277,157 bytes |
aria-label occurrences |
21 | 23 (+2) |
Identified the 2 net-new sites in the minified bundle:
// Site 1 — press_bar branch (note button_icon check immediately after)
"…onClick:fn,style:R,\"aria-label\":d||(null==t?void 0:t.title)||u},h),(t?.button_icon)&&\"none\"!==…"
// Site 2 — default branch (note link_button check immediately after)
"…onClick:fn,\"aria-label\":d||(null==t?void 0:t.title)||u},i),t.link_button?d:\"\",\" \",M),…yt_channel_…"Both sites are inside the Analytics-shaped IIFE with the exact context markers that match the two source branches. ✅ Right call sites. Fallback expression d || (t?.title) || u decodes to link_text || config?.title || link — matches the source fallback. ✅
⚠️ Source/bundle semantic divergence (worth a question)
The source uses a conditional:
const accessibleLabel = isLinkTextRendered ? undefined : (link_text || title || link);The compiled bundle drops the isLinkTextRendered guard and just inlines the fallback unconditionally:
"aria-label": d || (t?.title) || uBehaviour difference:
| Case | Source emits | Bundle emits | A11y impact |
|---|---|---|---|
| Button text rendered (toggle on, text non-empty) | no aria-label |
aria-label="<button text>" (same as visible) |
None — values match. Mildly redundant. |
| Button text hidden (toggle off OR text empty) | aria-label="<title|url>" |
aria-label="<title|url>" |
Identical |
Why it diverged: PR body says the bundle commit was hand-patched, not a full rebuild — "If CI rebuilds the bundle on merge, that commit can be dropped." The hand-patch is a slightly simplified version. Functionally fine, but the bundle and source aren't byte-for-byte equivalent.
Recommendation: rebuild from source on merge to keep bundle ≡ source.
Problem
During an accessibility audit (Lighthouse / axe
link-name), NotificationX links fail with "Links must have discernible text."Reported failing element:
The anchor has an empty accessible name, so screen readers and accessibility-tree validation can't interpret the link.
Root cause
In
frontend/core/Analytics.tsx, thea.notificationx-linkelement's only text-bearing child is the optional button text:When a link is configured (
linkset,link_type !== 'none') but the link text is hidden/empty (link_text/link_buttonoff, orlink_button_text === '') and no button icon is shown, every child resolves to empty/whitespace → empty<a>→ empty accessible name. There is noaria-label/titlefallback. The notification content (image/title) is a sibling component, so it doesn't name this anchor.The announcement-theme link button (
frontend/themes/helpers/Button.js, themes 13–15) has the same pattern — its only child isannouncement_link_button_text.Fix
Add an
aria-labelfallback — configured button text → notification title → destination URL — applied only when no visible text is rendered (so links that already have visible text/icons are untouched and not overridden):Analytics.tsx: both anchor branches (press_bar+ default); also mark the decorative press-bar button iconalt="".Button.js: announcement-theme anchor.Since the anchor only renders when a destination exists, the accessible name is now never empty.
Verification
Validated the exact anchor expression with axe-core (the engine Lighthouse uses) against the failing config (
linkset,link_text:false,link_button_text:""):""link-nameNotes
assets/public/js/frontend.jsneeds the standard frontend build to regenerate.Update: compiled bundle included
The runtime loads the prebuilt
assets/public/js/frontend.js, so the source change has no effect until that bundle is rebuilt. This branch now also commits the samearia-labelfix compiled intofrontend.js(botha.notificationx-linkanchors), so it works as soon as the branch is installed. If CI rebuilds the bundle on merge, that commit can be dropped — but please confirm the build actually emits (the current admin@types/reacttype errors block emit unless built with locked deps orTSC_COMPILE_ON_ERROR=true).