Skip to content

Commit 5e07ca3

Browse files
authored
fix(docs): code tabs behavior (#12652)
* refactor: rename childs to childElements * fix: code tabs behavior - do not scroll to top when changing tabs - use components names as url param value to make slug parsing deterministic - update local storage value according url param - separate local storage state for all frameworks and base frameworks * fix: use encoded framework values for url param and persistence logic
1 parent ac39132 commit 5e07ca3

File tree

1 file changed

+123
-26
lines changed

1 file changed

+123
-26
lines changed

docs/components/Code/index.tsx

Lines changed: 123 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface ChildrenProps {
99
}
1010

1111
const AUTHJS_TAB_KEY = "authjs.codeTab.framework"
12+
const AUTHJS_TAB_KEY_ALL = "authjs.codeTab.framework.all"
1213

1314
Code.Next = NextCode
1415
Code.NextClient = NextClientCode
@@ -34,66 +35,162 @@ const allFrameworks = {
3435
[ExpressCode.name]: "Express",
3536
}
3637

37-
/**
38-
* Replace all non-alphabetic characters with a hyphen
39-
*
40-
* @param url - URL to parse
41-
* @returns - A string parsed from the URL
42-
*/
43-
const parseParams = (url: string): string => {
44-
let parsedUrl = url.toLowerCase().replaceAll(/[^a-zA-z]+/g, "-")
45-
return parsedUrl.endsWith("-") ? parsedUrl.slice(0, -1) : parsedUrl
38+
const findFrameworkKey = (
39+
text: string,
40+
frameworks: Record<string, string>
41+
): string | null => {
42+
const entry = Object.entries(frameworks).find(([_, value]) => value === text)
43+
return entry ? entry[0] : null
44+
}
45+
46+
const getIndexFrameworkFromUrl = (
47+
url: string,
48+
frameworks: Record<string, string>
49+
): number | null => {
50+
const params = new URLSearchParams(url)
51+
const paramValue = params.get("framework")
52+
if (!paramValue) return null
53+
54+
const decodedValue = decodeURI(paramValue)
55+
56+
const index = Object.values(frameworks).findIndex(
57+
(value) => value === decodedValue
58+
)
59+
return index === -1 ? null : index
60+
}
61+
62+
const getIndexFrameworkFromStorage = (
63+
frameworks: Record<string, string>,
64+
isAllFrameworks: boolean
65+
): number | null => {
66+
const storageKey = isAllFrameworks ? AUTHJS_TAB_KEY_ALL : AUTHJS_TAB_KEY
67+
const storedIndex = window.localStorage.getItem(storageKey)
68+
69+
if (!storedIndex) {
70+
return null
71+
}
72+
73+
return parseInt(storedIndex) % Object.keys(frameworks).length
74+
}
75+
76+
const updateFrameworkStorage = (
77+
frameworkURI: string,
78+
frameworks: Record<string, string>,
79+
isAllFrameworks: boolean
80+
): void => {
81+
const index = Object.values(frameworks).findIndex(
82+
(value) => encodeURI(value) === frameworkURI
83+
)
84+
if (index === -1) return
85+
86+
const storageKey = isAllFrameworks ? AUTHJS_TAB_KEY_ALL : AUTHJS_TAB_KEY
87+
window.localStorage.setItem(storageKey, index.toString())
88+
89+
// Update other storage if framework exists in other object
90+
const otherFrameworksValues = Object.values(
91+
isAllFrameworks ? baseFrameworks : allFrameworks
92+
)
93+
const otherStorageKey = isAllFrameworks ? AUTHJS_TAB_KEY : AUTHJS_TAB_KEY_ALL
94+
95+
const encodedFrameworksValues = otherFrameworksValues.map((value) =>
96+
encodeURI(value)
97+
)
98+
const existsInOther = encodedFrameworksValues.some(
99+
(encodedFramework) => encodedFramework === frameworkURI
100+
)
101+
if (existsInOther) {
102+
const otherIndex = otherFrameworksValues.findIndex(
103+
(encodedFramework) => encodedFramework === frameworkURI
104+
)
105+
window.localStorage.setItem(otherStorageKey, otherIndex.toString())
106+
// see https://github.com/shuding/nextra/blob/7ae958f02922e608151411042f658480b75164a6/packages/nextra/src/client/components/tabs/index.client.tsx#L106
107+
window.dispatchEvent(
108+
new StorageEvent("storage", {
109+
key: otherStorageKey,
110+
newValue: otherIndex.toString(),
111+
})
112+
)
113+
}
46114
}
47115

48116
export function Code({ children }: ChildrenProps) {
49117
const router = useRouter()
50118
const searchParams = useSearchParams()
51-
const childs = Children.toArray(children)
119+
const childElements = Children.toArray(children)
52120
const { project } = useThemeConfig()
53121

54-
const withNextJsPages = childs.some(
122+
const withNextJsPages = childElements.some(
55123
// @ts-expect-error: Hacky dynamic child wrangling
56124
(p) => p && p.type.name === NextClientCode.name
57125
)
58126

59127
const renderedFrameworks = withNextJsPages ? allFrameworks : baseFrameworks
60128

61-
const updateFrameworkStorage = (value: string): void => {
129+
const updateFrameworkInUrl = (frameworkURI: string): void => {
130+
if (frameworkURI === "undefined") return
131+
62132
const params = new URLSearchParams(searchParams?.toString())
63-
params.set("framework", value)
64-
router.push(`${router.pathname}?${params.toString()}`)
133+
params.set("framework", frameworkURI)
134+
135+
router.push(`${router.pathname}?${params.toString()}`, undefined, {
136+
scroll: false,
137+
})
65138
}
66139

67140
const handleClickFramework = (event: MouseEvent<HTMLDivElement>) => {
68141
if (!(event.target instanceof HTMLButtonElement)) return
69142
const { textContent } = event.target as unknown as HTMLDivElement
70-
updateFrameworkStorage(parseParams(textContent!))
143+
if (!textContent) return
144+
145+
const frameworkURI = encodeURI(textContent)
146+
updateFrameworkInUrl(frameworkURI)
147+
updateFrameworkStorage(frameworkURI, renderedFrameworks, withNextJsPages)
148+
149+
// Focus and scroll to maintain position when code blocks above are expanded
150+
const element = event.target as HTMLButtonElement
151+
const rect = element.getBoundingClientRect()
152+
requestAnimationFrame(() => {
153+
element.focus()
154+
window.scrollBy(0, element.getBoundingClientRect().top - rect.top)
155+
})
71156
}
72157

73158
useEffect(() => {
74-
const length = Object.keys(renderedFrameworks).length
75-
const getFrameworkStorage = window.localStorage.getItem(AUTHJS_TAB_KEY)
76-
const indexFramework = parseInt(getFrameworkStorage ?? "0") % length
77-
if (!getFrameworkStorage) {
78-
window.localStorage.setItem(AUTHJS_TAB_KEY, "0")
79-
}
80-
updateFrameworkStorage(
81-
parseParams(Object.values(renderedFrameworks)[indexFramework])
159+
const indexFrameworkFromStorage = getIndexFrameworkFromStorage(
160+
renderedFrameworks,
161+
withNextJsPages
82162
)
83-
}, [router.pathname, renderedFrameworks])
163+
const indexFrameworkFromUrl = getIndexFrameworkFromUrl(
164+
router.asPath,
165+
renderedFrameworks
166+
)
167+
168+
if (indexFrameworkFromStorage === null) {
169+
updateFrameworkStorage(
170+
encodeURI(renderedFrameworks[indexFrameworkFromUrl ?? 0]),
171+
renderedFrameworks,
172+
withNextJsPages
173+
)
174+
}
175+
176+
if (!indexFrameworkFromUrl) {
177+
const index = indexFrameworkFromStorage ?? 0
178+
updateFrameworkInUrl(encodeURI(renderedFrameworks[index]))
179+
}
180+
}, [router.pathname, renderedFrameworks, withNextJsPages])
84181

85182
return (
86183
<div
87184
className="[&_div[role='tablist']]:!pb-0"
88185
onClick={handleClickFramework}
89186
>
90187
<Tabs
91-
storageKey={AUTHJS_TAB_KEY}
188+
storageKey={withNextJsPages ? AUTHJS_TAB_KEY_ALL : AUTHJS_TAB_KEY}
92189
items={Object.values(renderedFrameworks)}
93190
>
94191
{Object.keys(renderedFrameworks).map((f) => {
95192
// @ts-expect-error: Hacky dynamic child wrangling
96-
const child = childs.find((c) => c?.type?.name === f)
193+
const child = childElements.find((c) => c?.type?.name === f)
97194

98195
// @ts-expect-error: Hacky dynamic child wrangling
99196
return Object.keys(child?.props ?? {}).length ? (

0 commit comments

Comments
 (0)