Skip to content

Fix Reader footnotes opening in browser instead of scrolling#22733

Merged
nbradbury merged 15 commits intotrunkfrom
issue/CMM-969-reader-footnotes
Mar 26, 2026
Merged

Fix Reader footnotes opening in browser instead of scrolling#22733
nbradbury merged 15 commits intotrunkfrom
issue/CMM-969-reader-footnotes

Conversation

@nbradbury
Copy link
Copy Markdown
Contributor

@nbradbury nbradbury commented Mar 24, 2026

Description

Closes CMM-1098

Fixes footnote links in Reader posts opening in the external browser instead of scrolling to the footnote within the post.

WordPress footnote links resolve to the post's own URL with a fragment identifier (e.g., https://example.com/post/#fn-id). The Reader was treating these as external links because they passed the isValidClickedUrl http check without being recognized as same-page anchors.

Testing instructions

  1. Open a Reader post that contains WordPress Footnotes block content (you'll find one in the "rabbit-of-caribbeans" test site)
  2. Tap a footnote reference (superscript number in the post body)
  • Verify the post scrolls to the footnote at the bottom instead of opening the browser

Back-reference scrolling:

  1. In the footnotes section, tap the back-arrow (↩︎) next to a footnote
  • Verify the post scrolls back up to the reference in the text

External links still work:

  1. Tap a regular external link in a Reader post
  • Verify it still opens in the browser as before
footnote.mp4

Footnote links in WordPress posts resolve to the post's own URL with a
fragment identifier (e.g., "https://example.com/post/#fn-id"). The
Reader was routing these to the browser because they passed the http URL
check without being recognized as same-page anchors.

- In ReaderPostDetailFragment.onUrlClick, detect when a clicked URL's
  base matches the current post URL and redirect to onPageJumpClick
- Collapse the app bar on page jump so the target isn't obscured
- In ReaderWebView, add first-line fragment detection in onTouchEvent
  and shouldOverrideUrlLoading for URLs matching the content base URL
- Extract READER_CONTENT_BASE_URL constant in ReaderPostRenderer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Mar 24, 2026

1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

nbradbury and others added 5 commits March 24, 2026 15:32
The footnote fix works entirely in ReaderPostDetailFragment.onUrlClick
by comparing against the post's own URL. The ReaderWebView interception
(isSamePageFragmentUrl, loadDataWithBaseURL override, mLoadedBaseUrl)
compared against the reader base URL which never matched real footnote
URLs, so it was dead code. Also removes the now-unused
READER_CONTENT_BASE_URL constant from ReaderPostRenderer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Flatten nested if in onUrlClick fragment detection
- Remove redundant wasJsEnabled save/restore from onPageJumpClick and
  scrollToFragmentOrOpenUrl (JS is always enabled by ReaderPostRenderer)
- Fall back to opening the full URL in the browser when the footnote
  element isn't found in the page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rolling

When tapping the ↩︎ back-reference in a footnote, the WebView reports it as
an image inside an anchor (SRC_IMAGE_ANCHOR_TYPE). Previously this routed to
the fullscreen image viewer. Now we check if the parent anchor's href
contains a fragment identifier and route it through onUrlClick instead,
where existing fragment detection handles scrolling back to the reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes detekt ComplexCondition and ReturnCount violations by extracting
the 4-part fragment URL check into a dedicated method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 24, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22733-4e51379
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit4e51379
Installation URL5348fo41c7n1o
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

nbradbury and others added 2 commits March 24, 2026 16:45
Escape backslashes and single quotes in fragment identifiers passed to
evaluateJavascript to prevent potential script injection from maliciously
crafted post content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract scrollToElement() with a fallback lambda to deduplicate the JS
evaluation, sanitization, density conversion, and scroll logic that was
repeated in onPageJumpClick and scrollToFragmentOrOpenUrl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Mar 24, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22733-4e51379
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit4e51379
Installation URL63kjlnun9rqq8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@nbradbury nbradbury marked this pull request as ready for review March 24, 2026 21:15
@nbradbury nbradbury requested a review from adalpari March 24, 2026 21:15
@adalpari
Copy link
Copy Markdown
Contributor

Back-reference scrolling:

In the footnotes section, tap the back-arrow (↩︎) next to a footnote
Verify the post scrolls back up to the reference in the text

There's a strange behaviour when tapping on the 1 back note. Sometimes it takes me to the correct position, but other times it takes me to the 2nd note position 🤔

Screen_recording_20260325_193257.mp4

The scroll target was computed from the element's DOM offset alone,
ignoring the header and padding above the WebView. This caused the
page to land too high—sometimes on the wrong footnote reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nbradbury
Copy link
Copy Markdown
Contributor Author

There's a strange behaviour when tapping on the 1 back note. Sometimes it takes me to the correct position, but other times it takes me to the 2nd note position

I was unable to reproduce this, but I asked Claude to look into it and it found and fixed a bug. Can you give this another round?

…-footnotes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Copy Markdown
Contributor

There's a strange behaviour when tapping on the 1 back note. Sometimes it takes me to the correct position, but other times it takes me to the 2nd note position

I was unable to reproduce this, but I asked Claude to look into it and it found and fixed a bug. Can you give this another round?

Unfortunately, I am still able to reproduce it using my Pixel 6 physical device. Coulr you try these steps?

  1. Scroll to the bottom
  2. Tap 1 (back)
  3. Scroll to the bottom
  4. Tal 2 (back)
  5. Scroll tot he bottom
  6. Tal 1 (back) <-- I can always reproduce it in this step

nbradbury and others added 3 commits March 26, 2026 05:24
requestFocusNodeHref reads from a hit-test cache that is populated
asynchronously.  When the user taps quickly after scrolling, the
cache can still hold the URL from the previous footnote tap, causing
navigation to the wrong back-reference.

Use evaluateJavascript with document.elementFromPoint instead: it
resolves the anchor href from the actual tap coordinates at the
moment of the click, eliminating the stale-data race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous approach (elementFromPoint) failed because WebView
viewport coordinates don't map reliably to touch coordinates when
the WebView is inside a ScrollView.

Instead, inject a capturing JS click listener after page load that
intercepts taps on anchor elements with fragment hrefs and routes
them through the existing wvHandler.postMessage bridge. This sees
the actual DOM element that was clicked — no coordinate mapping and
no stale hit-test cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove scrollToFragmentOrOpenUrl and getTopWithinAncestor (both had
a single call site) by inlining their bodies. Also remove the extra
blank line left behind by the StringUtils import removal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nbradbury
Copy link
Copy Markdown
Contributor Author

Unfortunately, I am still able to reproduce it using my Pixel 6 physical device. Coulr you try these steps?

Thanks for the steps - I was able to reproduce this on a physical device. I believe it's fixed now.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 0% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.41%. Comparing base (d8ec119) to head (4e51379).
⚠️ Report is 7 commits behind head on trunk.

Files with missing lines Patch % Lines
.../wordpress/android/ui/reader/ReaderPostRenderer.kt 0.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22733      +/-   ##
==========================================
- Coverage   37.41%   37.41%   -0.01%     
==========================================
  Files        2318     2318              
  Lines      123413   123418       +5     
  Branches    16754    16755       +1     
==========================================
  Hits        46180    46180              
- Misses      73526    73531       +5     
  Partials     3707     3707              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…opagation

- Only intercept clicks on anchors whose base URL matches the post URL
  (or bare #fragment hrefs), so external links with fragments are not
  caught unnecessarily.
- Remove e.stopPropagation() to avoid suppressing other click listeners
  (text selection, analytics).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@adalpari adalpari left a comment

Choose a reason for hiding this comment

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

Works great, thank you for the fix!
🚢 it!

@nbradbury nbradbury enabled auto-merge (squash) March 26, 2026 11:04
@nbradbury nbradbury merged commit 0cbcb0b into trunk Mar 26, 2026
20 of 22 checks passed
@nbradbury nbradbury deleted the issue/CMM-969-reader-footnotes branch March 26, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants