diff --git a/components/article/ArticlePage.tsx b/components/article/ArticlePage.tsx
index de1d2e5f87..f7f80ecf19 100644
--- a/components/article/ArticlePage.tsx
+++ b/components/article/ArticlePage.tsx
@@ -95,8 +95,8 @@ export const ArticlePage = () => {
)}
- {includesPlatformSpecificContent && }
- {includesToolSpecificContent && }
+ {includesPlatformSpecificContent && }
+ {includesToolSpecificContent && }
{product && (
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])
+
+ 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])
+
+ const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
+
+ 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 (
+
+ {options.map((option) => {
+ params.set(queryStringKey, option.value)
+ const linkProps = {
+ [`data-${queryStringKey}`]: option.value,
+ }
+ return (
+ {
+ event.preventDefault()
+ onClickChoice(option.value)
+ }}
+ {...linkProps}
+ >
+ {option.label}
+
+ )
+ })}
+
+ )
+}
diff --git a/components/article/PlatformPicker.tsx b/components/article/PlatformPicker.tsx
index 234b2e5af3..579542eb3c 100644
--- a/components/article/PlatformPicker.tsx
+++ b/components/article/PlatformPicker.tsx
@@ -1,17 +1,14 @@
-import { useCallback, useEffect, useState } from 'react'
-import Cookies from 'js-cookie'
-import { SubNav, TabNav, UnderlineNav } from '@primer/react'
-import { sendEvent, EventType } from 'components/lib/events'
-import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
import { useArticleContext } from 'components/context/ArticleContext'
import { parseUserAgent } from 'components/lib/user-agent'
+import { InArticlePicker } from './InArticlePicker'
const platformQueryKey = 'platform'
const platforms = [
- { id: 'mac', label: 'Mac' },
- { id: 'windows', label: 'Windows' },
- { id: 'linux', label: 'Linux' },
+ { value: 'mac', label: 'Mac' },
+ { value: 'windows', label: 'Windows' },
+ { value: 'linux', label: 'Linux' },
]
// Nota bene: platform === os
@@ -22,7 +19,7 @@ const platforms = [
function showPlatformSpecificContent(platform: string) {
const markdowns = Array.from(document.querySelectorAll('.extended-markdown'))
markdowns
- .filter((el) => platforms.some((platform) => el.classList.contains(platform.id)))
+ .filter((el) => platforms.some((platform) => el.classList.contains(platform.value)))
.forEach((el) => {
el.style.display = el.classList.contains(platform) ? '' : 'none'
})
@@ -31,7 +28,7 @@ function showPlatformSpecificContent(platform: string) {
// example: inline content
const platformEls = Array.from(
document.querySelectorAll(
- platforms.map((platform) => `.platform-${platform.id}`).join(', ')
+ platforms.map((platform) => `.platform-${platform.value}`).join(', ')
)
)
platformEls.forEach((el) => {
@@ -39,153 +36,39 @@ function showPlatformSpecificContent(platform: string) {
})
}
-// uses the order of the supportedPlatforms array to
-// determine the default platform
-const getFallbackPlatform = (detectedPlatforms: Array): string => {
- const foundPlatform = platforms.find((platform) => detectedPlatforms.includes(platform.id))
- return foundPlatform?.id || 'linux'
-}
-
-type Props = {
- variant?: 'subnav' | 'tabnav' | 'underlinenav'
-}
-export const PlatformPicker = ({ variant = 'subnav' }: Props) => {
- const router = useRouter()
- const { query, asPath, locale } = router
+export const PlatformPicker = () => {
const { defaultPlatform, detectedPlatforms } = useArticleContext()
- const [currentPlatform, setCurrentPlatform] = useState(defaultPlatform || '')
- // Run on mount for client-side only features
+ const [defaultUA, setDefaultUA] = useState('')
useEffect(() => {
let userAgent = parseUserAgent().os
if (userAgent === 'ios') {
userAgent = 'mac'
}
+ setDefaultUA(userAgent)
+ }, [])
- // 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'
- }
+ // Defensively, just in case some article happens to have an array
+ // but for some reasons, it might be empty, let's not have a picker
+ // at all.
+ if (!detectedPlatforms.length) return null
- setCurrentPlatform(platform)
-
- // always trigger this on initial render. if the default doesn't change the other useEffect won't fire
- showPlatformSpecificContent(platform)
- }, [asPath])
-
- // Make sure we've always selected a platform that exists in the article
- useEffect(() => {
- // Only check *after* current platform has been determined
- if (currentPlatform && !detectedPlatforms.includes(currentPlatform)) {
- setCurrentPlatform(getFallbackPlatform(detectedPlatforms))
- }
- }, [currentPlatform, detectedPlatforms.join(',')])
-
- const onClickPlatform = useCallback(
- (platform: string) => {
- // Set platform in query param without altering other query params
- const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
- const params = new URLSearchParams(asPathQuery)
- params.set(platformQueryKey, platform)
- const newPath = `/${locale}${asPathRoot}?${params}`
- router.push(newPath, undefined, { shallow: true, locale })
-
- sendEvent({
- type: EventType.preference,
- preference_name: 'os',
- preference_value: platform,
- })
-
- Cookies.set('osPreferred', platform, {
- sameSite: 'strict',
- secure: document.location.protocol !== 'http:',
- expires: 365,
- })
- },
- [asPath, locale]
- )
-
- // only show platforms that are in the current article
- const platformOptions = platforms.filter((platform) => detectedPlatforms.includes(platform.id))
-
- const sharedContainerProps = {
- 'data-testid': 'platform-picker',
- 'aria-label': 'Platform picker',
- 'data-default-platform': defaultPlatform,
- className: 'mb-4',
- }
-
- if (variant === 'subnav') {
- return (
-
-
- {platformOptions.map((option) => {
- return (
- {
- onClickPlatform(option.id)
- }}
- >
- {option.label}
-
- )
- })}
-
-
- )
- }
-
- if (variant === 'underlinenav') {
- const [, pathQuery = ''] = asPath.split('?')
- const params = new URLSearchParams(pathQuery)
- return (
-
- {platformOptions.map((option) => {
- params.set(platformQueryKey, option.id)
- return (
- {
- event.preventDefault()
- onClickPlatform(option.id)
- }}
- >
- {option.label}
-
- )
- })}
-
- )
- }
+ const options = platforms.filter((platform) => detectedPlatforms.includes(platform.value))
return (
-
- {platformOptions.map((option) => {
- return (
- {
- onClickPlatform(option.id)
- }}
- >
- {option.label}
-
- )
- })}
-
+
)
}
diff --git a/components/article/ToolPicker.tsx b/components/article/ToolPicker.tsx
index 753a71a406..171865fb8e 100644
--- a/components/article/ToolPicker.tsx
+++ b/components/article/ToolPicker.tsx
@@ -1,11 +1,7 @@
-import { useCallback, useEffect, useState } from 'react'
-import { useRouter } from 'next/router'
-import Cookies from 'js-cookie'
-import { UnderlineNav } from '@primer/react'
-import { sendEvent, EventType } from 'components/lib/events'
import { preserveAnchorNodePosition } from 'scroll-anchoring'
import { useArticleContext } from 'components/context/ArticleContext'
+import { InArticlePicker } from './InArticlePicker'
// example: http://localhost:4000/en/codespaces/developing-in-codespaces/creating-a-codespace
@@ -48,97 +44,29 @@ function getDefaultTool(defaultTool: string | undefined, detectedTools: Array {
- const router = useRouter()
- const { asPath, query, locale } = router
+export const ToolPicker = () => {
// allTools comes from the ArticleContext which contains the list of tools available
const { defaultTool, detectedTools, allTools } = useArticleContext()
- const [currentTool, setCurrentTool] = useState(getDefaultTool(defaultTool, detectedTools))
- const sharedContainerProps = {
- 'data-testid': 'tool-picker',
- 'aria-label': 'Tool picker',
- 'data-default-tool': defaultTool,
- className: 'mb-4',
- }
+ if (!detectedTools.length) return null
- // Run on mount for client-side only features
- useEffect(() => {
- // If the user selected a tool preference and the tool is present on this page
- // Has to be client-side only for cookie reading
- const cookieValue = Cookies.get('toolPreferred')
- if (cookieValue && detectedTools.includes(cookieValue)) {
- setCurrentTool(cookieValue)
- }
- }, [])
+ const options = detectedTools.map((value) => {
+ return { value, label: allTools[value] }
+ })
- // Whenever the currentTool is changed, update the article content or selected tool from query param
- useEffect(() => {
- preserveAnchorNodePosition(document, () => {
- 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])
-
- const onClickTool = useCallback(
- (tool: string) => {
- // Set tool in query param without altering other query params
- const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
- const params = new URLSearchParams(asPathQuery)
- params.set(toolQueryKey, tool)
- const newPath = `/${locale}${asPathRoot}?${params}`
- router.push(newPath, undefined, { shallow: true, locale })
-
- sendEvent({
- type: EventType.preference,
- preference_name: 'application',
- preference_value: tool,
- })
- Cookies.set('toolPreferred', tool, {
- sameSite: 'strict',
- secure: document.location.protocol !== 'http:',
- expires: 365,
- })
- },
- [asPath, locale]
+ return (
+ {
+ preserveAnchorNodePosition(document, () => {
+ showToolSpecificContent(value, Object.keys(allTools))
+ })
+ }}
+ preferenceName="application"
+ ariaLabel="Tool"
+ options={options}
+ />
)
-
- if (variant === 'underlinenav') {
- const [, pathQuery = ''] = asPath.split('?')
- const params = new URLSearchParams(pathQuery)
- return (
-
- {detectedTools.map((tool) => {
- params.set(toolQueryKey, tool)
- return (
- {
- event.preventDefault()
- onClickTool(tool)
- }}
- >
- {allTools[tool]}
-
- )
- })}
-
- )
- }
-
- return null
}