Skip to content

Add dynamic banner colors and intent badge tooltips to content planner modals#23152

Open
JorPV wants to merge 7 commits intofeature/content-plannerfrom
content-outline-modal-dynamic-banner-colors-intent-badge-tooltips
Open

Add dynamic banner colors and intent badge tooltips to content planner modals#23152
JorPV wants to merge 7 commits intofeature/content-plannerfrom
content-outline-modal-dynamic-banner-colors-intent-badge-tooltips

Conversation

@JorPV
Copy link
Copy Markdown
Contributor

@JorPV JorPV commented Apr 10, 2026

Context

This PR implements dynamic background colors for the "Why this content?" callout banner in the Content Outline modal and adds hover tooltips to intent badges in both the Content Suggestions and Content Outline modals. Previously the callout always used hardcoded blue colors regardless of intent type. Now the colors adapt to match the intent (blue for Informational, violet for Navigational, yellow for Commercial, green for Transactional). The tooltips explaining what each intent type means are also added on hover. Additionally, suggestion cards in the Content Suggestions modal now have a hover state (subtle background tint, darker border, underlined title) matching the design.

Design reference: Figma – Content suggestions

Summary

This PR can be summarized in the following changelog entry:

  • Adds dynamic background and border colors and tooltips to intent badges in both the Content Suggestions modal and the Content Outline modal.
  • Adds a hover state to suggestion cards in the Content Suggestions modal.

Relevant technical choices

  • Shared intent config in intent-badge.js: Extended the existing mapping with calloutClasses, calloutTextClasses, and tooltip fields per intent type, keeping all intent-related styling and copy in one place.
  • Tooltip positioning: The Content Outline modal uses position="bottom-right" for its tooltip because the callout sits at the top of a scrollable container with overflow-y: auto — an absolutely-positioned tooltip above the badge would be clipped by the parent's overflow. The Content Suggestions modal uses position="top-right" since badges there have enough room above them. top-right / bottom-right anchors the tooltip from the badge's left edge so it extends rightward into available space, preventing it from overflowing the modal's left boundary.
  • IntentBadgeWithTooltip component: Extracted a dedicated component in the outline modal to keep the IntentCallout component's cyclomatic complexity within the ESLint limit.
  • Tooltip width constraint: Applied yst-max-w-48 to force the tooltip text to wrap into multiple lines (matching the Figma design), preventing a single wide line from overflowing the modal.
  • Hover state on suggestion cards: Used yst-group on the button with group-hover:yst-underline on the title, combined with hover:yst-bg-slate-50 and hover:yst-border-slate-300 plus yst-transition-colors for a smooth effect.
  • Transactional intent: Added as a new entry in the shared intentBadge mapping with ShoppingCartIcon and green color scheme. A placeholder transactional suggestion was added to the suggestions list for testing.

Test instructions for the acceptance test before the PR gets merged

  1. Go to Posts > Add New in WordPress.
  2. The Yoast Content Planner inline banner should appear below the first paragraph block.
  3. Click Get content suggestions from the Yoast SEO metabox or sidebar.
  4. In the approve modal, click Get content suggestions.
  5. Wait for the suggestions to load. Verify:
    • Each suggestion card shows a colored intent badge (Informational = blue, Navigational = violet, Commercial = yellow, Transactional = green).
    • Hovering over an intent badge shows a dark tooltip above-right with centered text explaining the intent.
    • Hovering over a suggestion card shows a hover state: the background tints slightly lighter, the border darkens, and the card title becomes underlined.
image
  1. Click on a suggestion to open the Content Outline modal. Verify:
    • The "Why this content?" callout banner has a background and border color matching the intent (e.g. blue for Informational, violet for Navigational).
    • Hovering over the intent badge in the callout shows a tooltip bottom-right with the intent description (see 🧵 slack thread with UX approval).
    • The modal content area scrolls normally (scrollbar spans from below the callout to the footer).
image
  1. Click ← Content suggestions to go back and select a suggestion with a different intent. Verify the callout colors update accordingly.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different browsers
  • 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 multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

Impact check

  • Content Planner intent badge component (intent-badge.js)
  • Content Suggestions modal (content-suggestions-modal.js)
  • Content Outline modal (content-outline-modal.js)

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.

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/1145

