Skip to content

Commit beda919

Browse files
committed
fix touch for menu preview
1 parent f164d9c commit beda919

5 files changed

Lines changed: 102 additions & 40 deletions

File tree

media/brand.sketch

-47.5 KB
Binary file not shown.

src/components/DocsLayout.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GithubIcon } from '~/components/icons/GithubIcon'
44
import { DiscordIcon } from '~/components/icons/DiscordIcon'
55
import { Link, useMatches, useParams } from '@tanstack/react-router'
66
import { useLocalStorage } from '~/utils/useLocalStorage'
7+
import { useClickOutside } from '~/hooks/useClickOutside'
78
import { last } from '~/utils/utils'
89
import type { ConfigSchema, MenuItem } from '~/utils/config'
910
import { Framework } from '~/libraries'
@@ -92,10 +93,13 @@ function DocsMenuStrip({
9293
}
9394

9495
return (
95-
<div
96-
className="flex flex-col gap-2 py-2 px-2 cursor-pointer h-full w-full"
96+
<button
97+
type="button"
98+
className="flex flex-col gap-2 py-2 px-2 cursor-pointer h-full w-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-400/50"
9799
onPointerEnter={onHover}
100+
onFocus={onHover}
98101
onClick={onClick}
102+
aria-label="Open documentation menu"
99103
>
100104
{/* FrameworkSelect + VersionSelect icons */}
101105
<div className="flex flex-col gap-2 shrink-0">
@@ -140,7 +144,7 @@ function DocsMenuStrip({
140144
)
141145
})}
142146
</div>
143-
</div>
147+
</button>
144148
)
145149
}
146150

@@ -346,9 +350,9 @@ export function DocsLayout({
346350
{group?.label}
347351
</LabelComp>
348352
<div className="h-2" />
349-
<ul className="text-[.85em] leading-6 list-none">
353+
<ul className="text-[.85em] leading-snug list-none">
350354
{group?.children?.map((child, i) => {
351-
const linkClasses = `flex gap-2 items-center justify-between group px-2 py-0.5 rounded-lg hover:bg-gray-500/10 opacity-60 hover:opacity-100`
355+
const linkClasses = `flex gap-2 items-center justify-between group px-2 py-1.5 rounded-lg hover:bg-gray-500/10 opacity-60 hover:opacity-100`
352356

353357
return (
354358
<li key={i}>
@@ -386,7 +390,7 @@ export function DocsLayout({
386390
>
387391
<div
388392
className={twMerge(
389-
'overflow-auto w-full',
393+
'w-full',
390394
props.isActive
391395
? `font-bold text-transparent bg-clip-text bg-linear-to-r ${colorFrom} ${colorTo}`
392396
: '',
@@ -423,7 +427,7 @@ export function DocsLayout({
423427
Documentation
424428
</div>
425429
</summary>
426-
<div className="flex flex-col gap-4 p-4 whitespace-nowrap overflow-y-auto border-t border-gray-500/20 bg-white/20 text-lg dark:bg-black/20">
430+
<div className="flex flex-col gap-4 p-4 overflow-y-auto border-t border-gray-500/20 bg-white/20 text-lg dark:bg-black/20">
427431
<div className="flex flex-col gap-1">
428432
<FrameworkSelect libraryId={libraryId} />
429433
<VersionSelect libraryId={libraryId} />
@@ -439,6 +443,15 @@ export function DocsLayout({
439443
const [showLargeMenu, setShowLargeMenu] = React.useState(false)
440444
const leaveTimer = React.useRef<NodeJS.Timeout | undefined>(undefined)
441445

446+
// Close menu when clicking outside (only on sm-xl screens where it's an overlay)
447+
const expandedMenuRef = useClickOutside<HTMLDivElement>({
448+
enabled:
449+
showLargeMenu &&
450+
typeof window !== 'undefined' &&
451+
window.innerWidth < 1280,
452+
onClickOutside: () => setShowLargeMenu(false),
453+
})
454+
442455
const largeMenu = (
443456
<>
444457
{/* Collapsed strip - visible on sm to xl, hidden on xl+. Lower z-index so expanded menu covers it */}
@@ -467,20 +480,22 @@ export function DocsLayout({
467480
}}
468481
onClick={() => {
469482
if (window.innerWidth < 1280) {
470-
setShowLargeMenu((prev) => !prev)
483+
clearTimeout(leaveTimer.current)
484+
setShowLargeMenu(true)
471485
}
472486
}}
473487
/>
474488
</div>
475489

476490
{/* Expanded menu - always visible on xl+, toggleable overlay on sm-xl */}
477491
<div
492+
ref={expandedMenuRef}
478493
className={twMerge(
479494
'max-w-[250px] xl:max-w-[300px] 2xl:max-w-[400px]',
480-
'flex-col gap-4',
495+
'flex-col',
481496
'h-[calc(100dvh-var(--navbar-height))] top-[var(--navbar-height)]',
482497
'z-20 border-r border-gray-500/20',
483-
'transition-all duration-300 p-4',
498+
'transition-all duration-300',
484499
// Hidden on smallest screens, flex on sm+
485500
'hidden sm:flex',
486501
// On sm to xl: fixed overlay that slides in from left-0 (covers the strip)
@@ -505,12 +520,14 @@ export function DocsLayout({
505520
}
506521
}}
507522
>
508-
<div className="flex flex-col gap-1">
509-
<FrameworkSelect libraryId={libraryId} />
510-
<VersionSelect libraryId={libraryId} />
511-
</div>
512-
<div className="flex-1 flex flex-col gap-4 whitespace-nowrap overflow-y-auto text-base pb-4">
513-
{menuItems}
523+
<div className="flex-1 flex flex-col overflow-y-auto">
524+
<div className="flex flex-col gap-1 p-4">
525+
<FrameworkSelect libraryId={libraryId} />
526+
<VersionSelect libraryId={libraryId} />
527+
</div>
528+
<div className="flex-1 flex flex-col gap-4 text-base px-4 pt-0 pb-4">
529+
{menuItems}
530+
</div>
514531
</div>
515532
</div>
516533
</>

src/components/MarkdownContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function MarkdownContent({
8181
ref={containerRef}
8282
className={twMerge(
8383
'prose prose-gray dark:prose-invert max-w-none',
84-
'[font-size:14px]',
84+
'[font-size:16px]',
8585
'styled-markdown-content',
8686
proseClassName,
8787
)}

src/components/Navbar.tsx

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from '~/components/AuthComponents'
2929
import { libraries } from '~/libraries'
3030
import { useCapabilities } from '~/hooks/useCapabilities'
31+
import { useClickOutside } from '~/hooks/useClickOutside'
3132
import { GithubIcon } from '~/components/icons/GithubIcon'
3233
import { DiscordIcon } from '~/components/icons/DiscordIcon'
3334
import { InstagramIcon } from '~/components/icons/InstagramIcon'
@@ -67,27 +68,16 @@ export function Navbar({ children }: { children: React.ReactNode }) {
6768
}, [])
6869

6970
const [showMenu, setShowMenu] = React.useState(false)
70-
const smallMenuRef = React.useRef<HTMLDivElement>(null)
7171

7272
const toggleMenu = () => {
7373
setShowMenu((prev) => !prev)
7474
}
7575

7676
// Close mobile menu when clicking outside
77-
React.useEffect(() => {
78-
if (!showMenu) return
79-
80-
const handleClickOutside = (event: MouseEvent) => {
81-
const target = event.target as HTMLElement
82-
// Check if click is outside the small menu
83-
if (smallMenuRef.current && !smallMenuRef.current.contains(target)) {
84-
setShowMenu(false)
85-
}
86-
}
87-
88-
document.addEventListener('click', handleClickOutside)
89-
return () => document.removeEventListener('click', handleClickOutside)
90-
}, [showMenu])
77+
const smallMenuRef = useClickOutside<HTMLDivElement>({
78+
enabled: showMenu,
79+
onClickOutside: () => setShowMenu(false),
80+
})
9181

9282
const loginButton = (
9383
<>
@@ -176,17 +166,13 @@ export function Navbar({ children }: { children: React.ReactNode }) {
176166
)
177167

178168
const navbar = (
179-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
180169
<div
181170
className={twMerge(
182171
'w-full p-2 fixed top-0 z-[100] bg-white/90 dark:bg-black/90 backdrop-blur-lg',
183172
'flex items-center justify-between gap-4',
184173
'border-b border-gray-500/20',
185174
)}
186175
ref={containerRef}
187-
onClick={() => {
188-
setShowMenu(false)
189-
}}
190176
>
191177
<div className="flex items-center gap-4">
192178
<div className="flex items-center gap-2 font-black text-xl uppercase">
@@ -202,10 +188,7 @@ export function Navbar({ children }: { children: React.ReactNode }) {
202188
? 'lg:w-9 lg:opacity-100 lg:translate-x-0'
203189
: 'lg:w-0 lg:opacity-0 lg:-translate-x-full',
204190
)}
205-
onClick={(e) => {
206-
e.stopPropagation()
207-
toggleMenu()
208-
}}
191+
onClick={toggleMenu}
209192
onPointerEnter={() => {
210193
if (window.innerWidth < 1024) {
211194
return

src/hooks/useClickOutside.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as React from 'react'
2+
3+
type UseClickOutsideOptions = {
4+
/** Whether the click-outside detection is active */
5+
enabled: boolean
6+
/** Callback when a click outside is detected */
7+
onClickOutside: () => void
8+
/** Whether to also close on Escape key press (default: true) */
9+
closeOnEscape?: boolean
10+
}
11+
12+
/**
13+
* Hook to detect clicks outside of a referenced element.
14+
* Returns a ref to attach to the element you want to detect clicks outside of.
15+
*
16+
* @example
17+
* ```tsx
18+
* const menuRef = useClickOutside({
19+
* enabled: isMenuOpen,
20+
* onClickOutside: () => setIsMenuOpen(false),
21+
* })
22+
*
23+
* return <div ref={menuRef}>Menu content</div>
24+
* ```
25+
*/
26+
export function useClickOutside<T extends HTMLElement = HTMLElement>({
27+
enabled,
28+
onClickOutside,
29+
closeOnEscape = true,
30+
}: UseClickOutsideOptions): React.RefObject<T | null> {
31+
const ref = React.useRef<T>(null)
32+
33+
React.useEffect(() => {
34+
if (!enabled) return
35+
36+
const handleClickOutside = (event: MouseEvent) => {
37+
if (ref.current && !ref.current.contains(event.target as Node)) {
38+
onClickOutside()
39+
}
40+
}
41+
42+
const handleEscape = (event: KeyboardEvent) => {
43+
if (event.key === 'Escape') {
44+
onClickOutside()
45+
}
46+
}
47+
48+
document.addEventListener('mousedown', handleClickOutside)
49+
if (closeOnEscape) {
50+
document.addEventListener('keydown', handleEscape)
51+
}
52+
53+
return () => {
54+
document.removeEventListener('mousedown', handleClickOutside)
55+
if (closeOnEscape) {
56+
document.removeEventListener('keydown', handleEscape)
57+
}
58+
}
59+
}, [enabled, onClickOutside, closeOnEscape])
60+
61+
return ref
62+
}

0 commit comments

Comments
 (0)