Skip to content

fix(nav): PR #118 follow-ups (#119, #120, #121)#122

Merged
yigitdot merged 7 commits into
mainfrom
fix/nav-followups-118
May 19, 2026
Merged

fix(nav): PR #118 follow-ups (#119, #120, #121)#122
yigitdot merged 7 commits into
mainfrom
fix/nav-followups-118

Conversation

@yigitdot
Copy link
Copy Markdown
Collaborator

@yigitdot yigitdot commented May 18, 2026

Resolves the three standalone follow-ups filed off the PR #118 review. Each is a pre-existing weakness deliberately deferred so #118 stayed scoped to the #116 hash-accumulation fix — none is a regression introduced by #118. Conventional commits, bisectable.

shouldInterceptNavClick extraction — Closes #120

Extracts the 6-flag modifier short-circuit from Chrome.tsx's handleNavClick into a pure shouldInterceptNavClick predicate in lib/scroll.ts, mirroring the #114#117 precedent. Adds a lib/scroll.test.ts block: plain left-click → true; defaultPrevented / middle-click / each modifier → false. Zero new infra (Node-env Vitest, existing glob).

Ease scrollToAnchor + preserve horizontal scroll — Closes #121

scrollToAnchor now uses a quadratic ease-in-out instead of linear, and reads window.scrollX live each rAF frame (instead of the original hard-coded 0, which would yank horizontal scroll to the left edge). Live-read — not a captured startX — is the deliberate choice: the loop only drives Y, so re-reading X each frame is a no-op pass-through that never fights the user (see review threads). Reduced-motion early-return, single-flight cancel/restore, and the distance === 0 short-circuit are unchanged. Only caller is the mobile drawer's close-then-scroll.

Move focus to section on in-page nav jump — Closes #119

