Skip to content

fix(core): make present-mode click-to-navigate pass through to slide content#179

Merged
1weiho merged 2 commits into
mainfrom
fix/present-edge-click-nav
May 30, 2026
Merged

fix(core): make present-mode click-to-navigate pass through to slide content#179
1weiho merged 2 commits into
mainfrom
fix/present-edge-click-nav

Conversation

@1weiho

@1weiho 1weiho commented May 30, 2026

Copy link
Copy Markdown
Owner

Problem

In present mode the left/right click-to-navigate zones were two invisible w-[30%] <button> overlays sitting on top of the slide (z-10). Any interactive slide content — embedded videos, links, buttons — within ~30% of either edge had its clicks swallowed by the overlay and was effectively unclickable. The overlays also flashed a stray focus outline (the global outline-ring/50 rule) on click.

Fix

Remove both overlay buttons and handle click-to-navigate via a position-based handler on the player root:

  • Reads the click X position — outer 30% left → prev, outer 30% right → next, middle 40% inert (centered content never flips the page). Preserves the original navigation feel.
  • Bails when the click target is interactive (a, button, input, iframe, video, audio, embed, object, [role=button], [contenteditable], …) or part of the present chrome (marked with data-osd-chrome), so content stays clickable everywhere.
  • Authors can opt any element out of navigation with data-osd-interactive.

Because nothing covers the slide anymore, the embedded-video case works (cross-origin iframe clicks never reach the parent), and the outline-flash bug disappears with the buttons.

Navigation keeps full keyboard parity (global keydown: arrows / space / PageUp-Down) and the control bar's prev/next buttons — hence the two suppressed a11y lints on the root div.

Test plan

  • Present mode: click empty background near the left/right edges → pages change.
  • Embed an interactive element (e.g. a YouTube iframe) near an edge → its UI is clickable; clicking it does not change pages.
  • Center clicks remain inert.
  • Keyboard nav and control-bar prev/next still work.
  • No outline flashes on click.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed click-to-navigate in present mode blocking clicks on embedded content near slide edges.
    • Prevented a stray outline from flashing during navigation clicks.
  • New Features

    • Improved click-to-navigate to ignore overlays and interactive elements and to enable mobile-appropriate tap navigation.
  • Chores

    • Cleaned up click-navigation locale entries.

Review Change Stack

…content

Replace the two invisible w-[30%] nav button overlays with a position-based
click handler on the player root. The overlays sat on top of the slide and
blocked clicks to any interactive content (embedded videos, links, buttons)
within ~30% of either edge.

The handler reads the click X position (outer 30% prev/next, inert center)
and bails when the target is interactive or part of the present chrome, so
content stays clickable everywhere. This also removes the stray focus outline
that flashed on the buttons. Navigation keeps full keyboard parity via the
global keydown handler and the control bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
open-slide-demo Ready Ready Preview, Comment May 30, 2026 4:39pm
open-slide-web Ready Ready Preview, Comment May 30, 2026 4:39pm

Request Review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Walkthrough

Hook-driven edge click navigation added and wired into Player and SlideViewportNavigation; mobile gating and chrome marking prevent accidental advances. Locale keys and Locale type for clickNav were removed. A changeset documents the present-mode fix.

Changes

Click-to-Navigate Edge Fix

Layer / File(s) Summary
Click navigation hook
packages/core/src/app/lib/use-click-page-navigation.ts
New hook useClickPageNavigation that registers a click listener on a provided ref, ignores non-primary clicks, prevented-default events, text selections, and passthrough targets, computes horizontal edge hits, and calls onPrev/onNext.
Mobile detection hook
packages/core/src/app/lib/use-is-mobile.ts
New SSR-safe useIsMobile() hook using matchMedia for a Tailwind-aligned mobile breakpoint.
Player integration and chrome wrapper
packages/core/src/app/components/player.tsx
Import and invoke useClickPageNavigation with rootRef, gated by !overlayActive; remove explicit prev/next button elements and wrap chrome in div[data-osd-chrome] with display: contents.
Slide viewport wiring
packages/core/src/app/routes/slide.tsx
Replace SlideWheelNavigation with SlideViewportNavigation, remove inline ClickNavZones, add useIsMobile() and enable useClickPageNavigation on mobile when not active.
Locale and type cleanup
packages/core/src/locale/*, packages/core/src/locale/types.ts
Remove clickNav translation entries (prevAria/nextAria) from en, ja, zh-CN, zh-TW and delete the clickNav block from the Locale type.
Release notes
.changeset/fix-present-edge-click-nav.md
Add changeset describing the present-mode edge click behavior fix and outline flash removal.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble at edges, gentle and sly,
Clicks now pass through where videos lie.
Chrome tucked safe, outlines undone,
Hop forward, hop back—navigation fun! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: removing overlay buttons that blocked slide content interaction and implementing position-based click navigation that passes through to interactive elements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/app/components/click-nav-zones.tsx`:
- Line 25: Replace the Tailwind class that removes outlines with the
accessibility-friendly variant: in the click-nav button components (look for the
className string containing "absolute inset-y-0 left-0 z-20 w-[18%] min-w-12
outline-none md:hidden" and the corresponding right-side button) change
"outline-none" to "outline-hidden" so forced-colors/high-contrast modes still
show outlines while keeping the visual hide in normal themes.

In `@packages/core/src/app/components/player.tsx`:
- Around line 34-35: The NAV_PASSTHROUGH selector currently only matches
[contenteditable="true"] and thus misses other editable states like
contenteditable="plaintext-only"; update NAV_PASSTHROUGH to match any editable
element except an explicit false by replacing [contenteditable="true"] with a
broader selector such as [contenteditable]:not([contenteditable="false"]) in the
NAV_PASSTHROUGH constant so editable regions in plaintext-only or other editable
modes are included.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4084a8cc-b722-4e76-959e-c70ec66eabfd

📥 Commits

Reviewing files that changed from the base of the PR and between 155049f and 184d743.

📒 Files selected for processing (3)
  • .changeset/fix-present-edge-click-nav.md
  • packages/core/src/app/components/click-nav-zones.tsx
  • packages/core/src/app/components/player.tsx

Comment thread packages/core/src/app/components/click-nav-zones.tsx Outdated
Comment on lines +34 to +35
const NAV_PASSTHROUGH =
'a, button, input, textarea, select, label, summary, iframe, video, audio, embed, object, [role="button"], [role="link"], [contenteditable="true"], [data-osd-interactive], [data-osd-chrome]';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are the valid HTML values for the contenteditable attribute, and does [contenteditable="true"] match elements declared as <div contenteditable> or contenteditable="plaintext-only"?

💡 Result:

The contenteditable attribute is an enumerated attribute, meaning it must take one of a specific set of keyword values [1][2][3]. Valid values for the contenteditable attribute include: - true: Indicates that the element is editable [2][4]. - false: Indicates that the element is not editable [2][4]. - plaintext-only: Indicates that the element's raw text is editable, but rich text formatting is disabled [2][3][5]. - "" (empty string): Treated as equivalent to true [2][3]. Note that although "true" and "false" are allowed values, contenteditable is an enumerated attribute, not a boolean attribute [2][3]. If the attribute is missing, or if an invalid value is provided, the element inherits its editability state from its parent [2][4][6]. Regarding your second question: Yes, [contenteditable="true"] will match elements declared as

[2][3]. When the attribute is specified without a value (e.g.,
), the browser treats it as having an empty string value, which is functionally equivalent to contenteditable="true" [2][3]. However, [contenteditable="true"] will not match an element declared as

Broaden NAV_PASSTHROUGH’s contenteditable selector to include plaintext-only

[contenteditable="true"] doesn’t match the contenteditable="plaintext-only" state, so editable regions in that mode can still trigger navigation. Update the selector to include all editability states except an explicit false:

Suggested fix
 const NAV_PASSTHROUGH =
-  'a, button, input, textarea, select, label, summary, iframe, video, audio, embed, object, [role="button"], [role="link"], [contenteditable="true"], [data-osd-interactive], [data-osd-chrome]';
+  'a, button, input, textarea, select, label, summary, iframe, video, audio, embed, object, [role="button"], [role="link"], [contenteditable]:not([contenteditable="false"]), [data-osd-interactive], [data-osd-chrome]';
📝 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
const NAV_PASSTHROUGH =
'a, button, input, textarea, select, label, summary, iframe, video, audio, embed, object, [role="button"], [role="link"], [contenteditable="true"], [data-osd-interactive], [data-osd-chrome]';
const NAV_PASSTHROUGH =
'a, button, input, textarea, select, label, summary, iframe, video, audio, embed, object, [role="button"], [role="link"], [contenteditable]:not([contenteditable="false"]), [data-osd-interactive], [data-osd-chrome]';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/app/components/player.tsx` around lines 34 - 35, The
NAV_PASSTHROUGH selector currently only matches [contenteditable="true"] and
thus misses other editable states like contenteditable="plaintext-only"; update
NAV_PASSTHROUGH to match any editable element except an explicit false by
replacing [contenteditable="true"] with a broader selector such as
[contenteditable]:not([contenteditable="false"]) in the NAV_PASSTHROUGH constant
so editable regions in plaintext-only or other editable modes are included.

… viewer

Extract the position-based click navigation into a single useClickPageNavigation
hook (interactive-content passthrough + inert center) and use it in both the
Player and the slide viewer. The viewer's mobile tap zones replace the old
ClickNavZones button overlays, so embedded videos/links near the edges stay
tappable there too. Removes the now-dead component and its aria-label locale
keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/app/lib/use-click-page-navigation.ts`:
- Around line 33-37: The NAV_PASSTHROUGH guard in onClick incorrectly narrows
event.target to HTMLElement so inline SVG clicks (SVGElement) bypass the closest
check; change the type check from "target instanceof HTMLElement" to "target
instanceof Element" (or cast event.target to Element) so
target.closest(NAV_PASSTHROUGH) runs for SVG elements as well; update
use-click-page-navigation's onClick to use Element for the guard and keep the
same NAV_PASSTHROUGH constant and logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edfffa05-ffd0-4262-b7a1-b5f878c62cc7

📥 Commits

Reviewing files that changed from the base of the PR and between 184d743 and 157d0f0.

📒 Files selected for processing (11)
  • .changeset/fix-present-edge-click-nav.md
  • packages/core/src/app/components/click-nav-zones.tsx
  • packages/core/src/app/components/player.tsx
  • packages/core/src/app/lib/use-click-page-navigation.ts
  • packages/core/src/app/lib/use-is-mobile.ts
  • packages/core/src/app/routes/slide.tsx
  • packages/core/src/locale/en.ts
  • packages/core/src/locale/ja.ts
  • packages/core/src/locale/types.ts
  • packages/core/src/locale/zh-cn.ts
  • packages/core/src/locale/zh-tw.ts
💤 Files with no reviewable changes (6)
  • packages/core/src/locale/zh-cn.ts
  • packages/core/src/locale/en.ts
  • packages/core/src/app/components/click-nav-zones.tsx
  • packages/core/src/locale/ja.ts
  • packages/core/src/locale/types.ts
  • packages/core/src/locale/zh-tw.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-present-edge-click-nav.md

Comment on lines +33 to +37
const onClick = (event: MouseEvent) => {
if (event.button !== 0 || event.defaultPrevented) return;
const target = event.target;
if (target instanceof HTMLElement && target.closest(NAV_PASSTHROUGH)) return;
if (window.getSelection()?.toString()) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is Element.closest() available on SVGElement, and is event.target an SVGElement when clicking an inline SVG inside a button?

💡 Result:

Yes, Element.closest is available on SVGElement [1]. Because SVGElement inherits from the Element interface [2], it possesses all standard Element methods, including closest [3]. When you click an inline SVG nested inside a button, event.target is indeed the specific element you clicked—which will frequently be the SVGElement itself (or one of its children like a path or rect element) rather than the parent button element [4][5][6]. To reliably reference the button, you should use event.currentTarget instead of event.target [4][7]. The currentTarget property always refers to the element to which the event handler is attached, regardless of which descendant was actually clicked [4]. Alternatively, you can apply pointer-events: none; in CSS to the SVG or its children, which causes the browser to ignore the SVG for pointer events, allowing the click to pass through to the button [6][8].

Citations:


Fix NAV_PASSTHROUGH guard to handle inline SVG clicks

When clicking an inline <svg> (or its child like <path>), event.target is typically an SVGElement, not an HTMLElement, so the current instanceof HTMLElement check skips NAV_PASSTHROUGH and navigation can fire on interactive icon content. SVGElement inherits from Element, so closest works with a widened Element check.

🐛 Proposed fix
       const target = event.target;
-      if (target instanceof HTMLElement && target.closest(NAV_PASSTHROUGH)) return;
+      if (target instanceof Element && target.closest(NAV_PASSTHROUGH)) return;
📝 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
const onClick = (event: MouseEvent) => {
if (event.button !== 0 || event.defaultPrevented) return;
const target = event.target;
if (target instanceof HTMLElement && target.closest(NAV_PASSTHROUGH)) return;
if (window.getSelection()?.toString()) return;
const onClick = (event: MouseEvent) => {
if (event.button !== 0 || event.defaultPrevented) return;
const target = event.target;
if (target instanceof Element && target.closest(NAV_PASSTHROUGH)) return;
if (window.getSelection()?.toString()) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/app/lib/use-click-page-navigation.ts` around lines 33 - 37,
The NAV_PASSTHROUGH guard in onClick incorrectly narrows event.target to
HTMLElement so inline SVG clicks (SVGElement) bypass the closest check; change
the type check from "target instanceof HTMLElement" to "target instanceof
Element" (or cast event.target to Element) so target.closest(NAV_PASSTHROUGH)
runs for SVG elements as well; update use-click-page-navigation's onClick to use
Element for the guard and keep the same NAV_PASSTHROUGH constant and logic.

@1weiho 1weiho merged commit ea67658 into main May 30, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request May 30, 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.

1 participant