Files
supabase/apps/docs/features/docs/Reference.ui.client.tsx
T
Devanshu SharmaandCharis 0674343912 Fix(docs): prevent SecurityError from rapid history.replaceState call… (#38672)
* Fix(docs): prevent SecurityError from rapid history.replaceState calls during fast scrolling

* url now chases the scroll

* fixing merge conflicts

* refactor: minor changes to safeHistoryReplaceState

---------

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
2025-09-18 21:00:10 +00:00

118 lines
3.6 KiB
TypeScript

'use client'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { safeHistoryReplaceState } from '~/lib/historyUtils'
import {
cn,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { ReferenceContentInitiallyScrolledContext } from '~/features/docs/Reference.navigation.client'
import { type IApiEndPoint } from './Reference.api.utils'
import { API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES } from './Reference.ui.shared'
/**
* Wrap a reference section with client-side functionality:
*
* - Intersection observer to auto-update the URL when the user scrolls the page
* - An ID to scroll to programmatically. This is on the entire section rather
* than the heading to avoid problems with scroll-to position when the heading
* is sticky.
*/
export function ReferenceSectionWrapper({
id,
link,
children,
className,
}: PropsWithChildren<{ id: string; link: string; className?: string }> &
HTMLAttributes<HTMLElement>) {
const initialScrollHappened = useContext(ReferenceContentInitiallyScrolledContext)
const { ref } = useInView({
threshold: 0,
rootMargin: '-10% 0% -50% 0%',
onChange: (inView) => {
if (
inView &&
initialScrollHappened &&
window.scrollY > 0 /* Don't update on first navigation to introduction */
) {
safeHistoryReplaceState(link)
}
},
})
return (
<section
ref={ref}
id={id}
className={cn('scroll-mt-[calc(var(--header-height)+4rem)]', className)}
>
{children}
</section>
)
}
export function ApiOperationBodySchemeSelector({
requestBody,
className,
}: {
requestBody: IApiEndPoint['requestBody']
className?: string
}) {
const availableSchemes = Object.keys(requestBody?.content || {}) as Array<
'application/json' | 'application/x-www-form-urlencoded'
>
const [selectedScheme, setSelectedScheme] = useState(availableSchemes[0])
const containerRef = useRef<HTMLDivElement>(null)
const allSchemeDetails = useRef<HTMLUListElement[]>([])
useEffect(() => {
const elements = containerRef.current?.querySelectorAll(
`[${API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY}]`
)
allSchemeDetails.current = elements ? (Array.from(elements) as HTMLUListElement[]) : []
}, [])
useEffect(() => {
allSchemeDetails.current?.forEach((schemeDetails) => {
schemeDetails.hidden =
schemeDetails.getAttribute(API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY) !==
selectedScheme
})
}, [selectedScheme])
return (
<div ref={containerRef} className={cn('flex items-center justify-between gap-2', className)}>
<h3 className="text-base text-foreground">Body</h3>
<Select_Shadcn_
value={selectedScheme}
onValueChange={(value) =>
setSelectedScheme(value as 'application/json' | 'application/x-www-form-urlencoded')
}
>
<SelectTrigger_Shadcn_ className="w-48 [&>span]:w-full [&>span]:truncate">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{availableSchemes.map((scheme) => (
<SelectItem_Shadcn_ key={scheme} value={scheme}>
{scheme}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
)
}