a11y: focused screen-reader cleanups across several screens#2819
Closed
rfiorentino1 wants to merge 55 commits into
Closed
a11y: focused screen-reader cleanups across several screens#2819rfiorentino1 wants to merge 55 commits into
rfiorentino1 wants to merge 55 commits into
Conversation
Replace the three `lodash` functions used by the backend with native equivalents: `isEqual` -> `node:util` `isDeepStrictEqual`, `uniq` -> `[...new Set(...)]`, and `orderBy` -> a small inline stable sort. Removes the direct `lodash` and `@types/lodash` dependencies.
Use `marked` directly via a new `MarkdownComponent` and inline the post-render fixups (link target, alt fallback, emoji conversion) that `PluginsMarkdownDirective` was doing. Removes the `ngx-md` dependency and its `prismjs` transitive, saving ~112 KB raw / ~30-50 KB gzipped from the UI bundle. The `NgxMdModule` import in `node-version-modal` was unused; removed without replacement.
Replace `withDefaultRegisterables()` with an explicit list covering the line-chart pieces the CPU/memory/network widgets actually need (`LineController`, `LineElement`, `PointElement`, `Filler`, `LinearScale`, `CategoryScale`). Saves ~56 KB raw JS from the UI bundle.
The login and setup-wizard pages display this logo at 100x100, but the existing SVG was a thin wrapper around an 850x856 base64 PNG (168 KB). Render the SVG to a 300x300 (3x for retina) WebP at q=90 to swap a heavy asset for an indistinguishable one at 6% of the size. Saves ~157 KB of eagerly loaded payload on the first-load pages.
Replace the 60 legacy `fa fa-…` usages with explicit `fas`/`far` style classes and current v6 icon names (sourced from Font Awesome's own shims.yml so rendering is unchanged), then drop the `v4-shims.scss` import. Removes ~25 KB of shim CSS and the `fa-v4compatibility.woff2` font from the bundle, and unblocks safe font subsetting.
Add a prebuild step that scans ui/src for referenced icon names and rewrites the installed Font Awesome web fonts down to just those glyphs. No SCSS or markup changes, so the icon CSS and any dynamically-applied classes keep working. Bundled fonts drop from ~238 KiB to ~15 KiB: fa-solid-900 112.1 -> 8.8 KiB fa-regular-400 18.5 -> 4.2 KiB fa-brands-400 107.5 -> 1.8 KiB
@ng-formworks/core and @ng-formworks/cssframework declare lodash-es as their dependency but their compiled fesm2022 bundles import the non-tree-shakeable CommonJS lodash. This dragged the full ~106 KB lodash into the lazy JSON-schema-form chunk even though only a handful of functions are used. Extend the existing patch-package patches to rewrite those bare lodash imports to lodash-es (identical 4.17/4.18 code, just ES modules) so esbuild can tree-shake them. lodash-es exposes both the default and named exports the bundles use, so the rewrite is runtime-equivalent. A global npm alias was not viable because the build-time subset-font tool still needs CommonJS lodash, and Angular's esbuild builder ignores tsconfig paths for imports originating inside node_modules. Result: the lazy form chunk drops from 577 KB to 464 KB raw (-111 KB); app-wide -48 KB raw / -13 KB gzip. All 439 e2e tests pass.
The markdown component only used emoji-js for `replace_colons`. Shipping the ~255kb library for that is wasteful, so distil its data into a compact ~45kb name->emoji map (regenerated via scripts/emoji-shortnames.mjs) and drop emoji-js to a devDependency.
Adds visually-hidden ARIA live regions to 11 HAP sensor types so screen reader users hear status changes (e.g. "Front door, motion sensor, detected"). Marks decorative SVGs and visual text labels with aria-hidden so they are skipped by screen readers — the live region announces them. Sensors covered: battery, carbon-dioxide-sensor, carbon-monoxide-sensor, contact-sensor, humidity-sensor, leak-sensor, light-sensor, motion-sensor, occupancy-sensor, smoke-sensor, temperature-sensor. Carries forward part of the accessibility improvements from homebridge#2677, adapted to beta's signal-based service refactor and the types/hap/ directory restructure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s to HAP/matter tiles Continues the accessibility work from homebridge#2677 across the remaining accessory tiles: - HAP interactive tiles (air-quality-sensor, switch, air-purifier, door, garage-door-opener) now expose role="button"/role="switch", aria-label, aria-checked where appropriate, tabindex="0", and (keydown.enter)/(keydown.space) handlers so keyboard and screen reader users can operate them. - HAP simple sensors (the 11 from b11d9aa98) re-use the lowercase pipe in templates instead of .toLowerCase() to satisfy angular-template lint rules. - HAP door.manage + garage-door-opener.manage modals get role="status" aria-live="polite" on the state div and aria-hidden on label decorations; garage's target buttons gain aria-pressed. - Matter sensors that mirror HAP sensors (contact, humidity, light, occupancy, smoke-co-alarm, temperature, water-leak-detector, air-quality-sensor) gain the same visually-hidden live region + aria-hidden treatment so screen readers announce state changes. Carries forward the accessibility improvements from homebridge#2677. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ninstall, config modals Carries forward the accessibility improvements from homebridge#2677, adapted to beta's signal-based component APIs and current paths under core/plugins/. - plugin-card: convert anchor-as-button action triggers to <button>, add aria-hidden on all decorative icons, label the plugin info button ("Plugin Info") with the visible npm name marked aria-hidden, add install tooltip + aria-label to the not-installed download button, rename dropdown toggle label to "Plugin Actions" - plugin-config: add aria-labels to the delete/edit/add-block buttons, add modal-title id, mark all decorative icons aria-hidden - manage-plugin: mark all decorative icons aria-hidden, convert backup download anchor to a button with aria-label - uninstall-plugin: mark all decorative icons aria-hidden Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nate anchor Follow-up on the previous plugin a11y commit: - plugin-card: the "Plugin Info" button now reads "Plugin info for <name>" so screen reader users still hear the npm package name (previously the visible name was aria-hidden with no equivalent in the label). - plugin-card: convert the donate anchor (href="javascript:void(0)") to a real <button>, matching the other action triggers in this card. - plugin-config: wire the modal-content aria-labelledby to the title id added in the previous commit, so the dialog announces its title.
Move three hardcoded English strings I added in the plugin a11y work
into i18n keys, and propagate to all locales via lang-sync.
- plugins.button_actions: "Plugin Actions" (dropdown toggle)
- plugins.button_info: "Plugin info for {{ pluginName }}" (npm-name button)
- plugins.config.add_block: "Add new config block" (add-block button)
The donate button now reuses the existing plugins.donate.tile_donate_to
key ("Donate to {{ author }}") instead of a new "Support {{ author }}" string.
…in dashboard widgets Carries forward the screen-reader improvements from homebridge#2677 for the status dashboard area, adapted to beta's signal-based components. - status.component: aria-hidden on the eye/lock/unlock toolbar icons. - widget-control: aria-hidden on label spans whose text duplicates the paired input's aria-label (hide_npm, expand_docker, show_toolbar) and on the search-city decorative icon. - bridges-widget: aria-hidden on the spinner icons (other a11y already in place via beta). - update-info-widget: convert 17 anchor-as-button instances to real <button type="button">, remove redundant href="javascript:void(0)" on routerLink anchors, and aria-hidden every decorative icon. - node-version-modal: aria-hidden on the spinner, node-js logo, per-plugin status icons, and the external-link icon in the footer link. Skipped (significant scope beyond attribute grafts; flagged for follow-up): - status.component reorder mode with keyboard nav (~400 lines of TS + live region). - bridges-widget whole-row-as-button refactor + live regions. - terminal-widget screen-reader-mode wiring for xterm (.ts side).
- Mark the current page in the sidebar with aria-current="page" via the routerLinkActive ariaCurrentWhenActive input, so screen readers announce the active menuitem as the current page. - Add aria-hidden="true" to decorative Font Awesome icons in every menu row, the hamburger toggle SVG, and the Homebridge logo link. - Hide the duplicate logo link from the accessibility tree (tabindex="-1" + aria-hidden="true") so the explicit "Status" menuitem is the canonical entry point and there is no nested-interactive issue inside the m-header role="button". - Add aria-label to the sidebar navigation landmark and the desktop header toggle (previously unlabelled). - Restore the missing menu.sidebar.aria_menu i18n key (referenced by the template but removed from en.json a while ago) and lang-sync to all 29 locales. - Propagation pass: add aria-hidden="true" to the three decorative icons in restart.component.html. Carries forward the accessibility improvements from homebridge#2677.
…een readers
settings.component.html
- Add aria-hidden="true" to 68 decorative Font Awesome icons (section
toggle chevrons, save indicators, arrow buttons, warning triangles,
search icon, exclamation icons). Every <i> in the template is now
hidden from screen readers since the surrounding buttons/inputs
already carry aria-label.
backup.component.html
- Add aria-hidden to all decorative icons (hard-drive header, action
arrows, exclamation, history/download/trash row buttons).
- Mark the empty rendux-label as aria-hidden — it is purely a styled
toggle visual with no text content.
- Differentiate the two ambiguous "Backup Now" buttons for screen
readers: the download button now reads as form.button_download
("Download"), the create-on-server button keeps backup.backup_now
("Backup Now"). Previously both buttons announced identically.
- Wrap the per-backup restore/download/delete buttons in
role="group" aria-label="Backup actions" so screen readers can
navigate them as a unit (the CHANGELOG-rocco intent for backups).
config-editor.component.html
- Add aria-hidden to decorative icons (cancel, restore, columns/bars
toggle, save floppy/spinner).
- Add aria-label to the mobile fallback <textarea> so screen readers
know it is the Homebridge JSON config — previously unlabelled.
config-restore.component.html
- Add aria-hidden to decorative icons that were missed by earlier
beta a11y commits (history, download, trash spinners).
- Wrap the per-backup buttons in role="group" with the shared
backup.aria_actions label, matching the backup-modal grouping.
i18n
- Add backup.aria_actions key ("Backup actions") and lang-sync.
Deferred for a follow-up commit (substantial .ts/CSS scope, same
bucket as the earlier deferred xterm/screen-reader-mode work):
- The custom restart-required toast in settings.component.ts with
role="alert", aria-live="assertive", and a real Restart Homebridge
<button> inside the toast.
- xterm screenReaderMode + DOM scrubbing in logs.component.ts (same
shape as the deferred manage-plugin and terminal-widget xterm SR
mode work).
- aria-label -> aria-labelledby refactor across ~50 form labels in
settings.component.html (label spans would need ids; current
aria-label approach is functionally equivalent for screen readers).
Carries forward the accessibility improvements from homebridge#2677.
…nstall completion logs.component.ts - Add patchXtermLiveRegion() to set role="status", aria-live="polite", aria-atomic="true" on the xterm screen-reader live region. Defaults to assertive announcements which is too noisy for streaming logs. - Pass disableStdin: true to getTerminalOptions(). Logs are read-only so the xterm textarea should not accept input (matches the plugin install/uninstall terminal already on beta). terminal-widget.component.ts - Same patchXtermLiveRegion() pattern, called from ngOnInit and onVisibilityChange. Mirrors the patch already present on the dedicated platform-tools/terminal page in beta. manage-plugin.component.html - Wrap the @if (actionComplete()) modal-body in role="status" aria-live="polite" aria-atomic="true". When a plugin install, uninstall, or update finishes, screen-reader users now hear the success message (e.g. "Installed: homebridge-foo (v1.2.3)") without having to navigate to find it. Note on PR homebridge#2677's scope: the PR adds a curated speakAction() pattern in manage-plugin that suppresses the xterm live region (aria-live=off) and replaces it with announcements like "Installing foo." / "foo updated, restart to apply changes." That work requires ~7 new i18n keys and orchestration of terminalAriaHidden state. Deferred to a follow-up. Beta keeps the xterm's announcements active (just less noisy via patchXtermLiveRegion) which is a reasonable middle ground. Also skipped: the PR's iconStar / iconThumbsUp role="img" + aria-label wrappers on donation-message icons. Those icons are decorative flourishes inside a translated text message; making them announceable would have screen readers read "star image, thumbs-up image" mid- sentence. Beta's empty <i> elements are already correctly silent. Carries forward the accessibility improvements from homebridge#2677.
…dge modal plugins.component.html - Add aria-expanded and aria-controls to the search and stats toggle buttons in the header so screen readers can tell whether the panel is open and which region it controls. - Add id="plugin-search-region" on the search row so the new aria-controls reference resolves. - Add aria-label to the search input (placeholder is not a valid SR label). - Add aria-hidden="true" to the three header icons (search, stats, help) and the empty-state icon — buttons and surrounding context already carry the meaning. - Wrap the empty-state alert in role="status" aria-live="polite" so "no matching plugins" is announced when the search returns nothing. - Add an aria-label to the external analytics link (plugins.stats_open_in_new_tab) since the icon-only anchor was unlabelled. plugin-bridge.component.html - Add aria-hidden="true" to ~14 decorative Font Awesome icons (loading spinner, advanced-section chevron, HAP and Matter brand icons, QR-code placeholders, link/paired indicators, save spinner) and the empty rendux-label toggle labels. - Mark the plugin logo <img> as decorative (alt="" aria-hidden) — the modal title already announces the plugin name. plugin-logs.component.html - Add aria-hidden to the download and restart-bridges icons (the buttons themselves carry aria-label). i18n - Add plugins.stats_open_in_new_tab key, lang-synced to 29 locales. Already covered on beta and skipped here: - plugin-card.component.html (33 aria-hidden vs PR's 15) — fully a11y'd by the homebridge#2677 group 3 work (commit bb37013c9). - plugin-config.component.html — already has aria-labelledby plus the icons aria-hidden. - manage-plugin.component.html — already has the download-backup button labelled (homebridge#2677 group 3 covers the same surface). - custom-plugins.component.html — iframe already has title and aria-label via plugins.button_settings on beta. - uninstall-plugin.component.html — PR's role="presentation" on the modal-content is an anti-pattern (removes the dialog from the SR tree), so the aria-describedby addition is skipped. Carries forward the accessibility improvements from homebridge#2696. Deferred to a follow-up commit (same bucket as the earlier xterm SR mode work): - manage-plugin.component.ts speakAction() + applyXtermA11yPatches() - plugin-logs.component.ts xterm DOM scrub
homebridge-logs-widget.component.{html,ts}
- Add aria-hidden="true" to the three toolbar button icons (search,
download, trash). The buttons themselves already have aria-labels;
the icons inside are decorative.
- Add patchXtermLiveRegion() and pass disableStdin: true to
startTerminal. Brings this widget into line with the same pattern
already on logs.component.ts and terminal-widget.component.ts:
xterm's default aria-live="assertive" is too noisy for streaming
logs (set to polite + role="status" + aria-atomic), and logs are
read-only so the xterm textarea should not accept input.
bridges-widget.component.html
- Add the missing aria-hidden="true" to the main bridge spinner.
The child bridge spinner already had it from homebridge#2677 group 4
(commit 805322214); the main one was missed.
Carries forward the accessibility improvements from homebridge#2697.
Deferred (substantial behavioural refactors, out of scope for pure
attribute grafting):
- homebridge-logs-widget expand/collapse title button + state
- terminal-widget srExpanded toggle + applyTerminalA11yState
- bridges-widget whole-row-as-button refactor + per-row aria-live regions
- status.component dashboard reorder mode (~400 lines)
- widget-visibility add/remove-in-place refactor (replaces close-and-add
with toggleWidget mutation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… icons - Add aria-hidden="true" to the three toolbar button icons (search, download, trash). The buttons already have translated aria-labels; the icons inside are decorative. Mirrors the same fix applied to homebridge-logs-widget. - Add aria-expanded + aria-controls on the search toggle button so screen readers announce whether the search panel is open. id matches the @if-rendered search region wrapper. Carries forward the accessibility improvements from homebridge#2698. Deferred (substantial behavioural refactors, out of scope for pure attribute grafting): - config-editor preferPlainTextEditor toggle (~50 .ts lines + new <select> control + localStorage state). Genuinely useful for SR users who struggle with Monaco, but a UX feature, not a pure a11y fix. - logs.component.ts xterm DOM scrub (querySelector for textarea, [aria-live], .xterm-accessibility children and hide them). Conflicts with the patchXtermLiveRegion() approach already on beta from 869d31d15 — that one keeps the live region but configures it polite+atomic. The PR's more aggressive scrub would undo that. Already on HEAD via earlier commits: - terminal.component.ts (patchXtermLiveRegion + screenReaderMode) — 869d31d15 - logs.component.ts (patchXtermLiveRegion + disableStdin) — 869d31d15 - config-restore.component.html — fully done (role="group" on action groups, aria-hidden on icons, visually-hidden labels, etc.) - config-editor.component.html mobile textarea aria-label Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…labels settings.component.html - Add aria-expanded + aria-controls on the search toggle button so screen readers announce whether the search panel is open. Mirrors the fix applied to logs.component.html. - Add aria-hidden="true" to the three duplicate-text spans next to setting-backup, setting-restore, and setting-users buttons. Each button already carries the same translated text as its aria-label, so SRs were announcing the label twice (once for the visible span, once for the button). Carries forward the accessibility improvements from homebridge#2699. Deferred (substantial behavioural refactors, out of scope for pure attribute grafting): - settings.component.ts restart-required toast rewrite (custom HTML toast with manually injected Restart button + click/keydown handlers on a synthesized <button>). Explicit in the deferred list. - sidebar.component.ts interaction refactor (cached element refs, touchstart routing, click-outside-to-close). Pure behavioural change, no a11y intent. - settings.service.ts getIframeOrigin helper extraction + comment removal. Pure refactor, no a11y intent. Already on HEAD (skipped): - backup.component.html — fully done (role="group" on per-backup action groups, aria-hidden on every icon and the toggle label, translated aria-labels on every button) - sidebar.component.html — already excellent (role="menuitem", tabindex=0, handleKeydown, ariaCurrentWhenActive="page", aria-hidden on all icons, <nav> wrapper with role+aria-label) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
on-off-light, on-off-light-switch, on-off-plug-in-unit - role="switch" + tabindex="0" + aria-checked bound to the on/off state - Computed full screen-reader text (name + type + state) as the accessory-box aria-label, mirroring the HAP switch component - Visually-hidden live region (role="status" aria-live="polite" aria-atomic="true") so state changes are announced after toggle - aria-hidden="true" on the SVG container, the SVG element (also focusable="false"), and both visible accessory-label divs since they're already covered by the parent aria-label - (keydown.space) handler in addition to enter, matching the switch role's expected keyboard interaction - LowerCasePipe import to support the includeType case-insensitive duplicate-name check Matter sensor tiles already received this pattern in af897f2e6. This extends the same propagation to the matter on/off tiles whose HAP shape equivalent (switch) already has it. The other matter types (dimmable/color lights, door-lock, fan, thermostat, window-covering, robotic-vacuum-cleaner, unknown) need new shape-specific work on both HAP and matter sides and are deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
terminal-widget.component.{html,ts}
- Add a visually-hidden-focusable button in the widget title with
aria-expanded + aria-controls so screen-reader users can opt into
the terminal pane. Visual users see no change (the button is only
rendered visibly when focused).
- Mirror srExpanded onto the xterm textarea: disabled + aria-hidden +
tabindex=-1 when collapsed. Otherwise the textarea stays in the tab
order on every dashboard load and disrupts keyboard navigation
through the other widgets.
- Set aria-hidden + inert on the terminal output container when
collapsed so AT trees skip past the xterm-accessibility live region
by default.
- kickFocusOutOfCollapsedTerminal() returns focus to the toggle button
when the widget collapses so focus never gets lost inside the now-
hidden terminal.
- Guard window-focus / click handlers to only re-activate the terminal
when srExpanded — was previously stealing focus on every page focus.
Carries forward the accessibility improvements from homebridge#2697 (deferred
from earlier in this branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hide the user-plus, support-question, admin-secret/user, and edit-pen icons from screen readers — every action button has an aria-label and the username text is already announced. Convert the "copy secret to clipboard" link in the enable-2FA modal from a javascript:void(0) anchor to a real <button> with aria-label, and add a visually-hidden live region that announces "Copied" when the secret is copied. Adds two shared i18n keys (common.a11y.copy_to_clipboard, common.a11y.copied) reused by the accessory-info copy buttons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The page already has a "Restart Homebridge" heading and three labelled buttons. The large power-off illustration in the header adds nothing beyond visual decoration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the two "copy to clipboard" links (HAP UID and UUID) from javascript:void(0) anchors to real <button>s with aria-label, and add a visually-hidden live region that announces "Copied" when the value lands on the clipboard. Hide the check/copy swap icons from screen readers. Reuses the common.a11y.copy_to_clipboard and common.a11y.copied keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hide Font Awesome decorative icons from screen readers across the plugin compatibility modal, plugin info modal, manual config editor, reset accessories modal, and the shared information CTA modal: - Top-of-modal illustrations (install/update/compare, Node.js, broom, circle-check, shield-alt verification badges) are visual cues only; the body text and modal title already announce the situation. - Inline external-link arrows alongside link text are decorative. - Action-button icons (trash-can, pen-to-square, plus, broom, undo, spinner) are inside buttons that already carry ngbTooltip or have surrounding labelled text. - Loading spinners are silent decoration — the surrounding state is already conveyed by the modal title or button label. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ten settings sections (general, display, startup, network, HAP, Matter, terminal, security, cache, reset) used <div role="button" tabindex="0"> headers with click-only handlers — keyboard users could focus them but pressing Enter or Space did nothing. Add keydown.enter and keydown.space handlers to match the click behaviour, with preventDefault on Space to stop page scroll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The global app-spinner used in loading sections (settings, accessories, plugin lists, status dashboard, etc.) is a pure CSS/SVG animation with no semantics — screen reader users had no signal that the page was loading. Wrap the spinner container in role="status" aria-live="polite" aria-busy="true" and add a visually-hidden "Loading" announcement. Mark the animation div and SVG aria-hidden so the decorative shape is not also announced. Adds common.a11y.loading translation key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eslint --fix reformatted four templates where the aria-hidden additions pushed line lengths past prettier's wrap point. No behavioural or semantic changes; pure whitespace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nouncements
sidebar.component.{html,scss}
Three related changes to the navigation drawer:
1. Each nav item changes from `<div class="link-row" role="menuitem" tabindex="0">`
to `<button type="button" class="link-row">`. `role="menuitem"` is only
valid inside a `role="menu"` container — wrapping menuitems in a
`role="navigation"` parent produces undefined screen-reader behaviour and
often skips items entirely. Buttons get correct semantics, native
Enter/Space activation, and pair cleanly with `routerLinkActive` +
`ariaCurrentWhenActive="page"` for the current-page announcement.
`(keydown)="handleKeydown"` is no longer needed and is dropped from the
item rows. The SCSS gets a small reset (background/border/font/text-align/
width) so the button renders pixel-identical to the previous div.
2. The decorative logo header inside `.sidebar` is no longer
`role="button" tabindex="0"`. It was a duplicate toggle that ended up
reading as a second "Menu" button on top of the m-header on mobile and
on top of the navigation landmark on desktop. It's now just a logo
wrapper, like the m-header's logo — `tabindex="-1" aria-hidden="true"`.
3. The `.sidebar` element is only a `role="navigation"` landmark with
`aria-label` on desktop. On mobile, the m-header button announces the
drawer's expanded state and the contents are already linear; a second
"Menu navigation" landmark just stutters with no added orientation
value.
Belt-and-suspenders on the m-header itself: when not on mobile it gets
`aria-hidden="true"` + `tabindex="-1"` + no role/aria-controls/aria-label,
in addition to the existing CSS `display: none` at ≥768px. Screen readers
that occasionally still surface display:none elements (or pre-CSS-load
captures) won't announce a phantom "Menu" button on desktop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ restart
bridges-widget.component.{html,ts,scss} + en.json
Each bridge row (main Homebridge + every child bridge) is now a single
<button> instead of a <div> + nested icon-only restart button. The
button's aria-label composes name, run state, matter state, and the
"Restart" action so a screen reader reads the row as one self-contained
control instead of forcing the user to navigate to a tiny power-off
icon to learn what the restart does.
Aria-label structure:
"<bridge name>, <Running|Not Running|Restarting>
[, Matter Running | Matter Not Running]
[, Restart]"
The matter clause only appears when Matter support is enabled AND the
bridge has matter configured; while a row is in transition (pending or
restarting) the status word is "Restarting" alone and matter is
suppressed to keep announcements short. The ", Restart" suffix only
appears when the user can actually trigger one (admin + not currently
restarting + not pending). `aria-disabled="true"` is applied in those
cases instead of the native `disabled` attribute so the row stays
focusable and the screen reader still announces its state.
Visual layout is unchanged: HAP and Matter status icons keep their
colour-coded classes, tooltips, and positions; the trailing power-off
or spinner icon is preserved. Those inner icons are now
`aria-hidden="true"` because the outer button already carries the full
label — without that, a screen reader would otherwise stutter "Matter
Running" twice (once from the icon, once from the outer label).
scss: add a small `.hb-status-row` reset (background, border, font,
color, width) so the <button> renders pixel-identical to the previous
<div>, and set `cursor: default` when aria-disabled.
Other fixes bundled in because they live in the same template:
- Restore broken HTML comments `<! -- Main Bridge - ->` /
`<! -- Child Bridges - ->` to proper `<!-- ... -->` syntax. The
malformed forms were rendering as visible text inside the widget.
- Fix copy/paste bug on the main bridge's Matter icon, which was
announcing `hap_not_running` / `hap_running` / `hap_not_enabled`
instead of the corresponding `matter_*` keys. Moot now that the icon
is aria-hidden, but corrected for any future surface that reads it.
en.json: restore `status.services.label_running`,
`status.services.label_not_running`, and add
`status.services.label_restarting`. The TS already referenced the first
two (`formatRestartCompleteMessage`), so without them the restart-complete
live region was announcing the raw key — e.g. "Homebridge restart
complete, status.services.label_running" — instead of "...Running".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… enabled bridges-widget.component.ts The previous version of `matterStatusLabel` only added a matter clause to the row's aria-label when matter was actually enabled for that specific bridge. That mirrored the bridge's matter *status* but not the matter *availability*: visually, the matter icon renders on every row whenever the `matterSupport` server feature flag is on, with tooltips cycling between Matter Not Enabled / Matter Not Running / Matter Running. The aria-label now mirrors that exactly: when matter support is enabled, every row announces one of those three states. When matter support is off at the server level, the matter clause stays silent (and the icon isn't rendered, so screen readers and sighted users see the same thing). The "while restarting" suppression is kept — during transition the row label is "Restarting" alone, since per-service status isn't meaningful mid-restart. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w/hide widgets modal
widget-visibility.component.html
Each widget row in the show/hide modal was producing five screen-reader
announcements for what is structurally one set of choices:
- Widget Name (heading)
- "Show on desktop" (visible span, no aria control)
- "Show on desktop <Widget Name>" (checkbox aria-label)
- "Show on mobile" (visible span)
- "Show on mobile <Widget Name>" (checkbox aria-label)
The widget name was spoken three times per row and the platform label
("Show on desktop") twice, with no semantic benefit — the visible spans
exist purely so sighted users can see what the toggle controls.
Hide the visible spans from the accessibility tree (`aria-hidden="true"`)
and trim the widget name out of each checkbox's aria-label so it isn't
re-announced right after the heading. The decorative platform icons and
the rendux toggle's visual `<label>` (CSS-only switch graphic, no text)
also get `aria-hidden="true"` for completeness — they had no text to
announce anyway, but being explicit prevents future regressions.
Per widget the announcement is now:
- Widget Name
- "Show on desktop, checkbox, checked"
- "Show on mobile, checkbox, unchecked"
Visual layout is unchanged — sighted users still see the labels, icons,
and toggle graphics exactly as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y terminals
log.service.ts + switch-to-scoped.component.ts + restore.component.ts
xterm.js always renders a hidden <textarea class="xterm-helper-textarea">
inside each terminal to capture keyboard input. Even when the terminal
is constructed with `disableStdin: true` the textarea stays in the DOM,
and screen readers find it as a focusable "Terminal input" form field —
which is wrong on views that are read-only: the logs page, the
homebridge-logs widget, plugin install/version-switch output, and the
backup-restore progress modal all have no input semantics, so a "Terminal
input" announcement is just dead surface for the user to navigate past.
Add a shared helper `hideXtermInputFromScreenReader()` to log.service.ts
that, after `term.open()`:
- Sets `aria-hidden`, `tabindex="-1"`, `readonly`, and the `disabled`
property on the helper textarea, and blurs it if it currently has
focus.
- Re-applies via a MutationObserver — xterm rebuilds parts of its DOM
as it renders, so a one-shot patch isn't enough. Also stagger four
timed retries at 0/50/250ms to cover xterm's lazy first-render path
before the observer is wired in.
- Returns a disposer the caller invokes on destroy to release the
observer.
Wired into log.service.ts (covers /logs and homebridge-logs widget),
switch-to-scoped.component.ts (plugin scope-switch progress modal), and
restore.component.ts (backup-restore progress modal). Each stores the
disposer and calls it from its respective destroy hook.
manage-plugin.component.ts is intentionally NOT touched — it already
ships its own (more aggressive) `applyXtermA11yPatches()` that does the
same job plus re-applies on every stdout write, and is called from
multiple staggered timeouts. Switching it to the shared helper would be
a no-op refactor we can do later if the duplication ever bites.
Interactive terminals (/terminal page, terminal widget on the dashboard,
both routed through terminal.service.ts) are unaffected — the helper
textarea is still announced as "Terminal input" because that's exactly
where the user is supposed to type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
plugin-card.component.html + i18n
The plugin info button's aria-label was "Plugin info for {{ pluginName }}",
which on a page with 20 plugin cards meant screen readers announced the
plugin name three times per card: once on the H5 card title, once as
visible (aria-hidden) grey text inside the button, and once again as
part of the button's own label. The repetition didn't add orientation —
the card heading already establishes which plugin the button belongs
to — it just made the page slower to traverse.
Shorten the aria-label to plain "Plugin info" and drop the `pluginName`
interpolation parameter from the template binding. The visible UI is
unchanged: the npm package name still renders inside the button as
aria-hidden grey text next to the shield icon.
i18n: every non-English locale currently held the literal English
string "Plugin info for {{ pluginName }}" (no real translation had
been done yet — lang-sync seeded them from en.json). Bulk-update all
29 non-English locale files to "Plugin info" so translators don't
have to chase a placeholder that the template no longer passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sidebar.component.html + i18n The sidebar buttons previously set `aria-current="page"` (via Angular Router's `ariaCurrentWhenActive`) when the corresponding route was active. That's the correct ARIA, but VoiceOver doesn't actually announce `aria-current` on `<button>` elements in practice — so a screen-reader user lands on a page and tabs through the sidebar with no audible cue that one of the items represents where they are. Bake the state into the aria-label instead. Each routerLink button gets a `#xxxRla="routerLinkActive"` template reference and an aria-label that conditionally appends ", current page" when the route is active. So the announcement becomes: - inactive item: "Plugins, button" - active item: "Plugins, current page, button" Drop `ariaCurrentWhenActive="page"` from the buttons at the same time. Keeping it would risk a double announcement on screen readers that DO honor `aria-current` — VO would speak "current page" twice, once from the aria-current state and once from the aria-label suffix. Since we're explicitly putting the state in the label, removing the redundant attribute is the safer choice. i18n: add `menu.current_page: "current page"` to en.json. The 29 non-English locales get the English fallback for now — translators can localise in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 25, 2026
855dca0 to
b9a4928
Compare
Contributor
|
Closing as commits brought into the beta branch manually. thanks for your efforts as always! |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Seven focused screen-reader fixes for the beta-5.23.1 branch. Each commit is
self-contained and individually reviewable — squash on merge if preferred.
Visible UI should be unchanged in all commits — sighted users see no difference.
Commits
fix(layout): use buttons for sidebar items and trim duplicate menu announcementsSidebar items become
<button>instead ofrole="menuitem"divs (menuitem is only valid inside arole="menu"container, which we don't have). The decorative.sidebar .headeris no longer a second toggle button. The.sidebaris only a navigation landmark on desktop — on mobile the m-header button already announces the drawer state, so a second landmark just stutters. The m-header itself isaria-hiddenon desktop as a belt-and-suspenders for the existing CSSdisplay: none. Small SCSS reset on.link-rowso the button renders identical to the previous div.fix(status): announce bridge rows as one button with status + matter + restartEach row in the Bridges widget is now a single
<button>instead of a<div>+ nested icon-only restart button. The button's aria-label composes"<name>, <Running|Not Running|Restarting>[, Matter Running|Not Running|Not Enabled][, Restart]"so screen readers read the row as one self-contained control. Inner icons keep their tooltips for sighted users; they're nowaria-hiddenbecause the outer button carries the full label. Also fixes the broken<! -- comment - ->syntax in this template (renders as visible text), the main-bridge Matter icon's aria-label usinghap_*translation keys, and restoresstatus.services.label_running/label_not_runningin en.json (the restart-complete live-region message was reading the raw key).fix(status): announce matter state on all rows when matter support is enabledThe previous version only added a matter clause to the aria-label when matter was enabled for that specific bridge — but the visible matter icon renders on every row whenever the
matterSupportserver feature is on, with tooltips cycling through Not Enabled / Not Running / Running. The aria-label now mirrors that.fix(status): drop duplicate text and widget-name announcements in show/hide widgets modalEach row in the Show/Hide Widgets modal produced 5 announcements (widget name × 3, "Show on desktop" × 2). Hide the visible platform labels from the AX tree (they're for sighted users) and trim the widget name out of each checkbox's aria-label so it isn't re-announced right after the heading. Result is 3 announcements: name + 2 checkbox states.
fix(a11y): hide xterm helper textarea from screen readers on read-only terminalsxterm.js always renders a hidden
<textarea class="xterm-helper-textarea">to capture keyboard input, even withdisableStdin: true. Screen readers find it as a focusable "Terminal input" form field — wrong on the logs page, homebridge-logs widget, plugin install progress, and backup-restore progress modal. Adds a shared helperhideXtermInputFromScreenReader()that aria-hides the textarea afterterm.open()and re-applies via a MutationObserver (xterm rebuilds the subtree as it renders). Interactive terminals (/terminal, terminal widget) are unaffected.fix(plugins): drop redundant plugin name from info button aria-labelThe plugin info button announced "Plugin info for
homebridge-config-ui-x" — spoken right after the card heading (which is the plugin name) and the visible npm-name inside the button. The plugin name was being announced three times per card. Aria-label simplified to "Plugin info". All 29 non-English locales also updated since they held the literal English string (no real translation had been done yet).fix(layout): announce active sidebar item as "current page"The sidebar buttons set
aria-current="page"via Angular Router, but VoiceOver doesn't reliably announce it on<button>elements — so a screen-reader user lands on a page and tabs through the sidebar with no audible cue that one of the items is where they are. Bake ", current page" into the active item's aria-label conditionally. Addsmenu.current_pagetranslation key (English fallback in all locales).Notes
The broken HTML comment regression from
735c54cf9 chore(format): apply prettier formatting to a11y-touched templates(<! -- ... - ->rendering as visible text in ~70 templates) is flagged but not fixed here — out of scope for an a11y PR.🤖 Generated with help from Claude Code