Skip to content

Move Traffic Boost actions from highlight markup to separate container#3460

Merged
acicovic merged 22 commits intodevelopfrom
fix/actions-bar-embed-in-highlight
Jun 18, 2025
Merged

Move Traffic Boost actions from highlight markup to separate container#3460
acicovic merged 22 commits intodevelopfrom
fix/actions-bar-embed-in-highlight

Conversation

@alecgeatches
Copy link
Contributor

@alecgeatches alecgeatches commented Jun 16, 2025

Description

Move the Traffic Boost actions bar out of the highlight markup for a more consistent experience across browsers and page markup. This required some changes to positioning calculations. Additionally, this PR adds:

Motivation and context

See the "Actions bar markup as a child of highlight span" section in #3445 for a summary of issues from embedding the actions bar with the highlight markup:

Actions bar markup as a child of highlight span

The actions bar is positioned absolutely above the current highlight, and structured this way:

highlight-as-parent

This caused some issues that I didn't notice until late into development on this feature. Namely, the inserted HTML can cause newlines to appear in the textContent of the highlight that weren't previously there when the highlight was only a single <span>, and the document selection sometimes including more than 1 combined range. This mainly affected:

  • <TextSelectionTooltip>'s handleSelection(), which includes a check for when multiple ranges are present. This logic mainly kicks in when a user selects text that includes an existing suggestion. This also fixes an existing bug that could allow a user to select over multiple paragraphs and still improperly recieve a prompt to "Use as Link Text" when the placement was invalid.
  • <TextSelectionTooltip>'s onSelect() callback, which also has additional steps to handle multiple ranges and strip out HTML when the selected text includes an existing suggestion.
  • removeAndClean() used in the highlighting process was also modified to handle the parent container node that is added in highlight markup.

After the prior PR was merged, we also discovered additional problems highlighting in Chrome due to the embedded highlight markup and other triple-click selection issues.

Separating the actions bar from the highlight markup makes Selection ranges are much more consistent.

How has this been tested?

  • Locally, manually

Summary by CodeRabbit

  • Bug Fixes
    • Improved text selection and highlighting behavior for more consistent results across browsers.
    • Enhanced accuracy of text extraction, preserving special characters during highlighting.
    • Fixed actions bar so that clicks within its area inside the preview iframe are no longer blocked.
  • Refactor
    • Simplified the hint display and actions bar positioning for a cleaner user experience.
    • Streamlined highlight and actions bar management for improved reliability.
    • Removed deprecated tooltip and adjusted button labeling for clarity.
    • Simplified DOM structure of highlights and unified cleanup logic.
  • Style
    • Adjusted highlight layering to ensure visibility above other elements.
  • New Features
    • Updated actions bar positioning to dynamically align above highlights and respond to iframe resizing.

@alecgeatches alecgeatches self-assigned this Jun 16, 2025
@alecgeatches alecgeatches requested a review from a team as a code owner June 16, 2025 19:52
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 16, 2025

📝 Walkthrough

"""

Walkthrough

This update refactors the Traffic Boost preview UI and highlight logic. It improves text selection handling, normalizes triple-click selection, simplifies the highlight DOM structure, repositions the actions bar above highlights, removes tooltip and hint interactivity, and deletes the unused useResize hook. Several helper functions are introduced or updated for robustness.

Changes

File(s) Change Summary
.../preview/components/preview-actions.tsx Removed Tooltip usage and hint interactivity; replaced icon and simplified hint text display; changed button label from "Clear changes" to "Revert".
.../preview/components/preview-iframe.tsx Removed resize/throttle logic and related hooks; updated iframe document click handler to allow clicks inside the actions bar container; removed adjustActionsBarPosition usage.
.../preview/components/text-selection-tooltip.tsx Increased highlight z-index; added normalizeRange, isRangeChanged, getClosestSelectableItem, expandToLinkNode, and expandToWordBoundary helpers; improved selection normalization for triple-click; updated text extraction to preserve special characters; added iframe mouseup listener for selection normalization; clarified comments.
.../preview/hooks/use-iframe-highlight.ts Refactored highlight and actions bar DOM structure; replaced global root with cleanup function; renamed CSS classes; added dynamic positioning of actions bar above highlight with alignment adjustments; unified cleanup logic; removed exported position adjustment method.
.../preview/hooks/use-resize.ts Deleted unused useResize hook and its export.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Iframe
    participant HighlightLogic
    participant ActionsBar

    User->>Iframe: Triple-clicks or selects text
    Iframe->>HighlightLogic: Detects selection
    HighlightLogic->>HighlightLogic: Normalize selection range
    HighlightLogic->>HighlightLogic: Extract selected text accurately
    HighlightLogic->>HighlightLogic: Insert highlight span
    HighlightLogic->>ActionsBar: Position actions bar above highlight
    User->>ActionsBar: Interacts with actions bar (if desired)
Loading

Assessment against linked issues

Objective Addressed Explanation
Triple-clicking selects highlight and shows "Use as Link Text" pop-up (#3458)
Popup toolbar placement does not block text above highlight (#3457)
'Highlight to Customize' hint uses help icon, not pointer cursor (#3456)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Possibly related PRs

Suggested reviewers

  • acicovic
    """

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-17T21_15_38_652Z-debug-0.log

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

564-571: Overlay can be mis-aligned after range expansion.

updatePosition() relies on the normalizedRange captured before expandToLinkNode / expandToWordBoundary.
If either helper replaces the selection with a new Range, the overlay rectangle no longer matches the actual selection, leading to visible offset.

Compute the rect from the current selection instead:

- const rect = normalizedRange.getBoundingClientRect();
+ const rect = iframeDocument.getSelection()!
+                 .getRangeAt( 0 )
+                 .getBoundingClientRect();

(or recompute normalizedRange after expansion).

🧹 Nitpick comments (2)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-actions.tsx (1)

138-144: Cursor suggests interactivity where none exists.

.traffic-boost-preview-actions-hint still receives cursor: help; but the tooltip functionality was removed, so the element is no longer interactive. Either drop the pointer-style cursor or add an actual title/tooltip for consistency and accessibility.

src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

532-544: Inefficient range cloning loop.

Cloning every range into a temporary container is correct for preserving entities, but using a plain loop is simpler & faster:

- for ( let i = 0; i < docSelection.rangeCount; i++ ) {
-     selectionContainer.append( docSelection.getRangeAt( i ).cloneContents() );
- }
+ Array.from( { length: docSelection.rangeCount } ).forEach( ( _, i ) =>
+     selectionContainer.append( docSelection.getRangeAt( i ).cloneContents() )
+ );

Minor, but keeps the code compact.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04778a0 and b499292.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (5)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-actions.tsx (2 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-iframe.tsx (2 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (8 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (10 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-resize.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-resize.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-actions.tsx
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-iframe.tsx
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
🧬 Code Graph Analysis (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-draggable.ts (1)
  • DRAG_MARGIN_PX (11-11)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: E2E against WordPress latest

@alecgeatches alecgeatches requested a review from acicovic June 16, 2025 21:57
@acicovic acicovic added Feature: PCI Ticket/PR related to Content Intelligence Feature: Engagement Boost Ticket/PR related to Engagement Boost labels Jun 17, 2025
@acicovic acicovic added this to the 3.20.0 milestone Jun 17, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (1)

520-537: Global cleanup is invoked for every highlight removal.

Calling window.wpParselyTrafficBoostCleanupActionsBar() in removeAndClean means removing one highlight tears down the toolbar even when another highlight still needs it (rare but possible during batch operations).

Add a guard: only call if the target highlight owns the current toolbar, or delegate this responsibility to higher-level logic.

🧹 Nitpick comments (4)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (4)

171-180: Potential first-paint flash & redundant inline positioning.

The CSS gives the container an initial top/left: ${ DRAG_MARGIN_PX }px; even though JS immediately overwrites top (and often left).
Because opacity is 0, the bar is invisible, so the inline coordinates aren’t needed and create dead code.

-				top: ${ DRAG_MARGIN_PX }px;
-				left: ${ DRAG_MARGIN_PX }px;
+				/* let JS set coordinates – avoids unnecessary style churn. */

Removing the hard-coded offsets avoids needless style recalculations and keeps the stylesheet declarative.


186-193: Alignment classes duplicate inline styles.

.align-left sets the same left value that already lives in the base rule.
Either rely on the base rule or switch the base rule to left: auto so the modifier actually modifies.

This small redundancy can be confusing when debugging.


361-365: Guard clause can be simplified.

existingActions && window.wpParselyTrafficBoostCleanupActionsBar is fine, but window.wpParselyTrafficBoostCleanupActionsBar?.() is shorter and avoids the extra DOM query.

-				const existingActions = iframeDocument.querySelector( '.parsely-traffic-boost-actions-container' );
-				if ( existingActions && window.wpParselyTrafficBoostCleanupActionsBar ) {
-					window.wpParselyTrafficBoostCleanupActionsBar();
-				}
+				window.wpParselyTrafficBoostCleanupActionsBar?.();

427-435: Scope the query to avoid false positives.

iframeDocument.querySelector('.traffic-boost-preview-actions') scans the entire DOM and could collide with other content that happens to carry the same class. Query inside actionsContainer instead:

-					const renderedActionsBar = iframeDocument.querySelector( '.traffic-boost-preview-actions' ) as HTMLElement;
+					const renderedActionsBar = actionsContainer.querySelector( '.traffic-boost-preview-actions' ) as HTMLElement;

This is faster and more robust.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16e3c9d and 3ed2826.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (3)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-iframe.tsx (2 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (9 hunks)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-iframe.tsx
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
🧠 Learnings (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (1)
Learnt from: alecgeatches
PR: Parsely/wp-parsely#3460
File: src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts:421-480
Timestamp: 2025-06-16T21:30:45.103Z
Learning: In iframe highlight positioning systems, when using `position: absolute` with document-relative coordinates calculated as `element.getBoundingClientRect().top + document.scrollTop`, the positioned element will naturally move with scroll events without requiring additional scroll event listeners, as both the target element and positioned element exist in the same document coordinate system.
🧬 Code Graph Analysis (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-draggable.ts (1)
  • DRAG_MARGIN_PX (11-11)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: E2E against WordPress latest
  • GitHub Check: build
  • GitHub Check: build
🔇 Additional comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (1)

485-486: Throttle interval may be too coarse for high-density layouts.

A 100 ms window is perceptible on aggressive resize (e.g. split-screen drag). Consider 50 ms to feel snappier without hurting performance.

@acicovic
Copy link
Collaborator

@alecgeatches, I've pushed commits to improve comments and also restored the exception handler in use-iframe-highlight.ts since it was unclear to me why it was removed. Please let me know if this was on purpose.

I'll be now testing this to check whether it solves the 3 issues mentioned.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

358-375: Cleanup logic leaks scroll listeners on every new selection.

When a new selection is made you remove the previous highlight (existingHighlight.remove()), yet the scrollHandler added a few lines below remains attached because its cleanup() closure lives in the previous invocation of handleSelection. Re-selecting text repeatedly will therefore accumulate scroll listeners on both the iframe and the parent window.

Consider persisting the cleanup function on the highlight element (or in a WeakMap) so you can call it before removing the node, or hoist a single scroll listener that simply checks whether a highlight is present.

- existingHighlight.remove();
+ existingHighlight.dispatchEvent(new Event('parsely-cleanup')); // triggers stored cleanup

or

// after creating highlight
highlight.addEventListener('parsely-cleanup', cleanup);

Make sure to invoke cleanup() before removing the element to detach the listeners and avoid memory / performance degradation.

Also applies to: 491-507

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbe348b and 7591036.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
🧠 Learnings (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)
Learnt from: alecgeatches
PR: Parsely/wp-parsely#3460
File: src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts:421-480
Timestamp: 2025-06-16T21:30:45.103Z
Learning: In iframe highlight positioning systems, when using `position: absolute` with document-relative coordinates calculated as `element.getBoundingClientRect().top + document.scrollTop`, the positioned element will naturally move with scroll events without requiring additional scroll event listeners, as both the target element and positioned element exist in the same document coordinate system.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: E2E against WordPress latest
  • GitHub Check: build
  • GitHub Check: build
🔇 Additional comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

36-38: z-index bump looks safe.

Raising the highlight from 1000 to 1010 keeps it above the admin toolbar (z-index: 1000) while still below fixed WP modals (100000). No issues spotted.

Comment on lines +656 to +670
while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/g.test( startText[ startOffset - 1 ] ) ) {
startOffset--;
}
}

// Find word boundary at end.
const endNode = range.endContainer as Text;
const initialEnd = range.endOffset;
let endOffset = range.endOffset;

if ( endNode.nodeType === Node.TEXT_NODE ) {
const endText = endNode.textContent ?? '';
while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/g.test( endText[ endOffset ] ) ) {
endOffset++;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Regex with global (g) flag inside .test() causes stateful bugs.

RegExp.prototype.test with the g flag mutates lastIndex; consecutive calls can therefore skip characters and break the boundary-scan loop.

- while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/g.test( startText[ startOffset - 1 ] ) ) {
+ while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/.test( startText[ startOffset - 1 ] ) ) {- while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/g.test( endText[ endOffset ] ) ) {
+ while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/.test( endText[ endOffset ] ) ) {

Removing g keeps the regex stateless and deterministic.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while ( startOffset > 0 && /[^\s.,!?;:'")\]}]/g.test( startText[ startOffset - 1 ] ) ) {
startOffset--;
}
}
// Find word boundary at end.
const endNode = range.endContainer as Text;
const initialEnd = range.endOffset;
let endOffset = range.endOffset;
if ( endNode.nodeType === Node.TEXT_NODE ) {
const endText = endNode.textContent ?? '';
while ( endOffset < endText.length && /[^\s.,!?;:'"([{]/g.test( endText[ endOffset ] ) ) {
endOffset++;
}
while ( startOffset > 0 && /[^\s.,!?;:'")\]}]/.test( startText[ startOffset - 1 ] ) ) {
startOffset--;
}
}
// Find word boundary at end.
const endNode = range.endContainer as Text;
const initialEnd = range.endOffset;
let endOffset = range.endOffset;
if ( endNode.nodeType === Node.TEXT_NODE ) {
const endText = endNode.textContent ?? '';
while ( endOffset < endText.length && /[^\s.,!?;:'"([{]/.test( endText[ endOffset ] ) ) {
endOffset++;
}
🤖 Prompt for AI Agents
In
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
around lines 656 to 670, the regular expressions used in the while loops for
detecting word boundaries include the global flag 'g', which causes stateful
behavior and can skip characters due to the mutation of lastIndex in
RegExp.prototype.test. To fix this, remove the 'g' flag from both regular
expressions so that the test method behaves statelessly and the loops correctly
identify word boundaries without skipping characters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This may be true, but for now it seems to work as-is so I'm not going to modify this now.

Copy link
Contributor

Choose a reason for hiding this comment

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

@alecgeatches Understood! You're right that it's working as expected currently. If you'd like to track this for a future cleanup, I can create an issue to document the potential regex stateful behavior concern, but no worries if you prefer to leave it as-is for now.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

32-38: Popover becomes un-clickable due to inherited pointer-events: none.

pointer-events: none; on .parsely-traffic-boost-highlight disables all mouse interaction for the element and its descendants, which includes the popover container and the buttons inside it. This prevents users from clicking “Use as Link Text / Replace Link”.

Proposed quick fix – keep the overlay transparent to selections, but re-enable events only for the popover:

.parsely-traffic-boost-highlight {
  position: absolute;
  /* Allow overlay itself to ignore clicks … */
  pointer-events: none;
  z-index: 1010;
  transition: all 0.15s ease-out;
}

.parsely-traffic-boost-popover-container {
  position: absolute;
+ /* … but re-enable interaction for the popover. */
+ pointer-events: auto;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
♻️ Duplicate comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

685-699: Regex still uses the g flag inside .test() – inherits previous issue.

Comment already raised in an earlier review; the stateful lastIndex continues to cause skipped characters on consecutive calls. Please drop the g flag.

🧹 Nitpick comments (2)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (2)

458-475: Scroll listeners are probably redundant and unthrottled.

Because the highlight’s absolute coordinates are recomputed from getBoundingClientRect() plus scrollY, its position already tracks scrolling without any extra work (see internal learning note on iframe positioning). The added scroll handlers:

  1. Fire on every scroll event in both the iframe and the parent window.
  2. Have no throttling apart from a single requestAnimationFrame, which can still overwhelm low-end devices during rapid scrolling.

Unless you have confirmed visual glitches without them, consider deleting the listeners and relying on normal document flow, or at least debounce them.


617-626: Minor: fallback for nodes without a parentElement.

node.parentElement is null for DocumentFragment and certain text nodes; calling .closest() on null is safe, but you silently skip those nodes. Using node.parentNode as Element would cover both cases. Not critical, just a consideration.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7591036 and 6444be0.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
🧠 Learnings (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)
Learnt from: alecgeatches
PR: Parsely/wp-parsely#3460
File: src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts:421-480
Timestamp: 2025-06-16T21:30:45.103Z
Learning: In iframe highlight positioning systems, when using `position: absolute` with document-relative coordinates calculated as `element.getBoundingClientRect().top + document.scrollTop`, the positioned element will naturally move with scroll events without requiring additional scroll event listeners, as both the target element and positioned element exist in the same document coordinate system.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build
  • GitHub Check: E2E against WordPress latest
  • GitHub Check: build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

448-451: Horizontal scroll not accounted for in overlay positioning.

left uses rect.left (viewport-relative) without adding scrollX, so the highlight drifts when the iframe content is horizontally scrolled.

If you keep manual positioning, mirror the vertical logic:

- highlight.style.left = `${ rect.left }px`;
+ const scrollX = iframeDocument.defaultView?.scrollX ?? 0;
+ highlight.style.left = `${ rect.left + scrollX }px`;
♻️ Duplicate comments (2)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (2)

588-590: document.createRange() built in the wrong document – still breaks iframe selections.

normalizeRange creates a range with document.createRange(), which belongs to the parent document.
When the selection originates inside the iframe, every triple-click throws WRONG_DOCUMENT_ERR in all major browsers.

This was flagged in a previous review and remains unresolved.
Replace the call with the owner document of the start node:

- const newRange = document.createRange();
+ const newRange = ( range.startContainer.ownerDocument || document ).createRange();

690-703: Global-flag regex in .test() is still stateful and skips characters.

The two while loops use /…/g.test(...).
Because the pattern has the g flag, RegExp.prototype.test mutates lastIndex; consecutive calls falsely return false, making the boundary scan unreliable.

Drop the g flag to keep the regex stateless:

- while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/g.test( startText[ startOffset - 1 ] ) ) {
+ while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/.test( startText[ startOffset - 1 ] ) ) {- while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/g.test( endText[ endOffset ] ) ) {
+ while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/.test( endText[ endOffset ] ) ) {
🧹 Nitpick comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

445-463: Scroll listeners & manual repositioning are redundant and incomplete.

Given the highlight overlay is position:absolute inside the same document as the selected text, its coordinates already move with scrolling (see learning note from PR #3460). The extra scroll listeners:

  1. Perform an unnecessary getBoundingClientRect/layout thrash on every scroll.
  2. Adjust only scrollY, ignoring horizontal scroll (scrollX), which can mis-align the overlay.

Unless you later detach the overlay into a fixed-position layer, you can delete the listener block and remove the + scrollY offset – the element will track naturally.

- const rect = normalizedRange.getBoundingClientRect();
- const scrollY = iframeDocument.defaultView?.scrollY ?? 0;
- highlight.style.top = `${ rect.top + scrollY }px`;
+ const rect = normalizedRange.getBoundingClientRect();
+ highlight.style.top = `${ rect.top }px`;- iframeDocument.addEventListener( 'scroll', scrollHandler, { passive: true } );
- window.addEventListener( 'scroll', scrollHandler, { passive: true } );
+ // No explicit scroll handler required.
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6444be0 and d1559b3.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
🧠 Learnings (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)
Learnt from: alecgeatches
PR: Parsely/wp-parsely#3460
File: src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts:421-480
Timestamp: 2025-06-16T21:30:45.103Z
Learning: In iframe highlight positioning systems, when using `position: absolute` with document-relative coordinates calculated as `element.getBoundingClientRect().top + document.scrollTop`, the positioned element will naturally move with scroll events without requiring additional scroll event listeners, as both the target element and positioned element exist in the same document coordinate system.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: E2E against WordPress latest
  • GitHub Check: build
  • GitHub Check: build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

691-705: Regex with global (g) flag inside .test() is still stateful – remove g.

This was flagged in an earlier review and remains unresolved.
RegExp.prototype.test mutates lastIndex when the pattern has g, leading to missed characters on subsequent iterations.

-while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/g.test( startText[ startOffset - 1 ] ) ) {
+while ( startOffset > 0 && /[^\s.,!?;:'"’)\]}]/.test( startText[ startOffset - 1 ] ) ) {-while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/g.test( endText[ endOffset ] ) ) {
+while ( endOffset < endText.length && /[^\s.,!?;:'"’([{]/.test( endText[ endOffset ] ) ) {
🧹 Nitpick comments (4)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (4)

349-358: Middle ranges are not validated – multi-range selections can still straddle paragraphs.

The logic checks only the first and last ranges for paragraph alignment. If docSelection contains more than two ranges, any range in the middle could still cross paragraph boundaries and slip through this guard.

Consider iterating over all ranges (for … rangeCount) and ensuring every range’s start/end container resolves to the same paragraph element.


458-463: Scroll listeners may be unnecessary – absolute positioning already follows scroll.

Because highlight and the target text live in the same document, coordinates from getBoundingClientRect() already account for scrolling; the element will move naturally. Listening to both the iframe and parent window scroll events on every highlight can introduce unnecessary re-flows and event overhead.

Re-test without these listeners; if behaviour is unchanged, the listeners (and cleanup code) can be dropped.


489-495: Debounce fires twice per event due to leading: true, trailing: true.

@wordpress/compose’s debounce will invoke the wrapped function on both the leading and trailing edge, resulting in two handleSelection executions for each selection change. If you only need one call, set either leading or trailing to true.

-const handleSelectionChange = debounce( handleSelection, 300, {
-  leading: true,
-  trailing: true,
-} );
+const handleSelectionChange = debounce( handleSelection, 300, { trailing: true } );

566-596: Create the new Range in the owner document to avoid WRONG_DOCUMENT_ERR.

currentDocument.createRange() is safe only when currentDocument is guaranteed to be the same as startParagraph.ownerDocument. Passing iframeDocument from the caller works today but is fragile if this helper is reused elsewhere.

-const newRange = currentDocument.createRange();
+const newRange = ( startParagraph.ownerDocument || currentDocument ).createRange();

This mirrors the fix applied elsewhere in the codebase and prevents cross-document exceptions.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1559b3 and d44cf69.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx
🧠 Learnings (1)
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)
Learnt from: alecgeatches
PR: Parsely/wp-parsely#3460
File: src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts:421-480
Timestamp: 2025-06-16T21:30:45.103Z
Learning: In iframe highlight positioning systems, when using `position: absolute` with document-relative coordinates calculated as `element.getBoundingClientRect().top + document.scrollTop`, the positioned element will naturally move with scroll events without requiring additional scroll event listeners, as both the target element and positioned element exist in the same document coordinate system.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build
  • GitHub Check: E2E against WordPress latest
  • GitHub Check: build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (5)

20-22: Consider tightening the global namespace.

window.wpParselyTrafficBoostCleanupActionsBar is already reasonably namespaced, but it is still a mutable global.
For even lower collision-risk you could attach it to window.wpParsely = { … } or wrap it in a Symbol. Purely optional.


171-250: Hard-coded colours & spacing – prefer design tokens / CSS variables.

The new rules embed literal values like #3858E9, #1e1e1e, and 35px offsets.
Using WP admin CSS variables (e.g. var(--wp-admin-theme-color)), or defining local custom properties, makes future theming easier and aligns with WordPress standards.
Example:

- outline: 2px solid #3858E9;
+ outline: 2px solid var(--tb-highlight-color, #3858E9);

Same applies to the fixed 35 / 48 / 36 pixel constants.


408-419: Docblock wording & early cancellation.

  1. The comment says “Sets up the actions bar cleanup function”, but the function itself performs the cleanup.
    Rename to “Cleans up the actions bar” (or adjust the description) for clarity.

  2. The cancellation block is 👍 – it prevents the throttled callback from firing after unmount.


431-483: Re-adding fade-in on every resize may cause flicker.

positionActionsBar calls classList.add('fade-in') unconditionally.
After the first call the class is already present, so subsequent additions are no-ops in most browsers, but Safari will restart the transition, producing a subtle opacity flash.

Guard once:

- actionsContainer.classList.add( 'fade-in' );
+ if ( ! actionsContainer.classList.contains( 'fade-in' ) ) {
+     actionsContainer.classList.add( 'fade-in' );
+ }

485-491: Timeout-based scroll settling is brittle.

Waiting a fixed 400 ms assumes the iframe finishes auto-scrolling in that window.
Consider:

const waitForScroll = () => new Promise( r => {
    const done = () => { iframeDocument.removeEventListener('scroll', done); r(); };
    iframeDocument.addEventListener('scroll', done, { once: true, passive: true });
});
await waitForScroll();
positionActionsBar();

or requestAnimationFrame polling until scrollTop stabilises.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d44cf69 and 4f61a0b.

⛔ Files ignored due to path filters (2)
  • build/content-helper/dashboard-page.asset.php is excluded by !build/**
  • build/content-helper/dashboard-page.js is excluded by !build/**
📒 Files selected for processing (1)
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (11 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{js,ts,tsx,jsx}`: "Perform a detailed review of the provided code with following key aspects in mind: - Review the code to ensure it is well-structured and adheres to best ...

**/*.{js,ts,tsx,jsx}: "Perform a detailed review of the provided code with following key aspects in mind:

  • Review the code to ensure it is well-structured and adheres to best practices.
  • Verify compliance with WordPress coding standards.
  • Ensure the code is well-documented.
  • Check for security vulnerabilities and confirm the code is secure.
  • Optimize the code for performance, removing any unnecessary elements.
  • Validate JSDoc comments for accuracy, currency, and adherence to WordPress coding standards.
  • Ensure each line comment concludes with a period.
  • Confirm every JSDoc comment includes a @SInCE tag indicating the next version of the plugin to include the code.
  • Guarantee compatibility with the latest version of WordPress, avoiding deprecated functions or features."
  • src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: E2E against WordPress latest
🔇 Additional comments (4)
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (4)

10-10: Import looks correct.

throttle is now imported exactly where it is used, eliminating an implicit global dependency.
Nothing else to flag.


361-364: Previous actions bar is cleaned before rendering the new one.

Good defensive check – prevents duplicate toolbars and dangling listeners.


494-496: Resize listener throttling is correctly implemented.

The handler is throttled and properly removed in the cleanup routine – well done.


526-532: Cleanup is invoked for every highlight removal.

Calling window.wpParselyTrafficBoostCleanupActionsBar() here guarantees no orphaned toolbar even when multiple highlights are removed in quick succession. Looks good.

@acicovic acicovic merged commit 14a9699 into develop Jun 18, 2025
38 of 39 checks passed
@acicovic acicovic deleted the fix/actions-bar-embed-in-highlight branch June 18, 2025 08:17
github-actions bot added a commit that referenced this pull request Jun 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature: Engagement Boost Ticket/PR related to Engagement Boost Feature: PCI Ticket/PR related to Content Intelligence

Projects

None yet

2 participants