@JorPV JorPV added the changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog label Apr 10, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 10, 2026

Coverage Report for CI Build 9232

Coverage decreased (-6.6%) to 46.971%

Details

  • Coverage decreased (-6.6%) from the base build.
  • Patch coverage: 12 of 12 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 5689
Covered Lines: 2715
Line Coverage: 47.72%
Relevant Branches: 1392
Covered Branches: 611
Branch Coverage: 43.89%
Branches in Coverage %: Yes
Coverage Strength: 528758.87 hits per line

💛 - Coveralls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

Implements intent-aware UI styling and explanations in the AI Content Planner modals by centralizing intent configuration and using it to drive badge/callout styling and tooltip copy.

Changes:

  • Extended the shared intentBadge mapping with per-intent callout colors and tooltip strings, plus a new “Transactional” intent.
  • Updated Content Suggestions modal intent badges to show hover tooltips.
  • Updated Content Outline modal intent callout to use intent-specific banner colors and show a tooltip for the intent badge.

Reviewed changes

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

File Description
packages/js/src/ai-content-planner/components/intent-badge.js Centralizes tooltip text and callout color classes per intent; adds transactional intent config.
packages/js/src/ai-content-planner/components/content-suggestions-modal.js Adds tooltip behavior to intent badges in the suggestions list (and a placeholder transactional suggestion).
packages/js/src/ai-content-planner/components/content-outline-modal.js Adds intent-colored callout styling and extracts an intent badge + tooltip helper component.
packages/js/tests/ai-content-planner/components/content-outline-modal.test.js Expands intent coverage to include the new transactional intent and adjusts unknown-intent test.

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

const [ isTooltipVisible, setIsTooltipVisible ] = useState( false );
const handleMouseEnter = useCallback( () => setIsTooltipVisible( true ), [] );
const handleMouseLeave = useCallback( () => setIsTooltipVisible( false ), [] );
const tooltipId = `suggestion-intent-tooltip-${ intent }-${ title }`;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

