Skip to content

Improve popup detection with modal signals and scroll lock release - #59

Open
RomanistHere wants to merge 2 commits into
claude/stoic-keller-0rhts7from
claude/popup-removal-patterns-828uv1
Open

RomanistHere wants to merge 2 commits into
claude/stoic-keller-0rhts7from
claude/popup-removal-patterns-828uv1

Conversation

@RomanistHere

Copy link
Copy Markdown
Owner

Summary

This PR enhances PopUpOFF's popup detection and removal capabilities by introducing language-independent modal recognition, user gesture tracking, and intelligent scroll lock release for page wrappers.

Key Changes

Modal Detection Improvements

  • Added hasModalSignals() function to recognize modals by markup (<dialog>, aria-modal, role="dialog") or spam z-index values (≥1,000,000) instead of relying solely on English keywords
  • Implemented unsolicitedModalCheck() to distinguish user-invoked popups (within 2.5s of click/keypress) from unsolicited ones
  • Added trackUserGestures() to monitor user interactions (pointer/keyboard events) and spare modals opened by user actions

Scroll Lock Release

  • Introduced unlockScrollContainers() function to detect and release scroll locks on page wrapper elements (common in SPAs and modern sites)
  • Added releaseTopLayer() to properly close native <dialog> elements and popovers before hiding, preventing the page from remaining inert
  • Implemented safeguards to only act after a popup was actually hidden/moved, avoiding unnecessary modifications to legitimate app shells

Content Detection Enhancements

  • Updated contentEasyCheck() to examine visible text via innerText instead of innerHTML, improving accuracy
  • Added getVisibleText() helper to skip script/style tags and hidden nodes
  • Expanded consent iframe detection with consentIframeSelector covering major consent managers (OneTrust, Cookiebot, Didomi, etc.)
  • Added aria-label checking to content detection

Position Detection Refinements

  • Switched from offsetTop/offsetLeft/offsetHeight/offsetWidth to getBoundingClientRect() for accurate viewport-relative measurements, especially in transformed ancestors
  • Added absolute size-based detection for centered modals on large screens (previously missed as "small elements" due to viewport-relative percentages)
  • Improved detection of horizontally-centered modals by checking element center alignment

Additional Updates

  • Added "the guardian" to forbidden words list
  • Updated ESLint config to recognize new global variables and functions
  • Added comprehensive test fixtures and e2e tests for new functionality

Implementation Details

  • Scroll unlock checks are throttled (250ms) to avoid excessive layout recalculations on mutation-heavy pages
  • Modal gesture tracking uses a 2.5-second window to identify user-invoked popups
  • Scroll lock detection walks up to 5 levels deep in the DOM to find nested wrapper constraints
  • All new functionality includes proper error handling for unsupported APIs and detached elements

https://claude.ai/code/session_01NRXMzqP94iqj5tfUihF4Dw

claude added 2 commits July 3, 2026 09:36
…pper scroll locks

Moderate mode:
- positionCheckTypeI reads geometry from getBoundingClientRect: viewport-true
  inside transformed ancestors, where offsetTop/offsetLeft were relative to
  the transformed parent and misclassified the buckets
- new mid-page bucket judged by absolute size (>=280x180) and horizontal
  centering: viewport-relative area shrank on big monitors, so the same
  newsletter modal was blocked on a laptop but kept on 4K as a "side button"
- language-independent modal signals (native <dialog>, aria-modal,
  role=dialog/alertdialog, z-index >= 1M) decide the ambiguous buckets next
  to the English word lists; a modal appearing within 2.5s of the user's own
  click or keypress counts as invited and is left alone
- word matching runs on innerText instead of innerHTML - no more hits on
  class names, URLs and inline JSON - with consent-manager iframes (OneTrust,
  Sourcepoint, Didomi, Usercentrics...) recognized by src instead

All modes:
- showModal() dialogs are close()d before hiding: a hidden modal dialog stays
  in the top layer and keeps the whole page inert (looks clean, nothing
  clickable); open popovers are closed too. Delicate reopens the dialog as a
  plain in-flow element instead
- scroll locks on page wrappers (#app { height: 100vh; overflow: hidden })
  are released once a popup was actually hidden or moved; previously only
  html/body locks were handled and some pages stayed frozen. Gated on having
  acted plus the wrapper really clipping content, so app shells whose inner
  panes scroll (editors, chats) are never touched

Covered by new fixtures: centered-modal (aria/z-index signals at laptop and
4K sizes), dialog-modal (page usable again after removal), dialog-after-click
(user-invoked modal kept), wrapper-scroll-lock, app-shell (negative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRXMzqP94iqj5tfUihF4Dw
Verification (two passes at 800ms/3000ms after each sweep):
- blank-page undo: if the text that existed before the sweep is gone after
  it (a fixed app wrapper was hidden along with the popups), restore hidden
  elements biggest-first and stamp them data-popupoff-ignore so the watcher
  does not hide them right back; skipped on text-light pages that cannot be
  judged this way
- escalation (moderate only): when a fixed element still owns 4/5 sampled
  center points (elementFromPoint, so stacking is the browser's answer),
  covers >=35% of the viewport, and real content exists behind it
  (scrollHeight evidence), remove it even though the classifier kept it.
  Never fires during fullscreen video, within the user-gesture window, on
  elements that appeared on the user's own click (tracked in a WeakSet),
  on hovered elements (mega menus), on video-carrying overlays, or on
  viewport-sized app pages (maps, editors)
- timers are cleared on mode change so a stale pass can't fire into the
  new mode's state

Performance:
- checkElems batches all computed-style reads (position, display, filter)
  before the checks write styles: one forced recalc per sweep instead of
  one per hidden element
- watchMutations deduplicates via Set instead of an O(n^2) spread-array
- deleted unused forbWords/allowedWords/contentCheck/contentUnlockCheck
  and the two archived positionCheckType comment blocks (~190 lines that
  shipped to every page on every load)

Covered by new fixtures: verify-blank (wrapper restored, real popup stays
gone, restore sticks) and verify-escalation (kept wall removed once it
provably blocks reading); the user-invoked dialog test now outlives both
verification passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRXMzqP94iqj5tfUihF4Dw
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.

2 participants