返回 marp
Mobile.tsx
根目录 / website / components / docs / layouts / Mobile.tsx
1 import { ThreeBarsIcon, XIcon } from '@primer/octicons-react'
2 import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
3 import classNames from 'classnames'
4 import FocusTrap from 'focus-trap-react'
5 import { useRouter } from 'next/router'
6 import { useCallback, useEffect, useRef, useState } from 'react'
7 import { Breadcrumb } from 'components/docs/Breadcrumb'
8 import { LayoutProps } from 'components/docs/Layout'
9 import { Navigation } from 'components/docs/Navigation'
10
11 const useOnPageLoad = (callback: () => void, immediate = false) => {
12 const router = useRouter()
13
14 useEffect(() => {
15 if (immediate) callback()
16 }, [callback]) // eslint-disable-line react-hooks/exhaustive-deps
17
18 useEffect(() => {
19 router.events.on('routeChangeComplete', callback)
20 return () => router.events.off('routeChangeComplete', callback)
21 }, [callback, router.events])
22 }
23
24 const useDrawer = (drawer?: HTMLElement) => {
25 const [open, setOpen] = useState(false)
26 const [active, setActive] = useState(false)
27 const activeTimer = useRef<number>()
28 const lastFocusedElement = useRef<HTMLElement | undefined>(undefined)
29
30 const handleOpen = useCallback((e?: React.SyntheticEvent<HTMLElement>) => {
31 setActive(true)
32 setOpen(true)
33
34 lastFocusedElement.current = e?.currentTarget
35 }, [])
36
37 const handleClose = useCallback(() => {
38 setActive(true)
39 setOpen(false)
40 }, [])
41
42 const toggle = useCallback(
43 (e?: React.SyntheticEvent<HTMLElement>) => {
44 open ? handleClose() : handleOpen(e)
45 },
46 [open, handleOpen, handleClose]
47 )
48
49 useOnPageLoad(handleClose)
50
51 useEffect(() => {
52 if (drawer && open) {
53 disableBodyScroll(drawer)
54 return () => enableBodyScroll(drawer)
55 }
56 }, [drawer, open])
57
58 useEffect(() => {
59 if (open) {
60 const escKeyListener = ({ key }: KeyboardEvent) => {
61 if (key === 'Escape') handleClose()
62 }
63
64 document.addEventListener('keydown', escKeyListener)
65 return () => document.removeEventListener('keydown', escKeyListener)
66 }
67 }, [handleClose, open])
68
69 useEffect(() => {
70 if (!open && lastFocusedElement.current) {
71 lastFocusedElement.current.focus()
72 }
73 }, [open])
74
75 useEffect(() => {
76 if (active) {
77 if (activeTimer.current !== undefined)
78 window.clearTimeout(activeTimer.current)
79
80 activeTimer.current = window.setTimeout(() => setActive(false), 300)
81 }
82 }, [active])
83
84 return { active, handleClose, handleOpen, toggle, open }
85 }
86
87 export const Mobile: React.FC<LayoutProps> = ({
88 children,
89 breadcrumbs,
90 manifest,
91 slug,
92 }) => {
93 const [drawer, setDrawer] = useState<HTMLElement | null>(null)
94 const { active, handleClose, toggle, open } = useDrawer(drawer ?? undefined)
95
96 const breadcrumbsContainer = useRef<HTMLDivElement | null>(null)
97
98 useOnPageLoad(
99 useCallback(() => {
100 breadcrumbsContainer.current?.scrollTo({
101 left: breadcrumbsContainer.current.scrollWidth,
102 behavior: 'instant' as any,
103 })
104 }, []),
105 true
106 )
107
108 return (
109 <>
110 <div
111 className={classNames('docs-backdrop', { active, open })}
112 onClick={handleClose}
113 aria-hidden
114 />
115 <FocusTrap
116 active={open}
117 focusTrapOptions={{
118 allowOutsideClick: () => true,
119 escapeDeactivates: false,
120 initialFocus: '#docs-nav',
121 fallbackFocus: '#docs-nav-toggle',
122 }}
123 >
124 <nav
125 id="docs-nav"
126 className={classNames({ active, open })}
127 ref={setDrawer}
128 tabIndex={-1}
129 inert={open ? undefined : ''}
130 >
131 <div className="p-8">
132 <p className="mb-6">
133 <button
134 className="docs-btn h-8 w-8"
135 onClick={handleClose}
136 aria-expanded={open}
137 >
138 <XIcon className="docs-btn-close-icon" />
139 <span className="sr-only">Close navigation</span>
140 </button>
141 </p>
142 <Navigation manifest={manifest} slug={slug} />
143 </div>
144 </nav>
145 </FocusTrap>
146 <nav className="docs-topbar">
147 <button
148 className="docs-btn relative z-20 m-1 h-8 w-8"
149 id="docs-nav-toggle"
150 type="button"
151 onClick={toggle}
152 aria-controls="docs-nav"
153 aria-expanded={open}
154 >
155 <ThreeBarsIcon className="docs-btn-open-drawer-icon" />
156 <span className="sr-only">Toggle navigation</span>
157 </button>
158 {breadcrumbs?.length && (
159 <div ref={breadcrumbsContainer} className="docs-breadcrumb-container">
160 <div className="docs-breadcrumb">
161 <Breadcrumb breadcrumbs={breadcrumbs} />
162 </div>
163 </div>
164 )}
165 </nav>
166 <article id="docs-article" className="container">
167 {children}
168 </article>
169 <style jsx global>{`
170 :root {
171 --anchor-margin: calc(var(--header-height) + 2.5rem);
172 }
173 `}</style>
174 <style jsx>{`
175 .docs-backdrop {
176 @apply pointer-events-none fixed inset-0 cursor-pointer opacity-0 transition-opacity;
177
178 -webkit-tap-highlight-color: transparent;
179 backdrop-filter: blur(2px);
180 background: rgba(255, 255, 255, 0.75);
181 z-index: 60;
182 }
183 .docs-backdrop.active {
184 @apply duration-300;
185 }
186 .docs-backdrop.open {
187 @apply pointer-events-auto opacity-100;
188 }
189
190 .docs-btn {
191 @apply appearance-none rounded text-gray-700;
192 }
193 .docs-btn:hover,
194 .docs-btn:focus {
195 @apply bg-gray-200 outline-none;
196 }
197 .docs-btn:hover:active {
198 @apply bg-gray-300 outline-none;
199 }
200 .docs-btn:focus-visible {
201 @apply ring-1 ring-white ring-offset-2;
202 }
203
204 .docs-btn :global(.docs-btn-close-icon) {
205 @apply h-8 w-8;
206 }
207 .docs-btn :global(.docs-btn-open-drawer-icon) {
208 @apply h-8 w-8 p-1;
209 }
210
211 #docs-nav {
212 @apply bg-background fixed inset-0 w-64 -translate-x-full transform overflow-hidden border-r outline-none;
213
214 transition-property: box-shadow, transform;
215 z-index: 60;
216 }
217 #docs-nav.active {
218 @apply duration-300;
219 }
220 #docs-nav.open {
221 @apply transform-none overflow-auto shadow-2xl;
222 }
223
224 .docs-topbar {
225 @apply fixed inset-0 top-16 z-50 flex h-10 items-stretch bg-white shadow-sm;
226 }
227 .docs-breadcrumb-container {
228 @apply flex flex-1 snap-x snap-proximity scroll-px-2 items-center overflow-x-auto;
229
230 mask: linear-gradient(to right, transparent, #000 0.5rem, #000)
231 no-repeat left top;
232 -ms-overflow-style: none;
233 scrollbar-width: none;
234 }
235 .docs-breadcrumb-container::-webkit-scrollbar {
236 display: none;
237 }
238 .docs-breadcrumb {
239 @apply relative mr-auto px-2;
240 @apply after:pointer-events-none after:absolute after:inset-0 after:mx-2 after:snap-end;
241 }
242 .docs-breadcrumb :global(li) {
243 @apply snap-start;
244 }
245
246 #docs-article {
247 @apply mx-auto p-6 pt-16;
248
249 --root-font-size: 0.9rem;
250 }
251 `}</style>
252 </>
253 )
254 }
255
255 lines Plain Text