Skip to content

1195 content planner improve inline banner accessibility#23231

Open
vraja-pro wants to merge 15 commits intorelease/27.6from
1195-content-planner-improve-inline-banner-accessibility
Open

1195 content planner improve inline banner accessibility#23231
vraja-pro wants to merge 15 commits intorelease/27.6from
1195-content-planner-improve-inline-banner-accessibility

Conversation

@vraja-pro
Copy link
Copy Markdown
Contributor

@vraja-pro vraja-pro commented May 5, 2026

Context

The inline content planner banner was made non-block in PR #23221 (it no longer lives in Gutenberg's block data model). As a side-effect, its buttons became unreachable via keyboard Tab and Shift+Tab navigation and invisible to screen readers.

The root cause is Gutenberg's useTabNav hook, which intercepts all Tab events inside the writing flow in the bubble phase. It only allows natural tabbing when the next tabbable element is inside the same [data-block] wrapper as the current focus. Because the banner sits outside any real block, Tab was always redirected to the sentinel divs — skipping the banner entirely.

Summary

This PR can be summarized in the following changelog entry:

  • Fixes an unreleased bug where the content planner inline banner's buttons could not be reached via keyboard Tab and Shift+Tab navigation and were not announced by screen readers, which was caused by the banner sitting outside Gutenberg's block wrappers.
  • Adds learn more link to the conten planner inline banner and refactor the learn more link in the content planner approve modal.

Relevant technical choices:

  • Added data-block="yoast-content-planner-banner" to the banner wrapper div. This satisfies Gutenberg's target.closest('[data-block]') check for intra-banner Tab: isInSameBlock compares .block-editor-block-list__block ancestors, both of which are null for elements inside the banner (since it is not inside a real block) — null === null evaluates to true, so Gutenberg allows Tab between the banner's own buttons without intervention.
  • Added a capture-phase keydown listener on the editor's ownerDocument to handle Tab and Shift+Tab crossing the banner boundary (entering from a preceding block, leaving from the last button, entering from a following block via Shift+Tab, or leaving from the first button via Shift+Tab). Capture fires before Gutenberg's bubble-phase handler; calling preventDefault() there triggers Gutenberg's early-return guard (if (event.defaultPrevented) return) and leaves focus management to our handler. Intra-banner navigation is intentionally not intercepted — it is already handled correctly by Gutenberg via the data-block attribute above.
  • Navigation logic extracted to handle-banner-tab-navigation.js so it can be unit-tested independently of the component. The helper picks findPrevious or findNext based on shiftKey and applies the same boundary-crossing XOR check for both directions.
  • Added role="group" and aria-label to the InlineBanner container so screen readers announce the banner as a distinct region when navigating with the virtual cursor.
  • Added @wordpress/dom as an explicit direct dependency in packages/js/package.json (previously only available transitively via @wordpress/block-editor).
  • While adding the learn more link I refactor the learn more link in the approve modal to support styling consistency with Link component from ui library and avoiding the placeholder in translation, and by that we use out already translated string.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

  1. Open a new post in the Gutenberg block editor with the content planner banner visible (new post, minimum posts threshold met, banner not dismissed).
  2. Tab into the editor from the toolbar above and verify focus reaches the "×" (dismiss) button on the banner.
  3. Press Tab again — verify focus moves to the "learn more link" button.
  4. Press Tab again — verify focus moves to the "Get content suggestions" button.
  5. Press Tab again — verify focus moves to the first content block, not back to the toolbar.
  6. From the first content block, press Shift+Tab — verify focus moves back to the "Get content suggestions" button.
  7. Press Shift+Tab again — verify focus moves to the "×" button.
  8. Press Shift+Tab again — verify focus leaves the banner upward (toward the title or toolbar).
  9. Activate the dismiss button via Enter — verify the banner is dismissed.
  10. Reload and repeat with a screen reader (e.g. VoiceOver or NVDA) — verify the banner is announced as "Content suggestions banner, group" when the virtual cursor enters it.
  11. Click on the yoast sidebar and click on the "Get content suggestion" button, check the modal has the learn more link centered at the bottom of the modal with the same link.

RTL:

  1. Switch to RTL language.
  2. Crete a new post, check the arrow in the learn more link is directed to the left.
  3. Repeat the test for the learn more link in the approve modal by clicking on the get content suggestions button in the editor or yoast sidebar.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA can test this PR by following these steps:

  1. Open a new post in the Gutenberg block editor (banner must be visible).
  2. Tab into the editor and verify the banner buttons ("×" and "Get content suggestions") are reachable via Tab in order.
  3. Verify Tab from the last banner button moves focus into the first content block.
  4. From the first content block, Shift+Tab back through the banner buttons and verify focus leaves the banner upward.
  5. Optionally verify with a screen reader that the banner is announced as a group with its label.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • Content planner inline banner (keyboard navigation and screen reader accessibility).
  • No changes to block data model, store, or persistence logic.

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and commited the results, if my PR introduces new images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes https://github.com/Yoast/reserved-tasks/issues/1195

vraja-pro added 3 commits May 5, 2026 12:12
…lity

  Adds role="group" and aria-label to the InlineBanner container so screen readers announce the banner as a distinct region when navigating with the virtual cursor.
Gutenberg's useTabNav hook runs in the bubble phase and redirects Tab to sentinel divs when the next tabbable is not inside a [data-block] wrapper.
The banner sits outside any real block, so it was always skipped.

  Two changes fix this:
  - Add data-block="yoast-content-planner-banner" to the wrapper div so that intra-banner Tab works via Gutenberg's existing null === null path in isInSameBlock.
  - Attach a capture-phase keydown listener on the editor document that fires before Gutenberg's bubble handler. When Tab crosses the banner boundary
    (entering or leaving), it calls preventDefault() and manually moves focus; Gutenberg's early-return guard then fires and leaves focus alone.

The navigation logic is extracted to a standalone helper so it can be unit tested independently. @wordpress/dom is added as an explicit direct dependency since it is now imported directly.
Covers all branches: early returns (defaultPrevented, non-Tab key, Shift+Tab, null banner, no next tabbable), Tab into banner, Tab out of  banner, intra-banner Tab, and Tab between two elements both outside the                                                                              banner.
@vraja-pro vraja-pro added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label May 5, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented May 5, 2026

Coverage Report for CI Build 0

Coverage increased (+0.03%) to 56.671%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 4 uncovered changes across 2 files (18 of 22 lines covered, 81.82%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
packages/js/src/ai-content-planner/components/inline-banner.js 2 0 0.0%
packages/js/src/ai-content-planner/components/with-inline-banner.js 10 8 80.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 26941
Covered Lines: 15664
Line Coverage: 58.14%
Relevant Branches: 16995
Covered Branches: 9235
Branch Coverage: 54.34%
Branches in Coverage %: Yes
Coverage Strength: 111650.25 hits per line

💛 - Coveralls

@vraja-pro vraja-pro added this to the 27.6 milestone May 5, 2026
@JorPV JorPV requested a review from Copilot May 5, 2026 10:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves the accessibility of the Content Planner inline banner in the Gutenberg editor by ensuring its controls are reachable via keyboard tab navigation and correctly announced by assistive technologies.

Changes:

  • Adds a capture-phase Tab handler (plus unit tests) to manage focus when tabbing across the inline banner boundary.
  • Marks the banner wrapper with a data-block attribute to satisfy Gutenberg’s writing-flow tabbing expectations.
  • Enhances screen reader semantics for the inline banner and adds @wordpress/dom as a direct dependency.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/js/tests/ai-content-planner/helpers/handle-banner-tab-navigation.test.js Adds unit tests for the new Tab/Shift+Tab boundary navigation helper.
packages/js/src/ai-content-planner/helpers/handle-banner-tab-navigation.js Introduces a focus-management helper for Tab navigation into/out of the banner.
packages/js/src/ai-content-planner/components/with-inline-banner.js Wires the capture-phase keydown listener and adds data-block to the banner wrapper.
packages/js/src/ai-content-planner/components/inline-banner.js Adds role="group" and an accessible label for screen readers.
packages/js/package.json Adds @wordpress/dom as an explicit dependency used by the new helper.

Comment thread packages/js/src/ai-content-planner/components/with-inline-banner.js Outdated
Comment thread packages/js/src/ai-content-planner/helpers/handle-banner-tab-navigation.js Outdated
Comment thread packages/js/src/ai-content-planner/helpers/handle-banner-tab-navigation.js Outdated
Comment thread packages/js/src/ai-content-planner/helpers/handle-banner-tab-navigation.js Outdated
Comment thread packages/js/src/ai-content-planner/helpers/handle-banner-tab-navigation.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants