Skip to content

Commit 4d7ffd3

Browse files
authored
feat: more aria (#329)
* menu and search dialogs * search tag * -MenuContext * cmdk * cmdk overflow * arrow keys nav
1 parent f3f86b7 commit 4d7ffd3

File tree

11 files changed

+865
-229
lines changed

11 files changed

+865
-229
lines changed

package-lock.json

Lines changed: 698 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@
3232
"dependencies": {
3333
"@codesandbox/sandpack-react": "^2.19.0",
3434
"@radix-ui/react-collapsible": "^1.1.0",
35+
"@radix-ui/react-dialog": "^1.1.1",
36+
"@radix-ui/react-visually-hidden": "^1.1.0",
3537
"@tailwindcss/aspect-ratio": "^0.4.2",
3638
"@tailwindcss/typography": "^0.5.14",
3739
"clsx": "^2.1.1",
40+
"cmdk": "^1.0.0",
3841
"gray-matter": "^4.0.3",
3942
"image-size": "^1.1.1",
4043
"match-sorter": "^6.3.4",

src/app/[...slug]/Burger.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,11 @@
33
import Icon from '@/components/Icon'
44
import cn from '@/lib/cn'
55
import { ComponentProps } from 'react'
6-
import { useMenu } from './MenuContext'
7-
8-
export function Burger({ className }: ComponentProps<'button'>) {
9-
const [menuOpen, setMenuOpen] = useMenu()
106

7+
export function Burger({ opened, className }: { opened: boolean } & ComponentProps<'span'>) {
118
return (
12-
<button
13-
className={cn(className, 'flex size-9 items-center justify-center')}
14-
type="button"
15-
aria-label="Menu"
16-
onClick={() => setMenuOpen(!menuOpen)}
17-
>
18-
{menuOpen ? <Icon icon="close" /> : <Icon icon="menu" />}
19-
</button>
9+
<span className={cn(className, 'flex size-9 items-center justify-center')} aria-label="Menu">
10+
{opened ? <Icon icon="close" /> : <Icon icon="menu" />}
11+
</span>
2012
)
2113
}

src/app/[...slug]/Menu.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
'use client'
22

3-
import { Nav } from '@/components/Nav'
4-
import cn from '@/lib/cn'
5-
import { ComponentProps, ElementRef, useEffect, useRef } from 'react'
6-
import { useDocs } from './DocsContext'
7-
import { useMenu } from './MenuContext'
3+
import * as Dialog from '@radix-ui/react-dialog'
4+
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
5+
import { ComponentProps, useState } from 'react'
86

9-
export function Menu({ className, asPath }: ComponentProps<'dialog'> & { asPath: string }) {
10-
const { doc, docs } = useDocs()
7+
import { Burger } from './Burger'
118

12-
const [opened, setOpened] = useMenu()
13-
const dialogRef = useRef<ElementRef<'dialog'>>(null)
14-
15-
useEffect(() => {
16-
if (opened) {
17-
dialogRef.current?.show()
18-
} else {
19-
dialogRef.current?.close()
20-
}
21-
}, [opened])
9+
export function Menu({ children, ...props }: ComponentProps<typeof Dialog.Content>) {
10+
const [opened, setOpened] = useState(false)
2211

2312
return (
24-
<dialog ref={dialogRef} className={cn(className, 'bg-surface-dim/95 backdrop-blur-xl')}>
25-
<Nav docs={docs} asPath={asPath} collapsible={false} />
26-
</dialog>
13+
<Dialog.Root open={opened} onOpenChange={setOpened}>
14+
<Dialog.Trigger>
15+
<Burger opened={opened} className="lg:hidden" />
16+
</Dialog.Trigger>
17+
<Dialog.Content {...props}>
18+
<VisuallyHidden.Root>
19+
<Dialog.Title>Menu</Dialog.Title>
20+
</VisuallyHidden.Root>
21+
{children}
22+
</Dialog.Content>
23+
</Dialog.Root>
2724
)
2825
}

src/app/[...slug]/MenuContext.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/app/[...slug]/layout.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import { getData } from '@/utils/docs'
99
import Link from 'next/link'
1010
import { PiDiscordLogoLight } from 'react-icons/pi'
1111
import { VscGithubAlt } from 'react-icons/vsc'
12-
import { Burger } from './Burger'
1312
import { DocsContext } from './DocsContext'
1413
import { Menu } from './Menu'
15-
import { MenuContext } from './MenuContext'
1614

1715
export type Props = {
1816
params: { slug: string[] }
@@ -52,7 +50,7 @@ export default async function Layoutt({ params, children }: Props) {
5250
</span>
5351
</div>
5452

55-
<Search />
53+
<Search className="grow" />
5654

5755
<div className="flex">
5856
{[
@@ -73,7 +71,9 @@ export default async function Layoutt({ params, children }: Props) {
7371
))}
7472
{/* <ToggleTheme className="hidden size-9 items-center justify-center sm:flex" /> */}
7573

76-
<Burger className="lg:hidden" />
74+
<Menu className="z-100 bg-surface absolute inset-0 top-[--header-height] h-[calc(100dvh-var(--header-height))] w-full overflow-auto lg:hidden">
75+
<Nav docs={docs} asPath={asPath} collapsible={false} />
76+
</Menu>
7777
</div>
7878
</div>
7979
)
@@ -158,25 +158,19 @@ export default async function Layoutt({ params, children }: Props) {
158158
return (
159159
<>
160160
<DocsContext value={{ docs, doc }}>
161-
<MenuContext>
162-
<Layout className="[--side-w:theme(spacing.72)]">
163-
<LayoutHeader className="z-10 border-b border-outline-variant/50 bg-surface/95 backdrop-blur-xl">
164-
{header}
165-
<Menu
166-
asPath={asPath}
167-
className="z-100 left-0 top-[--header-height] h-[calc(100dvh-var(--header-height))] w-full overflow-auto lg:hidden"
168-
/>
169-
</LayoutHeader>
170-
<LayoutContent className="lg:mr-[--rgrid-m] xl:mr-0">
171-
<article className="post-container">
172-
{children}
173-
{footer}
174-
</article>
175-
</LayoutContent>
176-
<LayoutNav className="pt-8">{nav}</LayoutNav>
177-
<LayoutAside className="pt-8">{toc}</LayoutAside>
178-
</Layout>
179-
</MenuContext>
161+
<Layout className="[--side-w:theme(spacing.72)]">
162+
<LayoutHeader className="z-10 border-b border-outline-variant/50 bg-surface/95 backdrop-blur-xl">
163+
{header}
164+
</LayoutHeader>
165+
<LayoutContent className="lg:mr-[--rgrid-m] xl:mr-0">
166+
<article className="post-container">
167+
{children}
168+
{footer}
169+
</article>
170+
</LayoutContent>
171+
<LayoutNav className="pt-8">{nav}</LayoutNav>
172+
<LayoutAside className="pt-8">{toc}</LayoutAside>
173+
</Layout>
180174
</DocsContext>
181175
</>
182176
)

src/components/Nav/NavCategory.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ function NavItem({
6363
<Link
6464
{...props}
6565
className={cn(
66-
'block cursor-pointer p-3 pl-8 focus:outline-none',
67-
active ? 'interactive-bg-primary-container' : 'interactive-bg-surface',
66+
'block cursor-pointer p-3 pl-8',
67+
active ? 'bg-primary-container' : 'interactive-bg-surface',
6868
className,
6969
)}
7070
>

src/components/Search/SearchItem.tsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Icon from '@/components/Icon'
2+
import cn from '@/lib/cn'
23
import { highlight } from '@/utils/text'
34
import Link from 'next/link'
5+
import { ComponentProps } from 'react'
46
import sanitizeHtml from 'sanitize-html'
57

68
export interface SearchResult {
@@ -24,38 +26,40 @@ function sanitizeAllHtmlButMark(str: string) {
2426
})
2527
}
2628

27-
function SearchItem({ search, result }: SearchItemProps) {
29+
function SearchItem({
30+
className,
31+
search,
32+
result,
33+
}: Omit<ComponentProps<typeof Link>, 'href'> & SearchItemProps) {
2834
return (
2935
<Link
3036
href={result.url}
31-
className="search-item block no-underline outline-none"
37+
className={cn(className, 'block no-underline')}
3238
target={result.url.startsWith('http') ? '_blank' : undefined}
3339
>
34-
<li className="px-2 py-1">
35-
<div className="interactive-bg-surface-container-high flex items-center justify-between rounded-md p-4 py-5 transition-colors">
36-
<div className="break-all pr-3">
37-
<div className="block pb-1 text-xs text-on-surface-variant/50">{result.label}</div>
40+
<div className="flex items-center justify-between rounded-md p-4 py-5">
41+
<div className="break-all pr-3">
42+
<div className="block pb-1 text-xs text-on-surface-variant/50">{result.label}</div>
43+
<span
44+
dangerouslySetInnerHTML={{
45+
__html: highlight(sanitizeAllHtmlButMark(result.title), search),
46+
}}
47+
/>
48+
<div className="block pt-2 text-sm text-on-surface-variant/50">
3849
<span
3950
dangerouslySetInnerHTML={{
40-
__html: highlight(sanitizeAllHtmlButMark(result.title), search),
51+
__html: highlight(sanitizeAllHtmlButMark(result.content), search),
4152
}}
4253
/>
43-
<div className="block pt-2 text-sm text-on-surface-variant/50">
44-
<span
45-
dangerouslySetInnerHTML={{
46-
__html: highlight(sanitizeAllHtmlButMark(result.content), search),
47-
}}
48-
/>
49-
</div>
5054
</div>
51-
{result.image ? (
52-
// eslint-disable-next-line @next/next/no-img-element
53-
<img className="max-w-[40%] rounded" src={result.image} alt={result.title} />
54-
) : (
55-
<Icon icon="enter" />
56-
)}
5755
</div>
58-
</li>
56+
{result.image ? (
57+
// eslint-disable-next-line @next/next/no-img-element
58+
<img className="max-w-[40%] rounded" src={result.image} alt={result.title} />
59+
) : (
60+
<Icon icon="enter" />
61+
)}
62+
</div>
5963
</Link>
6064
)
6165
}

src/components/Search/SearchModal.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/components/Search/SearchModalContainer.tsx

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,16 @@ import * as React from 'react'
33

44
import { useDocs } from '@/app/[...slug]/DocsContext'
55

6-
import { escape } from '@/utils/text'
6+
import cn from '@/lib/cn'
7+
import { Command } from 'cmdk'
8+
import { useRouter } from 'next/navigation'
9+
import { ComponentProps } from 'react'
710
import type { SearchResult } from './SearchItem'
8-
import SearchModal from './SearchModal'
11+
import SearchItem from './SearchItem'
912

10-
export interface SearchModalContainerProps {
11-
onClose: React.MouseEventHandler<HTMLButtonElement>
12-
}
13-
14-
export const SearchModalContainer = ({ onClose }: SearchModalContainerProps) => {
15-
// const router = useRouter()
16-
// const boxes = useCSB()
13+
export const SearchModalContainer = ({ className }: ComponentProps<'search'>) => {
14+
const router = useRouter()
1715
const { docs } = useDocs()
18-
// console.log('docs', docs)
19-
// const [lib] = router.query.slug as string[]
2016
const [query, setQuery] = React.useState('')
2117
const deferredQuery = React.useDeferredValue(query)
2218
const [results, setResults] = React.useState<SearchResult[]>([])
@@ -62,11 +58,43 @@ export const SearchModalContainer = ({ onClose }: SearchModalContainerProps) =>
6258
}, [docs, deferredQuery])
6359

6460
return (
65-
<SearchModal
66-
search={query}
67-
results={results}
68-
onClose={onClose}
69-
onChange={(e) => setQuery(escape(e.target.value))}
70-
/>
61+
<search
62+
className={cn(
63+
'[--Search-Input-height:theme(spacing.16)]',
64+
'mt-[--Search-Input-top]',
65+
className,
66+
)}
67+
>
68+
<Command shouldFilter={false} className="">
69+
<Command.Input
70+
name="search"
71+
id="search"
72+
className="bg-surface-container block h-[--Search-Input-height] w-full rounded-md px-4 pl-10 sm:text-sm"
73+
placeholder="Search the docs"
74+
value={query}
75+
autoFocus
76+
onValueChange={(value) => setQuery(value)}
77+
/>
78+
79+
<Command.List>
80+
{results.length > 0 && (
81+
<div className="bg-surface-container mt-1 flex max-h-[calc((100dvh-var(--Search-Input-top)-1.5rem)-var(--Search-Input-height))] flex-col gap-1 overflow-auto rounded-md p-1">
82+
{results.map((result, index) => {
83+
return (
84+
<Command.Item
85+
key={`search-item-${index}`}
86+
value={result.url}
87+
onSelect={router.push}
88+
className="rounded-md transition-colors data-[selected=true]:bg-surface-container-high"
89+
>
90+
<SearchItem search={query} result={result} />
91+
</Command.Item>
92+
)
93+
})}
94+
</div>
95+
)}
96+
</Command.List>
97+
</Command>
98+
</search>
7199
)
72100
}

0 commit comments

Comments
 (0)