Skip to content

🐛 Improved toggle card accessibility for screen readers - #30002

Open
MHJahanbakhsh wants to merge 1 commit into
TryGhost:mainfrom
MHJahanbakhsh:fix/toggle-card-a11y-27462
Open

🐛 Improved toggle card accessibility for screen readers#30002
MHJahanbakhsh wants to merge 1 commit into
TryGhost:mainfrom
MHJahanbakhsh:fix/toggle-card-a11y-27462

Conversation

@MHJahanbakhsh

Copy link
Copy Markdown

Summary

  • Fixes expand/collapse a11y for published kg-toggle-card content: aria-expanded, aria-controls, and a clear accessible name on the toggle button
  • Hides collapsed content from assistive tech with hidden / aria-hidden / inert, synced by frontend JS with data-kg-toggle-state
  • Updates both card renderers (kg-default-cards and kg-default-nodes) and unit tests for consistent markup

Fixes #27462

Test plan

  • Publish a post with one or more Toggle cards; confirm collapsed body is not read by VoiceOver/NVDA until expanded
  • Confirm the toggle control announces expanded/collapsed state and can be activated with keyboard (Enter/Space on the button)
  • Confirm clicking the heading still expands/collapses
  • Confirm email render still shows heading + content (always expanded)
  • Run pnpm exec vitest run test/cards/toggle.test.ts in koenig/kg-default-cards
  • Run pnpm exec vitest run test/renderers/toggle-renderer.test.ts test/nodes/toggle.test.ts in koenig/kg-default-nodes

fixes TryGhost#27462

Collapsed toggle content is no longer exposed to assistive tech, and the
expand control exposes state via aria-expanded while keeping heading click
and button keyboard activation.
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: c2d17bd2-1995-4154-bdd5-0563e8950df3

📥 Commits

Reviewing files that changed from the base of the PR and between b45ecaa and 40b6696.

📒 Files selected for processing (8)
  • .changeset/toggle-card-a11y.md
  • ghost/core/core/frontend/src/cards/css/toggle.css
  • ghost/core/core/frontend/src/cards/js/toggle.js
  • koenig/kg-default-cards/src/cards/toggle.ts
  • koenig/kg-default-cards/test/cards/toggle.test.ts
  • koenig/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • koenig/kg-default-nodes/test/nodes/toggle.test.ts
  • koenig/kg-default-nodes/test/renderers/toggle-renderer.test.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.

📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: 1. Never split sentences across multiple t() calls. Translators cannot reorder words across separate keys. Instead, use @doist/react-interpolate to embed React elements (links, bold, etc.) within a single translatable string.
3. Use interpolation for dynamic values. Ghost uses {variable} syntax: t('Welcome back, {name}!', {name: firstname})

Files:

  • koenig/kg-default-cards/src/cards/toggle.ts
  • koenig/kg-default-nodes/test/renderers/toggle-renderer.test.ts
  • koenig/kg-default-cards/test/cards/toggle.test.ts
  • koenig/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • koenig/kg-default-nodes/test/nodes/toggle.test.ts
  • ghost/core/core/frontend/src/cards/js/toggle.js
🧠 Learnings (6)
📚 Learning: 2026-06-04T15:15:20.265Z
Learnt from: JohnONolan
Repo: TryGhost/Ghost PR: 28368
File: apps/admin-x-settings/src/components/settings/site/navigation/navigation-edit-form.tsx:32-32
Timestamp: 2026-06-04T15:15:20.265Z
Learning: In this TryGhost/Ghost codebase (Tailwind CSS v4), use/accept the v4 suffix form of the important modifier in class names (e.g., `opacity-100!`, `flex!`). Do not flag these as incorrect or inconsistent with the older v3 prefix form (`!opacity-100`), since the suffix form is the established convention and aligns with the generated CSS.

Applied to files:

  • koenig/kg-default-cards/src/cards/toggle.ts
  • koenig/kg-default-nodes/test/renderers/toggle-renderer.test.ts
  • koenig/kg-default-cards/test/cards/toggle.test.ts
  • koenig/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts
  • koenig/kg-default-nodes/test/nodes/toggle.test.ts
  • ghost/core/core/frontend/src/cards/js/toggle.js
📚 Learning: 2026-08-03T21:09:05.797Z
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 29723
File: ghost/core/test/unit/server/services/automations/automations-repository.test.ts:2117-2117
Timestamp: 2026-08-03T21:09:05.797Z
Learning: In TypeScript test files, treat each `it(...)` or `test(...)` callback as a separate function scope. Identically named local declarations, such as `queries` or `recordQuery`, in separate test callbacks are valid and should not be reported as duplicate block-scoped declarations.

Applied to files:

  • koenig/kg-default-nodes/test/renderers/toggle-renderer.test.ts
  • koenig/kg-default-cards/test/cards/toggle.test.ts
  • koenig/kg-default-nodes/test/nodes/toggle.test.ts
📚 Learning: 2026-01-08T10:26:38.700Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25791
File: ghost/core/core/server/api/endpoints/member-comment-ban.js:64-68
Timestamp: 2026-01-08T10:26:38.700Z
Learning: In the Ghost API, endpoints rely on the serialization layer to prepare frame.data[docName] as a non-empty array before query() executes. Endpoints access frame.data[docName][0] directly (e.g., frame.data.comment_bans[0], frame.data.members[0], frame.data.posts[0]) without per-endpoint validation. This pattern is common across API endpoints. When maintaining or creating endpoints, avoid duplicating validation for frame.data[docName] and ensure the serializer guarantees the shape and non-emptiness. If you add a new endpoint that uses this frame.data[docName], follow the same assumption and avoid redundant checks unless there's a documented exception.

Applied to files:

  • ghost/core/core/frontend/src/cards/js/toggle.js
📚 Learning: 2026-02-04T15:58:09.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 26219
File: ghost/core/test/e2e-api/members-comments/comments.test.js:939-983
Timestamp: 2026-02-04T15:58:09.124Z
Learning: In Ghost core tests and code that interact with the Ghost comments API, count.replies is a backward-compatible alias for count.total_replies (all descendants via parent_id) and does not represent direct replies. The new field count.direct_replies returns tree-native direct reply counts. Reviewers should verify any code paths, tests, or API surface areas that rely on count.replies are preserved for compatibility, and consider updating or adding tests to cover count.direct_replies for direct counts. When updating or adding tests, ensure behavior is documented and that any assertions reflect the distinction between total (including descendants) and direct reply counts to avoid regressions in API consumer expectations.

Applied to files:

  • ghost/core/core/frontend/src/cards/js/toggle.js
📚 Learning: 2026-04-09T09:44:26.783Z
Learnt from: vershwal
Repo: TryGhost/Ghost PR: 27290
File: ghost/core/package.json:76-77
Timestamp: 2026-04-09T09:44:26.783Z
Learning: In the TryGhost/Ghost monorepo, treat `tryghost/admin-api-schema` as the single abstraction layer over AJV version differences. Do not raise code review findings for AJV-internal error field changes (e.g., `dataPath` → `instancePath` between AJV v6 and v8) when evaluating Ghost consumer code. The consumer-facing error contract for this package (`ValidationError` with `message`, `property`, `errorDetails`) is expected to remain stable, and Ghost wrapper code should not inspect raw AJV error objects—so review should focus on the stable `ValidationError` shape rather than AJV internals.

Applied to files:

  • ghost/core/core/frontend/src/cards/js/toggle.js
📚 Learning: 2026-06-22T14:36:35.803Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 28779
File: ghost/core/core/frontend/web/middleware/error-handler.js:0-0
Timestamp: 2026-06-22T14:36:35.803Z
Learning: When using Express.js view engines, Express stores engine handler functions in `app.engines` with keys that include a leading dot (e.g., `app.engines['.hbs']` and `app.engines['.ejs']`). Therefore, checking `app.engines.hbs` (no dot) will be `undefined`; to test whether an engine is already registered, use bracket notation with the dot prefix: `app.engines['.hbs'] !== undefined` (or equivalently `Object.prototype.hasOwnProperty.call(app.engines, '.hbs')`).

Applied to files:

  • ghost/core/core/frontend/src/cards/js/toggle.js
🪛 ast-grep (0.45.1)
ghost/core/core/frontend/src/cards/js/toggle.js

[warning] 43-43: Avoid using the initial state variable in setState
Context: setExpandedState(parentElement, isClosed)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[warning] 81-81: Avoid using the initial state variable in setState
Context: setExpandedState(card, isOpen)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[error] 43-43: React's useState should not be directly called
Context: setExpandedState(parentElement, isClosed)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[error] 81-81: React's useState should not be directly called
Context: setExpandedState(card, isOpen)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🔇 Additional comments (8)
.changeset/toggle-card-a11y.md (1)

1-6: LGTM!

koenig/kg-default-cards/src/cards/toggle.ts (1)

22-26: LGTM!

koenig/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts (1)

19-25: LGTM!

koenig/kg-default-cards/test/cards/toggle.test.ts (1)

16-16: LGTM!

koenig/kg-default-nodes/test/nodes/toggle.test.ts (1)

177-183: LGTM!

Also applies to: 224-224

koenig/kg-default-nodes/test/renderers/toggle-renderer.test.ts (1)

33-43: LGTM!

ghost/core/core/frontend/src/cards/js/toggle.js (1)

1-33: LGTM!

Also applies to: 35-45, 47-82, 84-87

ghost/core/core/frontend/src/cards/css/toggle.css (1)

13-13: LGTM!

Also applies to: 79-82


Walkthrough

Toggle cards now use semantic buttons with explicit expansion state and accessible labels. Collapsed content starts with hidden, aria-hidden, and inert. Frontend JavaScript synchronizes card, button, icon, and content state during initialization and interaction. Keyboard focus styling was added. Render fixtures and snapshots were updated. A changeset records patch releases for the affected packages.

Possibly related PRs

  • TryGhost/Ghost#29550: Implements related toggle-card accessibility changes across frontend behavior, renderers, markup, and tests.
  • TryGhost/Ghost#29597: Modifies the same toggle-card accessibility areas while using native <details>/<summary> markup.

Suggested reviewers: kevinansfield

Merge Risk: ⚪ Minimal · up to 40b66

This localized accessibility update is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: improved screen-reader accessibility for toggle cards.
Description check ✅ Passed The description accurately covers the accessibility changes, affected renderers, tests, and linked issue.
Linked Issues check ✅ Passed The changes address issue #27462 by adding accessible control semantics and hiding collapsed content from assistive technologies.
Out of Scope Changes check ✅ Passed The CSS, renderer, frontend, changeset, and test updates directly support the toggle card accessibility objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

a11y: kg-toggle-card not usable properly with a screenreader

1 participant