Skip to content

Add Content planner outline modal with loading state#23099

Merged
leonidasmi merged 21 commits intofeature/content-plannerfrom
content-planner-outline-modal
Apr 3, 2026
Merged

Add Content planner outline modal with loading state#23099
leonidasmi merged 21 commits intofeature/content-plannerfrom
content-planner-outline-modal

Conversation

@JorPV
Copy link
Copy Markdown
Contributor

@JorPV JorPV commented Mar 26, 2026

Context

This PR adds the Content Outline Modal component for the AI Content Planner feature. The modal appears after selecting a suggestion from the content suggestions modal and displays the AI-generated outline (focus keyphrase, title, meta description, blog post structure) for the user to review before adding it to their post.

Summary

This PR can be summarized in the following changelog entry:

  • Adds content outline modal with loading state and suggested content for the AI Content Planner feature.

Relevant technical choices

  • Native HTML5 drag-and-drop for structure row reordering — no additional library needed. Uses useCallback for all handlers to comply with react/jsx-no-bind lint rule.
  • Keyboard reorder via Alt+ArrowUp/Down on structure rows for screen reader and keyboard-only users.
  • Stable keys for reorderable structure rows — uses withIds() helper to assign IDs at initialization time, avoiding index-based keys that break React reconciliation during reorder.
  • Sticky gradient fade inside Modal.Container.Content with pb-0 on the scroll container and pb-4 on the content div, matching the pattern from the task-list children modal.
  • Loading state uses real elements with skeleton values (not a full skeleton replacement), matching the Figma design where IntentCallout, labels, toggle, and footer remain visible.
  • useSimulatedLoading custom hook — manages loading → success transition on mount using useState + useEffect (100ms delay to start loading, 3s to complete). Temporary: to be replaced with real API loading state when the outline endpoint is available.
  • Shared intentBadge configuration — extracted to intent-badge.js and shared between content-suggestions-modal and content-outline-modal to eliminate duplication (addresses reviewer feedback).
  • Modal transitions — selective approach:
    • Approve → suggestions: cross-fade with 300ms opacity animation (managed via SuggestionsPanel component and cameFromApproveModal flag).
    • Suggestions → outline: instant swap (plain conditional render, no Transition wrapper) to avoid layout flash caused by different panel sizes.
    • Outline → suggestions (back): instant swap (same plain conditional).
    • The SuggestionsPanel component conditionally wraps ContentSuggestionsModal in a HeadlessUI Transition only when coming from the approve modal, and uses a plain conditional render otherwise. This avoids the unmount-frame flicker that Transition causes even with empty leave classes.
  • getSuggestionsEnterTransition helper — extracted to keep FeatureModal complexity within the ESLint max (6).
  • Editable form fields — Focus Keyphrase, Title, and Meta description are editable via TextField / TextareaField with local state, allowing users to adjust the suggested text before applying.
  • Meta description progress bar — shows character length relative to the 156-character max. Uses inline backgroundColor styles to match the Yoast snippet editor color palette ($color_good #7ad03a for 121–156, $color_ok #ee7c1b for too short/long) — these are Yoast-specific colors that don't map to standard Tailwind classes.
  • Accessibility improvements:
    • Modal.Title (HeadlessUI Dialog.Title) for proper aria-labelledby association
    • Modal.Description for aria-describedby on instruction text
    • Modal.CloseButton with ref for programmatic focus on mount — triggers screen reader to re-announce the dialog context when transitioning between panels
    • useSvgAria hook on all SVG icons (YoastIcon, intent badge icons, drag handle) for consistent role="img" + aria-hidden
    • aria-busy={isLoading} on the content region instead of aria-live
    • role="listbox" + role="option" with aria-label and aria-roledescription on structure rows
    • role="note" on IntentCallout
    • UI library TextField/TextareaField provide proper <label htmlFor> / id association automatically

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

With only Free active

Loading state

The loading state is simulated internally, but it can also be triggered by following these instructions:

  1. Open the browser console and run: window.contentPlanner = { isOutlineLoading: true }
  2. Click through to open the Content Outline modal (Get content suggestions → approve modal → select a suggestion).
  3. Verify the loading state shows, matching the design:
    • Non-skeleton elements: IntentCallout with intent badge and description, instruction text, "Suggest category" toggle section, form field labels ("Focus Keyphrase", "Title", "Meta description"), footer buttons
    • Skeleton placeholders: Category badge value, form field input values (keyphrase and title as single skeleton bars, meta description as 3 skeleton lines)
    • Hidden: Blog post structure section should NOT appear during loading
  4. Verify the full content state is shown after the ~3 seconds loading simulation - that's IF you havent pasted the console command.

Content state

  1. Edit a post in the block editor. Open the Yoast sidebar or the metabox.
  2. Click "Get content suggestions" button.
  3. Verify the Get content suggestions modal opens.
  4. Click on the "Get content suggestions" button of the modal.
  5. The Content suggestion modal will open with dummy data.
  6. Click any suggestion card (e.g. "Commercial" or "Informational").
  7. Verify the Content Outline modal opens instantly — no fade-in/out animation, just an immediate panel swap.
  8. Verify the modal first shows a loading state (skeleton placeholders for form values) that transitions to the loaded content after ~3 seconds.
  9. Verify the loaded modal shows (check the figma design):
    • Correct intent badge (matching what you clicked)
    • "Suggest category" section with toggle pushed right within a narrow container, and "WordPress" badge below
    • Form fields: Focus Keyphrase, Title, Meta description (using UI library TextField/TextareaField — no double border on focus)
    • Blog post structure rows with drag handles
    • "Content suggestions" back button and "Add outline to post" button
    • Gradient fade at the bottom when content overflows (scroll-bar)
  10. Test drag-and-drop: drag a structure row to a different position and verify it reorders.
  11. Test keyboard reorder: focus a structure row and press Alt+ArrowUp / Alt+ArrowDown to move it.
  12. Click "Content suggestions" button in the footer — verify it goes back to the suggestions modal instantly (no fade animation).
  13. Scroll the modal content — verify the gradient fade appears when there's overflow and disappears when scrolled to bottom.
  14. Verify the form fields (Focus Keyphrase, Title, Meta description) are editable — click into each field and type to confirm the text can be modified.
  15. Verify the Meta description progress bar below the textarea changes color as you edit:
    • Orange when text is 1–120 characters (too short)
    • Green when text is 121–156 characters (correct length)
    • Orange when text exceeds 156 characters (too long)
    • Empty gray track when field is empty

Transitions

  1. Click "Get content suggestions" → approve modal → click "Get content suggestions" button inside the modal.
  2. Verify the approve modal fades out and the content suggestions modal fades in with a smooth cross-fade (no layout jump or stacking).
  3. After suggestions load, click any suggestion card.
  4. Verify the switch from suggestions to outline is instant — no fade-out/fade-in, no flash, no modal resize flicker.
  5. In the outline modal footer, click the "Content suggestions" back button.
  6. Verify the switch back to suggestions is also instant — the suggestions list appears immediately (already loaded, no loading state).
  7. Repeat steps 3–6 a few times to confirm the instant switches remain clean on repeated navigation.

Accessibility

  1. Screen reader test: Enable VoiceOver (or NVDA/JAWS). Navigate to the outline modal after clicking a suggestion. Verify the screen reader announces it as a dialog with the title "Content outline" (not as generic "web content").
  2. Verify the close button receives focus when the outline modal appears (the close button in the top-right corner should have a visible focus ring).
  3. Click the close button (or press Escape) — verify the entire modal closes from any view (approve, suggestions, or outline).
  4. Form field labels: With the screen reader active, tab to the Focus Keyphrase, Title, and Meta description fields in the outline modal. Verify each field announces its label (e.g. "Focus Keyphrase, edit text") — the UI library components handle htmlFor/id association automatically.

Unit tests

Run yarn jest tests/ai-content-planner/ --no-coverage from packages/js/ — all 79 tests should pass.

Relevant test scenarios

  • 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 with the browser console open
  • 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.

Impact check

  • New ContentOutlineModal component in packages/js/src/ai-content-planner/components/
  • New shared intent-badge.js module (extracted from both suggestion and outline modals)
  • New test file packages/js/tests/ai-content-planner/components/content-outline-modal.test.js
  • Modified ContentSuggestionsModal — uses shared intent badge, transitions restored for loading/success internal cross-fade
  • Modified FeatureModal — new SuggestionsPanel component for conditional transition handling, instant switches for suggestions↔outline
  • Integration changes in ContentPlannerEditorItem (added outline modal state, wiring, and window.contentPlanner.isOutlineLoading)

Other environments

  • This PR also affects Shopify.
  • This PR also affects Yoast SEO for Google Docs.

Documentation

  • I have written documentation for this change.

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 Yoast/reserved-tasks#1103

@JorPV JorPV requested a review from a team as a code owner March 26, 2026 08:50
@JorPV JorPV changed the base branch from trunk to content-planner-content-suggestion-modal March 26, 2026 08:52
@JorPV JorPV added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Mar 26, 2026
@JorPV JorPV added this to the feature/content-planner milestone Mar 26, 2026
…date form field handling to support character length tracking
@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.

@vraja-pro vraja-pro force-pushed the content-planner-content-suggestion-modal branch from 73bbe9a to 474fa32 Compare March 27, 2026 11:17
Base automatically changed from content-planner-content-suggestion-modal to feature/content-planner March 30, 2026 10:35
@coveralls
Copy link
Copy Markdown

coveralls commented Mar 30, 2026

Pull Request Test Coverage Report for Build 63c01b14af4e07c7cc4914777476bf978e0be915

Details

  • 113 of 141 (80.14%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.06%) to 53.572%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/js/src/ai-content-planner/components/content-suggestions-modal.js 5 6 83.33%
packages/js/src/ai-content-planner/components/feature-modal.js 18 24 75.0%
packages/js/src/ai-content-planner/components/content-outline-modal.js 89 110 80.91%
Totals Coverage Status
Change from base Build 3209e85c9df308a2653f9e35788020dfea4d0d81: 0.06%
Covered Lines: 34685
Relevant Lines: 65006

💛 - Coveralls

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

This PR introduces a new Content Outline Modal view to the AI Content Planner flow and wires it into the existing FeatureModal transition stack, including a simulated loading state and initial unit tests.

Changes:

  • Added ContentOutlineModal component with loading → loaded transitions, editable fields, and reorderable structure list.
  • Updated FeatureModal and ContentSuggestionsModal to support selecting a suggestion and transitioning to the outline view.
  • Added unit tests for the new outline modal and updated feature modal tests to support new dependencies.

Reviewed changes

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

Show a summary per file
File Description
packages/js/src/ai-content-planner/components/feature-modal.js Orchestrates new outline panel and transitions; adds suggestion selection state.
packages/js/src/ai-content-planner/components/content-suggestions-modal.js Adds onSuggestionClick wiring and passes full suggestion object on click.
packages/js/src/ai-content-planner/components/content-outline-modal.js New outline modal UI (loading state, editable fields, structure reorder, footer actions).
packages/js/tests/ai-content-planner/components/content-outline-modal.test.js New test suite covering outline modal rendering, loading/loaded states, a11y roles, and keyboard reorder.
packages/js/tests/ai-content-planner/components/feature-modal.test.js Updates mocks to accommodate the new outline modal dependency chain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +350 to +354
const handleDrop = useCallback( ( e, dropIndex ) => {
e.preventDefault();
const dragIndex = dragIndexRef.current;
if ( dragIndex === null || dragIndex === dropIndex ) {
setDragOverIndex( null );
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Drag-and-drop reordering logic is newly introduced but isn’t covered by unit tests, while keyboard reordering is. Add a test that simulates dragStart/dragOver/drop and asserts the structure order updates as expected to protect against regressions in the native DnD handlers.

Copilot uses AI. Check for mistakes.
Comment thread packages/js/src/ai-content-planner/components/feature-modal.js Outdated
Comment thread packages/js/src/ai-content-planner/components/content-outline-modal.js Outdated
…' attributes to form fields in ContentOutlineModal
Copy link
Copy Markdown
Contributor

@leonidasmi leonidasmi left a comment

Choose a reason for hiding this comment

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

CR: 🚧 (I didn't pay too close attention to the drag&drop parts because it doesn't work for me, as per below)

I also saw a couple of acceptance test issues already:

  • The transition from the suggestions modal to the outline modal is not very smooth, I'm adding a slowed-down screen recoding of it, to make that clear
  • There's a weird double border in the input fields when you focus on them:
image which comes from the **.yst-root textarea:focus** CSS rules (probably changing to using components from the UI library fixes this?) *drag-and-drop doesn't work for me, when I do it via the mouse. it does work when I do it via the keyboard

Comment thread packages/js/src/ai-content-planner/components/feature-modal.js Outdated
Comment thread packages/js/src/ai-content-planner/components/content-outline-modal.js Outdated
Comment thread packages/js/src/ai-content-planner/components/content-outline-modal.js Outdated
Comment thread packages/js/src/ai-content-planner/components/content-outline-modal.js Outdated
Comment thread packages/js/src/ai-content-planner/components/content-outline-modal.js Outdated
JorPV added 2 commits April 1, 2026 14:28
…badge component; replace form fields with TextField and TextareaField components for improved consistency.
JorPV and others added 5 commits April 2, 2026 10:16
…ature modal to include SuggestionsPanel with transition handling for improved user experience.
…modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on is focused when loading content suggestions.
…ndling to trigger when the panel becomes active.
… Remove unnecessary state management in ContentSuggestionsModal and implement a utility function for determining panel visibility in FeatureModal.
Copy link
Copy Markdown
Contributor

@leonidasmi leonidasmi left a comment

Choose a reason for hiding this comment

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

CR: ✅

Acceptance test: 🚧 I'm fine if we want to tackle those in a separate PR, to unblock things, but we have to make tasks for them if so:

  • I see in the design that toggling off the Suggest category toggle, hides the category badge with a transition (I see the content below slowly moving up), but in the implementation it's an instant disappearing.

  • about the step:

Verify the loading state shows, matching the design

I see a couple of disprepancies:

  • The Suggest category toggle is not disabled even though it is in the designs
  • The skeleton loader for the category badge has a border in the designs but not in the implementation:
image image
  • about the step:

In the outline modal footer, click the "Content suggestions" back button.
Verify the switch back to suggestions is also instant — the suggestions list appears immediately (already loaded, no loading state).

It's not the case for me, the first time I go back, I get the fade-in/out thing. The next time I'm trying to do so, it's instant.

…props and enhance focus management. Introduce skipTransitions prop for conditional rendering in ContentSuggestionsModal, improving loading state handling.
Copy link
Copy Markdown
Contributor

@leonidasmi leonidasmi left a comment

Choose a reason for hiding this comment

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

Acceptance: ✅

I left a CR comment on the last commit. I'm merging the CR nevertheless to keep things unblocked, but I think we should come back to this when you're back @JorPV

Comment on lines +170 to +180
<div>
<Modal.Description className="yst-mb-4">{ __( "Select a suggestion to generate a structured outline for your post.", "wordpress-seo" ) }</Modal.Description>
{ suggestions.map( ( suggestion ) => (
<SuggestionButton
key={ suggestion.title }
{ ...suggestion }
suggestion={ suggestion }
onClick={ onSuggestionClick }
/>
) ) }
</div>
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.

This is duplicated both in the branch where skipTransitions is true and where it's false., so that's not optimal. And it's weird that the skipTransitions check, controls not only the transitions but also other stuff (like the LoadingModalContent display logic, or that duplicated code's display logic). We should simplify this by making the if statement be in control of fewer things, or at the very least, rename the skipTransitions condition.

@leonidasmi leonidasmi merged commit d14bc5d into feature/content-planner Apr 3, 2026
19 checks passed
@leonidasmi leonidasmi deleted the content-planner-outline-modal branch April 3, 2026 09:29
@vraja-pro vraja-pro mentioned this pull request Apr 28, 2026
19 tasks
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.

5 participants