1
0
mirror of synced 2025-12-22 19:34:15 -05:00

Persist tab state in query params for linking (#31499)

This commit is contained in:
Evan Bonsignori
2022-10-12 04:43:31 -07:00
committed by GitHub
parent db77694065
commit d5cf8700ab
4 changed files with 95 additions and 45 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { SubNav, TabNav, UnderlineNav } from '@primer/react' import { SubNav, TabNav, UnderlineNav } from '@primer/react'
import { sendEvent, EventType } from 'components/lib/events' import { sendEvent, EventType } from 'components/lib/events'
@@ -7,6 +7,7 @@ import { useRouter } from 'next/router'
import { useArticleContext } from 'components/context/ArticleContext' import { useArticleContext } from 'components/context/ArticleContext'
import { parseUserAgent } from 'components/lib/user-agent' import { parseUserAgent } from 'components/lib/user-agent'
const platformQueryKey = 'platform'
const platforms = [ const platforms = [
{ id: 'mac', label: 'Mac' }, { id: 'mac', label: 'Mac' },
{ id: 'windows', label: 'Windows' }, { id: 'windows', label: 'Windows' },
@@ -49,9 +50,10 @@ type Props = {
variant?: 'subnav' | 'tabnav' | 'underlinenav' variant?: 'subnav' | 'tabnav' | 'underlinenav'
} }
export const PlatformPicker = ({ variant = 'subnav' }: Props) => { export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
const router = useRouter()
const { query, asPath } = router
const { defaultPlatform, detectedPlatforms } = useArticleContext() const { defaultPlatform, detectedPlatforms } = useArticleContext()
const [currentPlatform, setCurrentPlatform] = useState(defaultPlatform || '') const [currentPlatform, setCurrentPlatform] = useState(defaultPlatform || '')
const { asPath } = useRouter()
// Run on mount for client-side only features // Run on mount for client-side only features
useEffect(() => { useEffect(() => {
@@ -60,7 +62,15 @@ export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
userAgent = 'mac' userAgent = 'mac'
} }
const platform = defaultPlatform || Cookies.get('osPreferred') || userAgent || 'linux' // If it's a valid platform option, set platform from query param
let platform =
query[platformQueryKey] && Array.isArray(query[platformQueryKey])
? query[platformQueryKey][0]
: query[platformQueryKey] || ''
if (!platform || !platforms.some((platform) => platform.id === query.platform)) {
platform = defaultPlatform || Cookies.get('osPreferred') || userAgent || 'linux'
}
setCurrentPlatform(platform) setCurrentPlatform(platform)
// always trigger this on initial render. if the default doesn't change the other useEffect won't fire // always trigger this on initial render. if the default doesn't change the other useEffect won't fire
@@ -75,11 +85,13 @@ export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
} }
}, [currentPlatform, detectedPlatforms.join(',')]) }, [currentPlatform, detectedPlatforms.join(',')])
const onClickPlatform = (platform: string) => { const onClickPlatform = useCallback(
setCurrentPlatform(platform) (platform: string) => {
// Set platform in query param without altering other query params
// imperatively modify the article content const [pathRoot, pathQuery = ''] = asPath.split('?')
showPlatformSpecificContent(platform) const params = new URLSearchParams(pathQuery)
params.set(platformQueryKey, platform)
router.push({ pathname: pathRoot, query: params.toString() }, undefined, { shallow: true })
sendEvent({ sendEvent({
type: EventType.preference, type: EventType.preference,
@@ -89,9 +101,12 @@ export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
Cookies.set('osPreferred', platform, { Cookies.set('osPreferred', platform, {
sameSite: 'strict', sameSite: 'strict',
secure: true, secure: document.location.protocol !== 'http:',
expires: 365,
}) })
} },
[asPath]
)
// only show platforms that are in the current article // only show platforms that are in the current article
const platformOptions = platforms.filter((platform) => detectedPlatforms.includes(platform.id)) const platformOptions = platforms.filter((platform) => detectedPlatforms.includes(platform.id))
@@ -128,15 +143,20 @@ export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
} }
if (variant === 'underlinenav') { if (variant === 'underlinenav') {
const [, pathQuery = ''] = asPath.split('?')
const params = new URLSearchParams(pathQuery)
return ( return (
<UnderlineNav {...sharedContainerProps}> <UnderlineNav {...sharedContainerProps}>
{platformOptions.map((option) => { {platformOptions.map((option) => {
params.set(platformQueryKey, option.id)
return ( return (
<UnderlineNav.Link <UnderlineNav.Link
href={`?${params.toString()}`}
key={option.id} key={option.id}
data-platform={option.id} data-platform={option.id}
selected={option.id === currentPlatform} selected={option.id === currentPlatform}
onClick={() => { onClick={(event) => {
event.preventDefault()
onClickPlatform(option.id) onClickPlatform(option.id)
}} }}
> >

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { UnderlineNav } from '@primer/react' import { UnderlineNav } from '@primer/react'
@@ -47,11 +47,13 @@ function getDefaultTool(defaultTool: string | undefined, detectedTools: Array<st
return detectedTools[0] return detectedTools[0]
} }
const toolQueryKey = 'tool'
type Props = { type Props = {
variant?: 'subnav' | 'tabnav' | 'underlinenav' variant?: 'subnav' | 'tabnav' | 'underlinenav'
} }
export const ToolPicker = ({ variant = 'subnav' }: Props) => { export const ToolPicker = ({ variant = 'subnav' }: Props) => {
const { asPath } = useRouter() const router = useRouter()
const { asPath, query } = router
// allTools comes from the ArticleContext which contains the list of tools available // allTools comes from the ArticleContext which contains the list of tools available
const { defaultTool, detectedTools, allTools } = useArticleContext() const { defaultTool, detectedTools, allTools } = useArticleContext()
const [currentTool, setCurrentTool] = useState(getDefaultTool(defaultTool, detectedTools)) const [currentTool, setCurrentTool] = useState(getDefaultTool(defaultTool, detectedTools))
@@ -73,38 +75,66 @@ export const ToolPicker = ({ variant = 'subnav' }: Props) => {
} }
}, []) }, [])
// Whenever the currentTool is changed, update the article content // Whenever the currentTool is changed, update the article content or selected tool from query param
useEffect(() => { useEffect(() => {
preserveAnchorNodePosition(document, () => { preserveAnchorNodePosition(document, () => {
showToolSpecificContent(currentTool, Object.keys(allTools)) showToolSpecificContent(currentTool, Object.keys(allTools))
}) })
// If tool from query is a valid option, use it
const tool =
query[toolQueryKey] && Array.isArray(query[toolQueryKey])
? query[toolQueryKey][0]
: query[toolQueryKey] || ''
if (tool && detectedTools.includes(tool)) {
setCurrentTool(tool)
}
}, [currentTool, asPath]) }, [currentTool, asPath])
function onClickTool(tool: string) { const onClickTool = useCallback(
setCurrentTool(tool) (tool: string) => {
// Set tool in query param without altering other query params
const [pathRoot, pathQuery = ''] = asPath.split('?')
const params = new URLSearchParams(pathQuery)
params.set(toolQueryKey, tool)
router.push({ pathname: pathRoot, query: params.toString() }, undefined, { shallow: true })
sendEvent({ sendEvent({
type: EventType.preference, type: EventType.preference,
preference_name: 'application', preference_name: 'application',
preference_value: tool, preference_value: tool,
}) })
Cookies.set('toolPreferred', tool, { sameSite: 'strict', secure: true }) Cookies.set('toolPreferred', tool, {
} sameSite: 'strict',
secure: document.location.protocol !== 'http:',
expires: 365,
})
},
[asPath]
)
if (variant === 'underlinenav') { if (variant === 'underlinenav') {
const [, pathQuery = ''] = asPath.split('?')
const params = new URLSearchParams(pathQuery)
return ( return (
<UnderlineNav {...sharedContainerProps}> <UnderlineNav {...sharedContainerProps}>
{detectedTools.map((tool) => ( {detectedTools.map((tool) => {
params.set(toolQueryKey, tool)
return (
<UnderlineNav.Link <UnderlineNav.Link
href={`?${params.toString()}`}
key={tool} key={tool}
data-tool={tool} data-tool={tool}
selected={tool === currentTool} selected={tool === currentTool}
onClick={() => { onClick={(event) => {
event.preventDefault()
onClickTool(tool) onClickTool(tool)
}} }}
> >
{allTools[tool]} {allTools[tool]}
</UnderlineNav.Link> </UnderlineNav.Link>
))} )
})}
</UnderlineNav> </UnderlineNav>
) )
} }

View File

@@ -26,7 +26,7 @@ export function getUserEventsId() {
if (cookieValue) return cookieValue if (cookieValue) return cookieValue
cookieValue = uuidv4() cookieValue = uuidv4()
Cookies.set(COOKIE_NAME, cookieValue, { Cookies.set(COOKIE_NAME, cookieValue, {
secure: true, secure: document.location.protocol !== 'http:',
sameSite: 'strict', sameSite: 'strict',
expires: 365, expires: 365,
}) })

View File

@@ -102,7 +102,7 @@ export function RestCodeSamples({ operation, slug }: Props) {
setSelectedLanguage(languageKey) setSelectedLanguage(languageKey)
Cookies.set('codeSampleLanguagePreferred', languageKey, { Cookies.set('codeSampleLanguagePreferred', languageKey, {
sameSite: 'strict', sameSite: 'strict',
secure: true, secure: document.location.protocol !== 'http:',
}) })
} }