tooltipId is built from title, which contains spaces (and may contain other characters) and produces an invalid/fragile HTML id value. This can break aria-describedby and any selector-based logic. Use a sanitized/slugged value or a stable generated id (e.g. React useId/instance id) instead of interpolating the raw title string.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +37
<Badge
className={ classNames( "yst-relative yst-flex yst-items-center yst-gap-1 yst-w-fit yst-text-xs yst-cursor-default", badge.classes ) }
aria-describedby={ tooltipId }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Same ARIA/a11y issue as in the suggestions modal: aria-describedby is set unconditionally while the tooltip is conditionally mounted, so the trigger often points to a missing element. Also the tooltip is only shown on mouse enter/leave, so it’s not discoverable via keyboard. Consider switching to TooltipContainer + TooltipTrigger + TooltipWithContext from @yoast/ui-library (keeps tooltip in DOM and supports focus/ESC) or make aria-describedby conditional and add focus/blur support.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +169
it( "renders the correct badge for transactional intent", () => {
renderModal( { suggestion: { ...defaultSuggestion, intent: "transactional" } } );
expect( screen.getByText( "transactional" ) ).toBeInTheDocument();
expect( screen.getByText( "Transactional" ) ).toBeInTheDocument();
} );
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This PR introduces new intent behavior (tooltip text + dynamic callout colors per intent), but the tests here only assert the badge label. Consider adding assertions that the intent tooltip content becomes visible on hover/focus and that the callout container uses the intent-specific classes, so regressions in tooltip wiring or styling are caught.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +81
<button type="button" onClick={ handleClick } className="yst-text-start yst-w-full yst-rounded-md yst-border yst-border-slate-200 yst-mb-4 yst-p-4 yst-shadow-sm focus:yst-outline focus:yst-outline-2 focus:yst-outline-offset-2 focus:yst-outline-primary-500">
{ intentBadge[ intent ] ? (
<Badge className={ classNames( "yst-flex yst-items-center yst-gap-1 yst-w-fit yst-mb-2 yst-text-xs", intentBadge[ intent ].classes ) }>
<Icon className={ classNames( "yst-w-3 ", intentBadge[ intent ].classes ) } { ...svgAriaProps } /> { intentBadge[ intent ].label }
{ badge ? (
<Badge
className={ classNames( "yst-relative yst-flex yst-items-center yst-gap-1 yst-w-fit yst-mb-2 yst-text-xs", badge.classes ) }
aria-describedby={ tooltipId }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

aria-describedby is applied to the Badge, but the focusable element here is the surrounding <button>—so keyboard/screen reader users won’t get the tooltip description, and the tooltip itself is only mounted on mouse hover. Additionally, because the tooltip is conditionally rendered, aria-describedby often points to a non-existent id. Prefer the ui-library TooltipContainer + TooltipTrigger + TooltipWithContext (focus + hover, tooltip stays in DOM), or move aria-describedby/show-hide handling to the actual focusable trigger and only reference an id that exists.

Copilot uses AI. Check for mistakes.
@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.

JorPV and others added 2 commits April 17, 2026 10:20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d hover with the new IntentBadge/IntentCallout architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JorPV JorPV requested a review from Copilot April 17, 2026 10:19
@JorPV JorPV changed the title Add dynamic banner colors and intent badge tooltips to content planner modals Add dynamic banner colors, intent badge tooltips and hover state to content planner modals Apr 17, 2026
@JorPV JorPV added this to the feature/content-planner milestone Apr 17, 2026
@JorPV JorPV changed the title Add dynamic banner colors, intent badge tooltips and hover state to content planner modals Add dynamic banner colors and intent badge tooltips to content planner modals Apr 17, 2026
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

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


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

Comment on lines +70 to +79
return (
<Badge
className={ classNames( "yst-relative yst-flex yst-items-center yst-gap-1 yst-w-fit yst-cursor-default", badge.classes, className ) }
aria-describedby={ tooltipId }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
>
<Icon className={ classNames( "yst-w-3", badge.classes ) } { ...svgAriaProps } /> { badge.label }
{ isTooltipVisible && <Tooltip id={ tooltipId } className="yst-max-w-48 yst-z-50 yst-text-center" position={ tooltipPosition }>{ badge.tooltip }</Tooltip> }
</Badge>
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The new tooltip behavior (show/hide + intent-specific tooltip copy) and the intent-driven callout styling are introduced/changed here, but there are no unit tests asserting tooltip rendering on hover (or that the correct tooltip text appears for each intent). Consider extending the existing modal/component tests to simulate hover and assert the tooltip content, to prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
const [ isTooltipVisible, setIsTooltipVisible ] = useState( false );
const handleMouseEnter = useCallback( () => setIsTooltipVisible( true ), [] );
const handleMouseLeave = useCallback( () => setIsTooltipVisible( false ), [] );
const tooltipId = `intent-tooltip-${ intent }`;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

tooltipId is derived only from intent (e.g. intent-tooltip-informational), which will produce duplicate IDs when multiple badges of the same intent are rendered (e.g. in the suggestions list). This breaks DOM id uniqueness and can confuse ARIA relationships. Generate a per-instance stable unique id (e.g. via useInstanceId from @wordpress/compose or a useRef-backed unique suffix) and incorporate it into the tooltip id.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
className={ classNames( "yst-relative yst-flex yst-items-center yst-gap-1 yst-w-fit yst-cursor-default", badge.classes, className ) }
aria-describedby={ tooltipId }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
>
<Icon className={ classNames( "yst-w-3", badge.classes ) } { ...svgAriaProps } /> { badge.label }
{ isTooltipVisible && <Tooltip id={ tooltipId } className="yst-max-w-48 yst-z-50 yst-text-center" position={ tooltipPosition }>{ badge.tooltip }</Tooltip> }
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

aria-describedby is always set to tooltipId, but the element with that id is only rendered conditionally when the tooltip is visible. When the tooltip is hidden, the referenced element doesn't exist, so assistive tech won't have a valid description target. Consider only setting aria-describedby while the tooltip is rendered, or render a persistently-present (visually hidden / offscreen) description node so the reference is always valid.

Copilot uses AI. Check for mistakes.
…dby, tests

- Use React useId to generate per-instance tooltip ids so rendering multiple badges with the same intent no longer produces duplicate DOM ids.
- Only set aria-describedby while the tooltip is rendered, so the reference always points to an existing element.
- Add intent-badge.test.js covering label rendering, hover show/hide, aria-describedby toggling and unique-id generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants