1
0
mirror of synced 2025-12-22 03:16:52 -05:00
Files
docs/components/article/InArticlePicker.tsx
2023-05-08 15:33:13 +00:00

174 lines
5.5 KiB
TypeScript

import { useEffect, useState } from 'react'
import Cookies from 'js-cookie'
import { UnderlineNav } from '@primer/react'
import { sendEvent, EventType } from 'src/events/components/events'
import { useRouter } from 'next/router'
type Option = {
value: string
label: string
}
type Props = {
// Use this if not specified on the query string
defaultValue?: string
// Use this if not specified on the query string or no cookie
fallbackValue: string
cookieKey: string
queryStringKey: string
onValue: (value: string) => void
preferenceName: string
options: Option[]
ariaLabel: string
}
export const InArticlePicker = ({
defaultValue,
fallbackValue,
cookieKey,
queryStringKey,
onValue,
preferenceName,
options,
ariaLabel,
}: Props) => {
const router = useRouter()
const { query, locale } = router
const [currentValue, setCurrentValue] = useState('')
// Run on mount for client-side only features
useEffect(() => {
const raw = query[queryStringKey]
let value = ''
if (raw) {
if (Array.isArray(raw)) value = raw[0]
else value = raw
}
// Only pick it up from the possible query string if its value
// is a valid option.
const possibleValues = options.map((option) => option.value)
if (!value || !possibleValues.includes(value)) {
const cookieValue = Cookies.get(cookieKey)
if (defaultValue) {
value = defaultValue
} else if (cookieValue && possibleValues.includes(cookieValue)) {
value = cookieValue
} else {
value = fallbackValue
}
}
setCurrentValue(value)
}, [query, fallbackValue, defaultValue, options])
const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
useEffect(() => {
// This will make the hook run this callback on mount and on change.
// That's important because even though the user hasn't interacted
// and made an overriding choice, we still want to run this callback
// because the page might need to be corrected based on *a* choice
// independent of whether it's a change.
if (currentValue) {
onValue(currentValue)
}
}, [
currentValue,
// This is important because we can't otherwise rely on the firing
// of this effect on initial mount. It also needs to fire when the
// URL (i.e. route) changes.
// Don't use `router.asPath` because that contains the query string
// which we handle in the other useEffect above.
asPathRoot,
])
// This is exclusively for local development.
// If you're in local development, you have the <ClientSideRefresh>
// causing a XHR refresh of the content triggered by the Page Visibility
// API (implemented in the uswSWR hook). That means that on the pages that
// contain these `.extended-markdown` classes, any DOM changes we might
// have previously made are lost and started over.
useEffect(() => {
let mounted = true
const toggleVisibility = () => {
if (document.visibilityState === 'visible') {
// We don't need to track this timer, and possibly cancel it on
// dismount, because within the callback we use the `mounted`
// boolean which means we can know to do nothing if the parent
// component has been dismounted.
// The reason this is wrapped in a short timeout is because the
// React rendering might not actually have fully updated the DOM
// (from the XHR HTML it receives) so allow the DOM to refresh
// first before asking it to change. The number can be quite low
// (which is sufficient for human eyes) but must be at least
// in the lower hundreds of milliseconds.
setTimeout(() => {
if (mounted) {
onValue(currentValue)
}
}, 100)
}
}
if (process.env.NODE_ENV === 'development') {
document.addEventListener('visibilitychange', toggleVisibility)
}
return () => {
mounted = false
if (process.env.NODE_ENV === 'development') {
document.removeEventListener('visibilitychange', toggleVisibility)
}
}
}, [currentValue])
function onClickChoice(value: string) {
const params = new URLSearchParams(asPathQuery)
params.set(queryStringKey, value)
const newPath = `/${locale}${asPathRoot}?${params}`
router.push(newPath, undefined, { shallow: true, locale })
sendEvent({
type: EventType.preference,
preference_name: preferenceName,
preference_value: value,
})
Cookies.set(cookieKey, value, {
sameSite: 'strict',
secure: document.location.protocol !== 'http:',
expires: 365,
})
}
const sharedContainerProps = {
'data-testid': `${queryStringKey}-picker`,
'aria-label': ariaLabel,
[`data-default-${queryStringKey}`]: defaultValue || '',
className: 'mb-4',
}
const params = new URLSearchParams(asPathQuery)
return (
<UnderlineNav {...sharedContainerProps}>
{options.map((option) => {
params.set(queryStringKey, option.value)
const linkProps = {
[`data-${queryStringKey}`]: option.value,
}
return (
<UnderlineNav.Link
href={`?${params}`}
key={option.value}
selected={option.value === currentValue}
onClick={(event) => {
event.preventDefault()
onClickChoice(option.value)
}}
{...linkProps}
>
{option.label}
</UnderlineNav.Link>
)
})}
</UnderlineNav>
)
}