Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 81 additions & 61 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dynamic from 'next/dynamic';
import Image from 'next/image';
import Link from 'next/link';
import {useCallback, useEffect, useState} from 'react';
import {useBodyScrollLock} from 'sentry-docs/hooks/useBodyScrollLock';
import SentryLogoSVG from 'sentry-docs/logos/sentry-logo-dark.svg';
import {Platform} from 'sentry-docs/types';

Expand Down Expand Up @@ -60,46 +61,70 @@ export function Header({
}, [])
);

// Close mobile search overlay on navigation
// Close mobile overlays on navigation
useEffect(() => {
setMobileSearchOpen(false);
setHomeMobileNavOpen(false);
}, [pathname]);

// Lock body scroll when mobile search overlay is open, close on resize to desktop or escape key
const closeSidebar = useCallback(() => {
const checkbox = document.getElementById(sidebarToggleId) as HTMLInputElement | null;
if (checkbox) {
checkbox.checked = false;
}
setSidebarOpen(false);
}, []);

useBodyScrollLock(mobileSearchOpen);
useBodyScrollLock(homeMobileNavOpen);

// Close the home mobile nav if the viewport is resized to desktop width
useEffect(() => {
if (mobileSearchOpen) {
document.body.style.overflow = 'hidden';
if (!homeMobileNavOpen) {
return undefined;
}
const handleResize = () => {
if (window.innerWidth >= 768) {
setHomeMobileNavOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [homeMobileNavOpen]);

// Close mobile search if viewport is resized to desktop width
const handleResize = () => {
if (window.innerWidth >= 768) {
setMobileSearchOpen(false);
}
};
// Close mobile search on resize to desktop or escape key
useEffect(() => {
if (!mobileSearchOpen) {
return undefined;
}

// Close mobile search on escape key (only if search input is empty)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
// Check if search input has a value - if so, let the Search component
// handle the escape key to clear the query first
const searchInput = document.querySelector(
'.mobile-search-overlay input[type="text"]'
) as HTMLInputElement | null;
if (!searchInput?.value) {
setMobileSearchOpen(false);
}
// Close mobile search if viewport is resized to desktop width
const handleResize = () => {
if (window.innerWidth >= 768) {
setMobileSearchOpen(false);
}
};

// Close mobile search on escape key (only if search input is empty)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
// Check if search input has a value - if so, let the Search component
// handle the escape key to clear the query first
const searchInput = document.querySelector(
'.mobile-search-overlay input[type="text"]'
) as HTMLInputElement | null;
if (!searchInput?.value) {
setMobileSearchOpen(false);
}
};
}
};

window.addEventListener('resize', handleResize);
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = '';
window.removeEventListener('resize', handleResize);
window.removeEventListener('keydown', handleKeyDown);
};
}
return undefined;
window.addEventListener('resize', handleResize);
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('keydown', handleKeyDown);
};
}, [mobileSearchOpen]);

// Track sidebar checkbox state for non-home pages
Expand All @@ -126,31 +151,28 @@ export function Header({
return () => checkbox.removeEventListener('change', handleChange);
}, [isHomePage]);

// Lock body scroll when sidebar is open on mobile (fixes iOS Safari touch scrolling)
useBodyScrollLock(sidebarOpen);

// Close sidebar if viewport is resized to desktop width
useEffect(() => {
if (sidebarOpen) {
document.body.style.overflow = 'hidden';
if (!sidebarOpen) {
return undefined;
}

// Close sidebar if viewport is resized to desktop width
const handleResize = () => {
if (window.innerWidth >= 768) {
const checkbox = document.getElementById(
sidebarToggleId
) as HTMLInputElement | null;
if (checkbox) {
checkbox.checked = false;
setSidebarOpen(false);
}
const handleResize = () => {
if (window.innerWidth >= 768) {
const checkbox = document.getElementById(
sidebarToggleId
) as HTMLInputElement | null;
if (checkbox) {
checkbox.checked = false;
setSidebarOpen(false);
}
};
}
};

window.addEventListener('resize', handleResize);
return () => {
document.body.style.overflow = '';
window.removeEventListener('resize', handleResize);
};
}
return undefined;
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [sidebarOpen]);

// Show header search if: not on home page, OR on home page but home search is scrolled out of view
Expand Down Expand Up @@ -283,14 +305,7 @@ export function Header({
onClick={() => {
setMobileSearchOpen(true);
setHomeMobileNavOpen(false);
// Close sidebar to prevent competing scroll locks
const checkbox = document.getElementById(
sidebarToggleId
) as HTMLInputElement | null;
if (checkbox) {
checkbox.checked = false;
setSidebarOpen(false);
}
closeSidebar();
}}
aria-label="Search"
>
Expand Down Expand Up @@ -350,7 +365,7 @@ export function Header({
{/* Home page mobile navigation overlay */}
{isHomePage && homeMobileNavOpen && (
<div
className="md:hidden fixed inset-0 bg-[var(--gray-1)] z-40"
className="md:hidden fixed inset-0 bg-[var(--gray-1)] z-40 overflow-y-auto overscroll-none"
style={{top: 'var(--header-height)'}}
>
<nav className="px-4 py-4 space-y-1">
Expand Down Expand Up @@ -404,6 +419,11 @@ export function Header({
searchPlatforms={searchPlatforms}
autoFocus
useStoredSearchPlatforms={useStoredSearchPlatforms}
// Release every overlay's lock before Kapa opens so it owns scrolling alone.
onAskAi={() => {
setMobileSearchOpen(false);
closeSidebar();
}}
/>
</div>
</div>
Expand Down
12 changes: 9 additions & 3 deletions src/components/search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const search = new SentryGlobalSearch(config);

type Props = {
autoFocus?: boolean;
/** Called before the Kapa modal opens, so a parent overlay can close itself first. */
onAskAi?: () => void;
path?: string;
searchPlatforms?: string[];
showChatBot?: boolean;
Expand All @@ -83,6 +85,7 @@ const SDK_AGNOSTIC_PATH_PREFIXES = [
export function Search({
path,
autoFocus,
onAskAi,
searchPlatforms = [],
useStoredSearchPlatforms = true,
}: Props) {
Expand Down Expand Up @@ -373,10 +376,13 @@ export function Search({
className={styles['sgs-ai-button']}
onClick={() => {
if (window.Kapa?.open) {
// close search results
setInputFocus(false);
// open kapa modal
window.Kapa.open({query, submit: true});
onAskAi?.();
// Open next frame, after the overlay's scroll lock is released
// on commit, so Kapa's lock and ours never overlap.
requestAnimationFrame(() => {
window.Kapa?.open({query, submit: true});
});
}
}}
>
Expand Down
83 changes: 52 additions & 31 deletions src/components/sidebar/MobileSidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import {ChevronDownIcon} from '@radix-ui/react-icons';
import Link from 'next/link';
import {usePathname} from 'next/navigation';
import {useEffect, useState} from 'react';
Expand All @@ -12,6 +13,11 @@ export function MobileSidebarNav({platforms = []}: {platforms?: Platform[]}) {

const isActive = (href: string) => pathname?.startsWith(href);

// Collapse all top-level sections behind a single "Menu" disclosure so the
// current section's page nav stays near the top on mobile. Collapsed by
// default; the active section is conveyed by the page nav heading below.
const [menuOpen, setMenuOpen] = useState(false);

// Compute the SDK link href - use stored platform URL if available
const [sdkLinkHref, setSdkLinkHref] = useState('/platforms/');

Expand Down Expand Up @@ -61,37 +67,52 @@ export function MobileSidebarNav({platforms = []}: {platforms?: Platform[]}) {

return (
<div className="md:hidden px-3 pb-3 border-b border-[var(--gray-a3)]">
{/* Main navigation sections - simple links that navigate to index pages */}
<nav className="space-y-1">
{mainSections.map(section =>
section.label === 'SDKs' ? (
<a
key={section.href}
href={sdkLinkHref}
onClick={handleSdkClick}
className={`block py-2 px-2 rounded text-sm font-medium transition-colors ${
isActive('/platforms/')
? 'text-[var(--accent-purple)] bg-[var(--accent-purple-light)]'
: 'text-[var(--gray-12)] hover:bg-[var(--gray-a3)]'
}`}
>
{section.label}
</a>
) : (
<Link
key={section.href}
href={section.href}
className={`block py-2 px-2 rounded text-sm font-medium transition-colors ${
isActive(section.href)
? 'text-[var(--accent-purple)] bg-[var(--accent-purple-light)]'
: 'text-[var(--gray-12)] hover:bg-[var(--gray-a3)]'
}`}
>
{section.label}
</Link>
)
)}
</nav>
<button
type="button"
onClick={() => setMenuOpen(open => !open)}
aria-expanded={menuOpen}
className="flex items-center justify-between w-full py-2 px-2 rounded text-sm font-medium text-[var(--gray-12)] hover:bg-[var(--gray-a3)] transition-colors"
>
{menuOpen ? 'Close Main Menu' : 'Open Main Menu'}
<ChevronDownIcon
className={`transition-transform ${menuOpen ? 'rotate-180' : ''}`}
width="18"
height="18"
/>
</button>
{menuOpen && (
// Main navigation sections - simple links that navigate to index pages
<nav className="space-y-1 mt-1">
{mainSections.map(section =>
section.label === 'SDKs' ? (
<a
key={section.href}
href={sdkLinkHref}
onClick={handleSdkClick}
className={`block py-2 px-2 rounded text-sm font-medium transition-colors ${
isActive('/platforms/')
? 'text-[var(--accent-purple)] bg-[var(--accent-purple-light)]'
: 'text-[var(--gray-12)] hover:bg-[var(--gray-a3)]'
}`}
>
{section.label}
</a>
) : (
<Link
key={section.href}
href={section.href}
className={`block py-2 px-2 rounded text-sm font-medium transition-colors ${
isActive(section.href)
? 'text-[var(--accent-purple)] bg-[var(--accent-purple-light)]'
: 'text-[var(--gray-12)] hover:bg-[var(--gray-a3)]'
}`}
>
{section.label}
</Link>
)
)}
</nav>
)}
</div>
);
}
22 changes: 21 additions & 1 deletion src/components/sidebar/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
@media only screen and (max-width: 767px) {
&:has(> #navbar-menu-toggle:checked) {
display: flex;
// On mobile the whole sidebar is a single scroll container, so the
// expanded section menu and the page nav scroll together instead of
// fighting over the fixed height with a clipped inner scroll region.
overflow-y: auto;
overscroll-behavior: contain;

& + :global(.main-content) {
margin-left: 0;
Expand All @@ -93,6 +98,14 @@
@media only screen and (min-width: 768px) {
display: block;
}

// On mobile the outer .sidebar is the single scroll container, so let .toc
// grow naturally instead of nesting its own scroll region.
@media only screen and (max-width: 767px) {
flex: 0 0 auto;
overflow: visible;
min-height: auto;
}
}

:global(.toc-item) {
Expand Down Expand Up @@ -155,6 +168,13 @@
overflow: auto;
min-height: 0; // Critical for flex children to allow shrinking and enable scrolling
-webkit-overflow-scrolling: touch; // Smooth momentum scrolling on iOS

// On mobile the outer .sidebar is the single scroll container (see .sidebar).
@media only screen and (max-width: 767px) {
flex: 0 0 auto;
overflow: visible;
min-height: auto;
}
}

.sidebar-external-links {
Expand Down Expand Up @@ -229,7 +249,7 @@
padding: 0.5rem 0 0.375rem;
margin-top: 0.5rem;
list-style: none;

&:first-child {
margin-top: 0;
}
Expand Down
Loading
Loading