The intercepted same-page jump scrolled the section into view but never moved keyboard / screen-reader focus, leaving tab order and the SR reading cursor stuck on the nav. Both call sites (desktop handleNavClick, mobile drawer cleanup) now set tabindex="-1" (guarded by if (el.tabIndex < 0) so an already-focusable target isn't pulled out of tab order) and .focus({ preventScroll: true }). On mobile, an additional focusMovedToSectionRef flag stops the toggle-refocus effect from yanking focus back to the hamburger after an anchor-close (its setup runs after the cleanup that moves focus, in the same commit) — without it the mobile focus move was a no-op. No CSS guard needed — globals.css uses :focus-visible only, which doesn't fire for programmatic focus.

Review follow-ups

Verification

  • pnpm test — 90 passed (incl. shouldInterceptNavClick cases)
  • pnpm lint / pnpm format:check — clean
  • pnpm build — static export succeeds

Not auto-verifiable (needs manual / AT testing): keyboard tab-order + VoiceOver focus landing for #119 (desktop and mobile drawer, incl. that Escape/scrim still returns focus to the toggle), and the eased scroll feel for #121.

🤖 Generated with Claude Code

yigitdot and others added 3 commits May 18, 2026 17:41
Closes #120

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #121

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #119

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 14:42
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 18, 2026

Deploying website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 18ea0b1
Status: ✅  Deploy successful!
Preview URL: https://8ca35177.website-70y.pages.dev
Branch Preview URL: https://fix-nav-followups-118.website-70y.pages.dev

View logs

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses three follow-ups from the PR #118 review by improving (1) testability of the nav-click interception guard, (2) smooth-scroll ergonomics in the mobile drawer’s custom anchor scroll, and (3) accessibility by ensuring focus follows in-page navigation.

Changes:

  • Extracted the “modified click” short-circuit into a pure shouldInterceptNavClick helper and added unit tests for it.
  • Updated scrollToAnchor to use quadratic ease-in-out and to preserve horizontal scroll by pinning scrollX during the animation.
  • After intercepted in-page navigation, moved keyboard/screen-reader focus into the destination section (desktop + mobile drawer).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
lib/scroll.ts Adds shouldInterceptNavClick; improves scrollToAnchor easing and preserves horizontal scroll during animation.
lib/scroll.test.ts Adds unit tests covering shouldInterceptNavClick cases.
components/site/MobileMenu.tsx Moves focus to the target section after the drawer closes and scrollToAnchor starts.
components/site/Chrome.tsx Replaces inline click-guard with shouldInterceptNavClick and moves focus to the section after same-page nav interception.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors navigation click interception into a shared utility, adds unit tests, and enhances the custom smooth-scrolling implementation with quadratic easing. It also introduces accessibility improvements by shifting focus to target sections after scrolling. Feedback focuses on maintaining horizontal scroll responsiveness by using window.scrollX instead of a captured value and improving tabindex handling to ensure that already focusable elements are not negatively impacted.

Comment thread lib/scroll.ts Outdated
// Quadratic ease-in-out — linear reads mechanical next to the
// browser's native eased curve.
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
window.scrollTo(startX, startY + distance * ease);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a captured startX locks the horizontal scroll position for the duration of the animation, which can feel restrictive and "stiff" if the user attempts to scroll horizontally. Using window.scrollX directly in each frame preserves the current horizontal position while remaining responsive to user input, providing a more natural feel. This also makes the startX variable at line 71 unnecessary.

Suggested change
window.scrollTo(startX, startY + distance * ease);
window.scrollTo(window.scrollX, startY + distance * ease);
References
  1. When implementing custom smooth-scrolling, use easing functions (such as quadratic ease-in-out) and preserve the horizontal scroll position (window.scrollX) to ensure a natural feel and prevent horizontal jumping.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Accepted — fixed in 2eb08cd. You're right: startY is captured because it's the animation origin the loop interpolates from, but X isn't animated, so a captured startX re-asserted X every frame and would fight a horizontal scroll. Now reading window.scrollX live each tick (and the startX variable is gone). This also matches what #121 originally specified.

Comment thread components/site/Chrome.tsx Outdated
Comment on lines +78 to +79
el.setAttribute("tabindex", "-1");
el.focus({ preventScroll: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Unconditionally setting tabindex="-1" can remove elements that are already focusable (e.g., those with tabindex="0") from the natural tab order. It is safer to only apply it to non-focusable elements and clean it up on blur to avoid leaving unnecessary attributes in the DOM.

      if (el.tabIndex < 0) {
        el.setAttribute("tabindex", "-1");
        el.addEventListener("blur", () => el.removeAttribute("tabindex"), { once: true });
      }
      el.focus({ preventScroll: true });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Partially adopted in d6ffafb. Took the guard — tabindex is now set only if (el.tabIndex < 0), so an already-focusable target can't be pulled out of tab order.

Declined the blur-removal cleanup, on merit: every nav target here renders through components/ui/Frame.tsx as a plain non-focusable <section>, and leaving tabindex="-1" on a focus-target region is the recommended WAI-ARIA-APG pattern for post-navigation focus — not stale state. A per-navigation blur listener that adds/removes the attribute each time is churn without a user-facing benefit.

Comment thread components/site/MobileMenu.tsx Outdated
Comment on lines +192 to +193
targetEl.setAttribute("tabindex", "-1");
targetEl.focus({ preventScroll: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Unconditionally setting tabindex="-1" can remove elements that are already focusable from the natural tab order. Consider applying it only when necessary and cleaning up on blur to maintain a clean DOM state.

        if (targetEl.tabIndex < 0) {
          targetEl.setAttribute("tabindex", "-1");
          targetEl.addEventListener("blur", () => targetEl.removeAttribute("tabindex"), { once: true });
        }
        targetEl.focus({ preventScroll: true });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same resolution as the Chrome.tsx thread — guard adopted in d6ffafb (if (targetEl.tabIndex < 0)); blur-removal cleanup declined for the same reason (target is a plain <section> via Frame.tsx; tabindex="-1" left on a focus-target region is the standard WAI-ARIA-APG pattern, and per-nav blur listeners are unnecessary churn).

yigitdot and others added 2 commits May 19, 2026 09:46
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yigitdot
Copy link
Copy Markdown
Collaborator Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request improves accessibility by ensuring keyboard and screen reader focus is correctly moved to target sections after programmatic scrolls in Chrome.tsx and MobileMenu.tsx. It also refactors navigation click interception into a testable utility and upgrades the custom scroll animation to use quadratic easing. Feedback was provided to capture the initial horizontal scroll position at the start of the animation to prevent potential horizontal jumping during the scroll loop.

Comment thread lib/scroll.ts
// (not pinned to a captured value) so a horizontal scroll mid-
// animation isn't fought; this loop only drives Y.
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
window.scrollTo(window.scrollX, startY + distance * ease);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The implementation currently uses window.scrollX live in each frame, which can cause horizontal jumping if the layout shifts or the user interacts with the page during the animation. To ensure a natural feel and strictly preserve the horizontal scroll position as per repository rules, capture the initial scrollX value (startX) at the beginning of the animation and use that fixed value in the scroll loop.

Suggested change
window.scrollTo(window.scrollX, startY + distance * ease);
window.scrollTo(startX, startY + distance * ease);
References
  1. When implementing custom smooth-scrolling, use easing functions (such as quadratic ease-in-out) and preserve the horizontal scroll position (window.scrollX) to ensure a natural feel and prevent horizontal jumping.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Declining this — it reverses the change you correctly recommended in #122 (comment), which we accepted and shipped in 2eb08cd.

The reasoning here is inverted. Reading window.scrollX live each frame reads the current horizontal offset and writes it straight back — a no-op pass-through that never moves X and never fights the user. A captured startX is the opposite: it re-asserts the start offset on every one of ~36 frames, which is exactly the "horizontal jumping when the user interacts" this comment is trying to avoid. The cited rule ("preserve the horizontal scroll position (window.scrollX)") is satisfied by the shipped code.

Also concretely: startX was deleted in 2eb08cd, so the suggested window.scrollTo(startX, …) would not type-check.

Keeping window.scrollTo(window.scrollX, …) as-is. No code change.

yigitdot and others added 2 commits May 19, 2026 10:25
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yigitdot
Copy link
Copy Markdown
Collaborator Author

/review-pr follow-up pushed (11368d7, 18ea0b1): fixed a Critical regression where the mobile #119 focus-into-section was clobbered every close by the toggle-refocus effect (its setup runs after the focus-moving cleanup in the same commit) — now gated by a focusMovedToSectionRef flag so anchor-close keeps focus in the section while Escape/scrim/toggle/resize still return focus to the hamburger. Also comment-accuracy polish. Pre-existing NaN scrollMarginTop footgun (not introduced here) tracked separately as #124.

@yigitdot yigitdot merged commit c6a08ed into main May 19, 2026
7 checks passed
@yigitdot yigitdot deleted the fix/nav-followups-118 branch May 19, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants