Skip to content

Bulk editor: add in-app tour for first-time users - #23509

Open
JorPV wants to merge 21 commits into
trunkfrom
1348-bulk-editor-in-app-tour
Open

Bulk editor: add in-app tour for first-time users#23509
JorPV wants to merge 21 commits into
trunkfrom
1348-bulk-editor-in-app-tour

Conversation

@JorPV

@JorPV JorPV commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Context

  • The bulk editor has new functionalities, so first-time users need orientation. This adds a first-run in-app guided tour that walks a new user through the page's key areas, plus a one-time entry notification on the General page that points them to it.

Figma design flow.

Summary

This PR can be summarized in the following changelog entry:

  • Adds a first-time guided tour to the bulk editor.

Relevant technical choices:

  • Persistence: reuses the shared opt-in REST route + shared-admin opt-in store slice; one user-meta flag (bulk_editor_tour) marks the tour seen on finish or dismiss so it never returns.
  • Spotlight: a viewport-filling SVG dim with masked cut-outs — single-region steps clamp to a boundary element (highlightEndSelector); per-control steps cut one exact-size hole per matching child (highlightChildren, with an optional childSelector like button on the generate step, so slot-filled siblings such as Premium's AI usage counter stay dimmed).
  • Re-measuring: a ResizeObserver, a MutationObserver on the target, and a short capped settle loop re-measure the cut-outs (deduped by geometry) so they follow targets that reflow after the tour opens — async slot fills, fonts, the RTL scrollbar.
  • Modal behaviour: a click-blocking backdrop plus an in-card focus trap and Escape-to-close, because the ui-library Popover provides none despite aria-modal="true".
  • AI gating: the final "generate" step is dropped when the AI feature is off, so the tour is 3 steps instead of 4.
  • RTL: the popover flips to the left via ui-library's logical positioning; the arrow is drawn with logical borders (physical ones get re-mirrored by the RTL stylesheet build), and the spotlight rectangles are re-based onto the fixed overlay — whose origin the left-side RTL scrollbar shifts — so every cut-out and the card still land on their controls.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Prerequisites

  • Yoast SEO Free active.

How to (re)set the tour so it shows again
The tour auto-shows once per user and then never returns — a user-meta flag (_yoast_wpseo_bulk_editor_tour_opt_in_notification_seen) records that it has been seen. To make it show again:

  1. Reset the flag with WP-CLI (<USER_ID> is your user id — find it via wp user list):
    wp user meta update <USER_ID> _yoast_wpseo_bulk_editor_tour_opt_in_notification_seen 0
    In the Docker dev environment: docker exec <wp-container> wp user meta update 1 _yoast_wpseo_bulk_editor_tour_opt_in_notification_seen 0 --allow-root
    (Deleting the meta works too: wp user meta delete <USER_ID> _yoast_wpseo_bulk_editor_tour_opt_in_notification_seen.)
  2. Reload the bulk editor page — the tour appears again.
  3. The same flag also gates the one-time entry notification on the Yoast SEO General page; resetting it brings that back as well.

Main flow — entry notification → 4 steps (AI enabled)

  1. Make sure Yoast AI is on: Yoast SEO → Settings → turn on the AI/AI-generator site feature (enable_ai_generator).
  2. Reset the tour (above), then open the Yoast SEO → General page.
  3. A one-time entry notification "New: Work faster with bulk updates" appears (bottom corner). Click Show me — you are redirected to the Bulk editor page and the tour auto-starts.
  • Confirm it matches the design.
  • Confirm the focus (keyboard tab) is trapped within the notification until dissmissed or clicked the Show me button (Clicking Dismiss instead marks the tour seen and skips it entirely — neither the notification nor the tour returns until you reset the flag.)
image
  • Click on the Show me button.
  1. Confirm the tour runs with 4 steps, each with the highlight hugging only the relevant control(s):
    1. Select your content type — highlights Posts + Pages in the sidebar (not Products / custom types). Design.
    2. Select search or social appearance — highlights the two appearance tabs. Design.
    3. Multi-select — highlights the select-all checkbox and the Select button as two separate areas (the gap between them stays dimmed). Design.
    4. Get SEO-friendly options at scale — highlights the two Generate buttons as two separate areas. Design.
  2. Click Next through the steps; the last step's button reads Got it!. Click it — the tour closes.
  3. Console must stay clean on "Got it!" — no React warnings, in particular no "Cannot update a component while rendering a different component".
  4. Reload the Bulk editor — the tour does not reappear; return to General and the entry notification is gone too.

While the tour is open, the rest of the page is not interactive. Everything behind the dim is click-blocked — clicking a tab, the Select menu, a row, the sidebar, or any admin control does nothing, and only the tour card (Back / Next / Got it, the close button, and Escape) responds. This is intentional (the tour is modal), so a user cannot drift off the guided flow.

Scenario — Yoast AI disabled (tour is 3 steps) (see 🧵 thread ).

  1. Turn off the AI/AI-generator site feature (Yoast SEO → Settings). The Generate buttons disappear from the bulk editor toolbar.
  2. Reset the tour, open Yoast SEO → General, and click Show me on the "New: Work faster with bulk updates" notification to be redirected to the Bulk editor.
  3. Confirm the tour now runs with only 3 steps (content type → appearance → multi-select). The final step is Multi-select and its button reads Got it!. There is no generate step, because its target does not exist without AI.
  4. Finish the tour: no console errors, and rows are not auto-selected (auto-selection only happens for the generate step).

Scenario — RTL

  1. Reset the tour and open the bulk editor in RTL. In the dev environment append &d=rtl to the URL (admin.php?page=wpseo_page_bulk_edit&d=rtl); otherwise set an RTL site language. The layout mirrors — the sidebar and tabs move to the right.
  2. Walk all steps and confirm each spotlight still hugs its control(s) exactly (Posts + Pages, the tabs, the checkbox + Select, the two Generate buttons), and the popover appears on the left of the highlight with the arrow pointing at it across a small gap.
  3. Also test a real RTL site language, not only &d=rtl. A right-to-left locale moves the document scrollbar to the left, which used to shift every cut-out ~15px to the left of its control; confirm the cut-outs now land exactly on their controls in that setup too.
  4. Pay particular attention to the final "Get SEO-friendly options at scale" step — its two cut-outs must sit exactly on the Generate buttons even as the toolbar settles. In Premium the AI usage counter beside the buttons must stay dimmed (only the two buttons are spotlighted).

Accessibility checks

  1. Focus trap — with the tour open, press Tab repeatedly: focus must cycle only through the card's controls (Close, Back, Next/Got it) and never reach the dimmed page behind. Shift+Tab cycles backwards within the card.
  2. Initial focus — on each step, focus lands on the primary action (Next / Got it) automatically.
  3. Escape — pressing Escape closes the tour (and marks it seen).
  4. Page is inert to the pointer — clicking anything behind the dim (a tab, the Select button, a table row) does nothing; only the card responds.
  5. Screen reader — the card is announced as a dialog with its title and description; the Yoast logo icon is decorative (aria-hidden). (Full background-inertness of aria-modal is a ui-library follow-up: Yoast/reserved-tasks#1373.)

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:

  • Same steps as above. Note the bulk editor is still in development; test wherever it is available, and use the flag-reset step to re-run the tour.

Impact check

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

  • The bulk editor page (React app + its enqueue/localize integration).
  • The Yoast SEO General page (the one-time entry notification).
  • The shared opt-in REST route allowlist and the shared-admin opt-in store slice (a new bulk_editor_tour key).

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 committed the results, if my PR introduces or edits 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 Yoast/reserved-tasks#1348

@JorPV JorPV added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Jul 27, 2026
Removes the AI Brand Insights modals/classes/tests and image that #23439 deleted
from trunk, plus stray wordpress-urls (class-wpseo-utils) and yarn.lock edits, so
this PR only contains the guided-tour work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coveralls-official

coveralls-official Bot commented Jul 27, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 71

Coverage increased (+0.2%) to 55.582%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 26 uncovered changes across 5 files (199 of 225 lines covered, 88.44%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
packages/js/src/bulk-editor/components/tour/tour-card.js 23 13 56.52%
packages/js/src/bulk-editor/components/tour/use-tour-anchor.js 105 97 92.38%
packages/js/src/general/components/bulk-editor-tour-notification.js 29 26 89.66%
src/bulk-editor/user-interface/bulk-editor-integration.php 14 11 78.57%
src/general/user-interface/general-page-integration.php 5 3 60.0%
Total (11 files) 225 199 88.44%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
src/general/user-interface/general-page-integration.php 1 96.39%

Coverage Stats

Coverage Status
Relevant Lines: 72728
Covered Lines: 40282
Line Coverage: 55.39%
Relevant Branches: 18889
Covered Branches: 10641
Branch Coverage: 56.33%
Branches in Coverage %: Yes
Coverage Strength: 41381.71 hits per line

💛 - Coveralls

@JorPV
JorPV requested a review from Copilot July 27, 2026 13:34
@JorPV JorPV added changelog: other Needs to be included in the 'Other' category in the changelog and removed changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog labels Jul 27, 2026
@JorPV JorPV added the UX label Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a first-run guided tour to the Bulk Editor and a one-time entry notification on the General page, using the existing opt-in REST route + shared opt-in store slice to persist a per-user “seen” flag.

Changes:

  • Adds a multi-step, spotlight-style in-app tour to the Bulk Editor (including AI-disabled step gating).
  • Replaces the old task-list opt-in notification with a Bulk Editor tour entry notification on the General page.
  • Extends server + client script data and tests to support the new bulk_editor_tour opt-in key.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/WP/General/User_Interface/Set_Opt_In_Seen_Test.php Updates WP REST opt-in “seen” tests to use bulk_editor_tour + new meta key.
tests/Unit/General/User_Interface/Opt_In_Route/Validate_Key_Test.php Updates key validation tests and asserts task_list is no longer accepted.
tests/Unit/General/User_Interface/General_Page_Integration_Test.php Adjusts General page integration tests for the new opt-in meta key and constructor wiring.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Enqueue_Assets_Test.php Verifies bulk editor script localization now includes opt-in “seen” state.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Constructor_Test.php Updates constructor expectations for the new User_Helper dependency.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Abstract_Bulk_Editor_Integration_Test.php Adds User_Helper mock plumbing for bulk editor integration unit tests.
src/general/user-interface/opt-in-route.php Replaces allowlisted opt-in key with bulk_editor_tour.
src/general/user-interface/general-page-integration.php Exposes bulk_editor_tour seen state in General page localized script data.
src/bulk-editor/user-interface/bulk-editor-integration.php Adds user-meta based opt-in “seen” state to bulk editor localized script data.
packages/js/tests/general/components/bulk-editor-tour-notification.test.js Adds unit tests for the General page entry notification behavior.
packages/js/tests/bulk-editor/tour/use-tour-anchor.test.js Adds unit tests for spotlight geometry/behavior in the tour anchor hook.
packages/js/tests/bulk-editor/tour/tour-steps.test.js Tests tour step ordering/shape.
packages/js/tests/bulk-editor/tour/tour-card.test.js Tests tour card navigation, close, and Escape behavior.
packages/js/tests/bulk-editor/tour/bulk-editor-tour.test.js Tests tour flow, AI-disabled step removal, and persistence dispatch.
packages/js/tests/bulk-editor/initialize.test.js Updates bulk editor store initialization to include opt-in seen state.
packages/js/src/shared-admin/store/opt-in.js Adjusts constants import path for the shared opt-in store slice.
packages/js/src/shared-admin/store/index.js Re-exports the shared opt-in store slice.
packages/js/src/general/store/index.js Switches General page store to use the shared opt-in slice exports.
packages/js/src/general/initialize.js Initializes General page opt-in state from localized script data as an object map.
packages/js/src/general/components/task-list-opt-in-notification.js Removes the old task list opt-in notification component.
packages/js/src/general/components/opt-in-container.js Removes the old opt-in container used for task list notification gating.
packages/js/src/general/components/index.js Exposes the new bulk editor tour notification component instead of the removed ones.
packages/js/src/general/components/bulk-editor-tour-notification.js Adds the General page entry notification pointing users to the Bulk Editor tour.
packages/js/src/general/app.js Mounts the new General page entry notification component.
packages/js/src/bulk-editor/store/index.js Adds the shared opt-in slice to the bulk editor store (actions/selectors/reducer/controls).
packages/js/src/bulk-editor/initialize.js Initializes bulk editor opt-in state from localized script data.
packages/js/src/bulk-editor/constants.js Adds the TOUR_OPT_IN_KEY constant.
packages/js/src/bulk-editor/components/tour/use-tour-anchor.js Adds target tracking + spotlight geometry hook for the guided tour.
packages/js/src/bulk-editor/components/tour/tour-steps.js Adds the bulk editor tour step definitions (incl. selection-required step).
packages/js/src/bulk-editor/components/tour/tour-card.js Adds the tour UI card with focus management and keyboard handling.
packages/js/src/bulk-editor/components/tour/bulk-editor-tour.js Adds the full tour controller (step flow, AI gating, selection handling, persistence).
packages/js/src/bulk-editor/components/bulk-editor-tabs.js Adds data-tour-id anchor for the appearance tabs step.
packages/js/src/bulk-editor/components/bulk-editor-nav.js Adds data-tour-id anchor and highlight boundary for the content type navigation step.
packages/js/src/bulk-editor/components/bulk-editor-content.js Mounts the tour component alongside the bulk editor content.
packages/js/src/bulk-editor/components/bulk-action-bar.js Adds data-tour-id anchors for selection toolbar and generate actions steps.
css/src/bulk-editor-page.css Adds styling to support the tour overlay layering/pointer interaction behavior.

Comment thread src/general/user-interface/general-page-integration.php
Comment thread packages/js/src/bulk-editor/components/tour/use-tour-anchor.js Outdated
Comment thread css/src/bulk-editor-page.css Outdated
Comment thread css/src/bulk-editor-page.css Outdated
JorPV and others added 2 commits July 27, 2026 15:47
- Lower YOASTCS_THRESHOLD_ERRORS to 2385 to match the branch (the specific
  get_script_data() return type removes one MissingTraversableTypeHint error).
- Fix comment typos (exists, overlaid) and reword the spotlight CSS comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… padding

The appearance-tabs step looked too loose (8px sides + 24px right). Use a small
uniform padding so the spotlight hugs the target and the popover sits closer,
matching the design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JorPV
JorPV force-pushed the 1348-bulk-editor-in-app-tour branch from d867580 to dca3457 Compare July 27, 2026 14:21
@JorPV
JorPV requested a review from Copilot July 27, 2026 14:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/general/user-interface/general-page-integration.php:253

  • The get_script_data() PHPDoc return type is currently too narrow and doesn’t match the actual data shape (e.g. alerts is a numerically indexed list of arrays, and other entries can contain non-string scalars). This can mislead static analysis and makes the annotation effectively incorrect.

Consider widening the value type while still keeping iterable generics (to satisfy YoastCS) by using scalar plus a generic nested-array union.

	 * @return array<string, string|array<string, string|bool|array<string, string|bool>>> The script data.

Comment thread src/bulk-editor/user-interface/bulk-editor-integration.php
@JorPV
JorPV requested a review from Copilot July 28, 2026 11:25
@JorPV JorPV added the innovation Innovative issue. Relating to performance, memory or data-flow. label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/general/user-interface/general-page-integration.php:254

  • The PHPDoc return type for get_script_data() doesn’t match the actual data shape: e.g. dismissedAlerts can be false (Alert_Dismissal_Action::all_dismissed()), and multiple fields (alerts, currentPromotions) are list arrays with integer keys. This type is likely to mislead static analysis and future callers; please broaden it to reflect scalar values, nested arrays with int|string keys, and false.
	/**
	 * Creates the script data.
	 *
	 * @return array<string, string|array<string, string|bool|array<string, string|bool>>> The script data.
	 */

src/bulk-editor/user-interface/bulk-editor-integration.php:222

  • The PHPDoc return type for Bulk_Editor_Integration::get_script_data() is inaccurate now that it includes list arrays like contentTypes (array<int, array<string,string>>). The current type implies nested arrays always have string keys, which isn’t true for contentTypes (and likely endpoints). Consider using an explicit array shape here, consistent with other integrations, so the contract matches what is localized to JS.
	/**
	 * Creates the script data.
	 *
	 * @return array<string, string|array<string, string|bool|array<string, string>>> The script data.
	 */
	public function get_script_data() {

@JorPV JorPV removed the UX label Jul 28, 2026
Comment thread packages/js/src/general/components/bulk-editor-tour-notification.js Outdated
Comment thread packages/js/src/bulk-editor/components/bulk-editor-nav.js Outdated
Comment thread packages/js/src/bulk-editor/components/tour/bulk-editor-tour.js
Comment thread packages/js/tests/bulk-editor/tour/use-tour-anchor.test.js

@vraja-pro vraja-pro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CR 🚧
I left a few comments in the code.
Please consider cleaning up the opt-in code in #22425:

  • The css in css/src/new-settings.css
  • packages/js/src/settings/routes/llms-txt.js
  • packages/js/src/settings/components/llm-txt-popover.js

@github-actions

Copy link
Copy Markdown

A merge conflict has been detected for the proposed code changes in this PR. Please resolve the conflict by either rebasing the PR or merging in changes from the base branch.

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

Labels

changelog: other Needs to be included in the 'Other' category in the changelog innovation Innovative issue. Relating to performance, memory or data-flow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants