Skip to content

fix(185122): don't anchor product tour steps to hidden elements - #188

Merged
floooat merged 1 commit into
masterfrom
fix-185122-tour-hidden-target
Oct 21, 2026
Merged

floooat merged 1 commit into
masterfrom
fix-185122-tour-hidden-target

Conversation

@floooat

@floooat floooat commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Symptom

Gleap ticket #185122 (Simple School Management). A product tour starts on the customer's site, but the first step ("Click on Library") is stuck. The popover sits in the top-left corner of the viewport, the page is dimmed, and nothing is clickable. Reloading brings the same step back. On 2026-10-14, one visitor was stuck on /app/library/books for 2 h 43 min (16 reloads, no tour-…-quit event). The tour only ended when it was switched back to draft. On 2026-10-20, a second tour ("Cafeteria Tour") hung the same way.

Root cause

  • The step selector :nth-child(38) > .cursor-pointer > .py-3 > .flex > :nth-child(2) points at the sidebar item's label <span>.
  • The customer's Vue sidebar hides that label with v-show (display:none) whenever the sidebar is collapsed. The group headers that shift the nth-child index are also only rendered while it is expanded.
  • GleapTours.highlight() took the first querySelector match without checking that it is rendered. A display:none node was therefore treated as found:
    • getBoundingClientRect() is all zeros, so the popover and overlay cut-out are pinned to (0,0).
    • .gleap-tour-active *{pointer-events:none} freezes the rest of the page.
    • A CLICK step has no buttons, and allowClose is off, so there is no way forward.
    • beforeunload + checkForUncompletedTour restores the step after every reload.
  • The existing "selector didn't match → quit after 5 s" path (onElementNotFound) never ran, because the selector did match.

Fix

  • findRenderedElement() resolves a step's selector to the first rendered match. It uses querySelectorAllSafe and the existing isElementVisible (offset size or client rects).
  • It is used in highlight() and in the refreshActiveHighlight() re-query.
  • If only hidden matches exist, the step keeps polling within the existing 5 s window. A target that becomes visible in that window is still picked up; otherwise the step ends in onElementNotFound, which quits the tour and frees the page.
  • A visible duplicate is now preferred over a hidden first match, for example responsive layouts that render a hidden mobile copy of a menu first.

Not changed: post steps without a selector (dummy element), the tooltip and co-browse engines, and the selector picker.

Verification

Unit tests — new src/GleapTours.test.js (jsdom; getClientRects stubbed to honour display:none):

Test Before fix After fix
treats a target that only matches a display:none element as not found (#185122) ✕ hidden label became the active element ✓
skips hidden matches and highlights the first rendered one ✕ hidden first match won ✓
highlights a rendered target right away ✓ ✓
keeps waiting for a hidden target that becomes visible within the timeout ✓ ✓
still quits when the selector matches nothing ✓ ✓
  • Full suite: 33 suites / 577 tests pass. npx webpack builds. Build artifacts are not committed, matching other fix PRs.
  • Real browser: a static page with a collapsed sidebar shaped like the customer's (li > a.cursor-pointer > div.flex.py-3 > div.flex > [span>i, span(label, display:none)], Library = 38th item). It runs the customer's exact step via Gleap.startProductTourWithConfig(…, true):
    • Current 19.3.4: the active element is the display:none "Library" span, and the popover is pinned top-left as in the customer's screenshot. Clicks on the Library icon and on a page button do nothing; the tour stays on "1 of 2".
    • This branch: "Product tour selector did not match" → destroy() → body.gleap-tour-active removed → the page button is clickable again.
    • This branch, expanded sidebar (label rendered): Library is highlighted, and clicking it advances to step 2. No regression for valid targets.

Note: the customer's site bundles gleap 16.1.1, so they only get this after upgrading. Their tours also need a stable anchor (data-gleap-tour on the menu links) to work in the collapsed sidebar.

Ticket: https://app.gleap.ai/projects/613b5dd730aed737108f87ab/bugs/6aca8d83a58db1f5a99292d0

🤖 Generated with Claude Code

A stored step selector that only matches a display:none node (a collapsed
menu's label, the hidden copy of a responsive menu) was treated as found:
the popover pinned to the viewport corner, the overlay blocked the page and
a CLICK step had no way forward, so the visitor was stuck until the tour was
unpublished (and reloads resumed it). Resolve steps to the first rendered
match instead; with none, the existing 5s wait ends in onElementNotFound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@floooat
floooat merged commit b25ca35 into master Oct 21, 2026
1 check passed
@floooat
floooat deleted the fix-185122-tour-hidden-target branch October 21, 2026 05:55
@floooat floooat mentioned this pull request Oct 21, 2026
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