Skip to content

fix(reader): show selection popup below text on Android to avoid native action bar conflict#3398

Open
croakingtoad wants to merge 2 commits into
booklore-app:developfrom
croakingtoad:fix/android-selection-popup-below
Open

fix(reader): show selection popup below text on Android to avoid native action bar conflict#3398
croakingtoad wants to merge 2 commits into
booklore-app:developfrom
croakingtoad:fix/android-selection-popup-below

Conversation

@croakingtoad
Copy link
Copy Markdown

Problem

On Android, the BookLore text selection popup was broken in two ways:

1. Angular Zone.js — popup state updated but view never rendered

The iframe's document.addEventListener callbacks run outside Angular's Zone.js. This meant that when eventSubject.next({ type: 'text-selected' }) fired after a text selection, Angular had no knowledge of the state change and never ran change detection. The popup was effectively set to visible internally, but the DOM never updated.

The symptom: the popup would only appear after an unrelated Angular interaction (e.g. scrolling the page), which happened to trigger change detection as a side effect.

Fix: Inject NgZone into ReaderEventService and wrap the text-selected emission in this.ngZone.run(() => { ... }), forcing Angular to run change detection immediately when a selection is made.

2. Popup position — conflicting with Android's native copy/paste action bar

On Android, the OS-level text selection action bar (Copy / Paste / Select All etc.) always appears at the top of the screen above the selection. The BookLore popup was also positioning itself above the selection by default, causing the two to overlap — Android's bar would cover the BookLore popup entirely.

Fix: On touch devices, always show the BookLore popup below the selected text instead of above it. This keeps Android's native bar at the top and the BookLore highlight/annotate popup below the selection where it's fully visible.

// event.service.ts — handleSelectionEnd()
const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const showBelow = isMobile || selectionTop < minSpaceAbove;

Note for maintainers: The exact vertical offset and placement of the popup below the selection (selectionBottom + 10px) may want further review for different screen sizes and font sizes. The goal is simply to keep it clear of the Android action bar, which occupies the top of the screen. Desktop behavior is unchanged.

Changes

  • core/event.service.ts — inject NgZone, wrap text-selected emission in ngZone.run()
  • core/event.service.ts — detect touch device and force showBelow = true on mobile

Testing

Tested on Android via PWA (Chrome). After these fixes:

  • Long-press to begin selection ✓
  • Drag to extend selection ✓
  • BookLore popup appears immediately below the selected text on finger lift ✓
  • Android's native copy/paste bar remains accessible at the top ✓
  • Desktop behavior unchanged ✓

Platform

These changes are Android/touch-device specific. Desktop (mouse) behavior is unaffected — the isMobile check ensures the below-positioning only applies on touch devices, and the ngZone.run() fix is a correctness fix that benefits all platforms.

Marty and others added 2 commits May 6, 2026 13:51
On touch devices the popup now always appears below the selection,
avoiding overlap with Android's native copy/paste action bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tection

Iframe document event listeners run outside Angular's Zone.js, so the
popup state was being set but Angular never ran change detection — the
popup only appeared after an unrelated Angular interaction (scroll, tap).
Wrapping the text-selected emission in ngZone.run() triggers immediate
change detection so the popup renders at the same time as the selection.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant