Skip to content

Commit e3b2e82

Browse files
authored
React tool picker (#23004)
* Working on tool picker * Start on react version of tool picker * Update ToolPicker.tsx * Update tool-switcher.html * Get it working * Update browser.js * Update browser.js * Update ToolPicker.tsx
1 parent d6e56f3 commit e3b2e82

9 files changed

Lines changed: 160 additions & 166 deletions

File tree

components/article/ArticlePage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MarkdownContent } from 'components/ui/MarkdownContent'
1515
import { Lead } from 'components/ui/Lead'
1616
import { ArticleGridLayout } from './ArticleGridLayout'
1717
import { PlatformPicker } from 'components/article/PlatformPicker'
18+
import { ToolPicker } from 'components/article/ToolPicker'
1819

1920
// Mapping of a "normal" article to it's interactive counterpart
2021
const interactiveAlternatives: Record<string, { href: string }> = {
@@ -52,6 +53,7 @@ export const ArticlePage = () => {
5253
contributor,
5354
permissions,
5455
includesPlatformSpecificContent,
56+
includesToolSpecificContent,
5557
product,
5658
miniTocItems,
5759
currentLearningTrack,
@@ -111,6 +113,7 @@ export const ArticlePage = () => {
111113
)}
112114

113115
{includesPlatformSpecificContent && <PlatformPicker variant="underlinenav" />}
116+
{includesToolSpecificContent && <ToolPicker variant="underlinenav" />}
114117

115118
{product && (
116119
<Callout

components/article/PlatformPicker.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ const platforms = [
1212
{ id: 'linux', label: 'Linux' },
1313
]
1414

15+
// Nota bene: platform === os
16+
1517
// Imperatively modify article content to show only the selected platform
1618
// find all platform-specific *block* elements and hide or show as appropriate
17-
// example: {% mac } block content {% mac %}
19+
// example: {% mac %} block content {% endmac %}
1820
function showPlatformSpecificContent(platform: string) {
1921
const markdowns = Array.from(document.querySelectorAll<HTMLElement>('.extended-markdown'))
2022
markdowns

components/article/ToolPicker.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useEffect, useState } from 'react'
2+
import Cookies from 'js-cookie'
3+
import { UnderlineNav } from '@primer/components'
4+
import { sendEvent, EventType } from 'components/lib/events'
5+
import { preserveAnchorNodePosition } from 'scroll-anchoring'
6+
7+
import { useArticleContext } from 'components/context/ArticleContext'
8+
9+
// example: http://localhost:4000/en/codespaces/developing-in-codespaces/creating-a-codespace
10+
11+
// Nota bene: tool === application
12+
// Nota bene: picker === switcher
13+
14+
const supportedTools = ['cli', 'desktop', 'webui', 'curl', 'codespaces', 'vscode']
15+
const toolTitles = {
16+
webui: 'Web browser',
17+
cli: 'GitHub CLI',
18+
curl: 'cURL',
19+
desktop: 'Desktop',
20+
codespaces: 'Codespaces',
21+
vscode: 'Visual Studio Code',
22+
} as Record<string, string>
23+
24+
// Imperatively modify article content to show only the selected tool
25+
// find all platform-specific *block* elements and hide or show as appropriate
26+
// example: {% webui %} block content {% endwebui %}
27+
function showToolSpecificContent(tool: string) {
28+
const markdowns = Array.from(document.querySelectorAll<HTMLElement>('.extended-markdown'))
29+
markdowns
30+
.filter((el) => supportedTools.some((tool) => el.classList.contains(tool)))
31+
.forEach((el) => {
32+
el.style.display = el.classList.contains(tool) ? '' : 'none'
33+
})
34+
35+
// find all tool-specific *inline* elements and hide or show as appropriate
36+
// example: <span class="tool-webui">inline content</span>
37+
const toolEls = Array.from(
38+
document.querySelectorAll<HTMLElement>(supportedTools.map((tool) => `.tool-${tool}`).join(', '))
39+
)
40+
toolEls.forEach((el) => {
41+
el.style.display = el.classList.contains(`tool-${tool}`) ? '' : 'none'
42+
})
43+
}
44+
45+
function getDefaultTool(defaultTool: string | undefined, detectedTools: Array<string>): string {
46+
// If there is a default tool and the tool is present on this page
47+
if (defaultTool && detectedTools.includes(defaultTool)) return defaultTool
48+
49+
// Default to webui if present (this is generally the case where we show UI/CLI/Desktop info)
50+
if (detectedTools.includes('webui')) return 'webui'
51+
52+
// Default to cli if present (this is generally the case where we show curl/CLI info)
53+
if (detectedTools.includes('cli')) return 'cli'
54+
55+
// Otherwise, just choose the first detected tool
56+
return detectedTools[0]
57+
}
58+
59+
type Props = {
60+
variant?: 'subnav' | 'tabnav' | 'underlinenav'
61+
}
62+
export const ToolPicker = ({ variant = 'subnav' }: Props) => {
63+
const { defaultTool, detectedTools } = useArticleContext()
64+
const [currentTool, setCurrentTool] = useState(getDefaultTool(defaultTool, detectedTools))
65+
66+
const sharedContainerProps = {
67+
'data-testid': 'tool-picker',
68+
'aria-label': 'Tool picker',
69+
'data-default-tool': defaultTool,
70+
className: 'mb-4',
71+
}
72+
73+
// Run on mount for client-side only features
74+
useEffect(() => {
75+
// If the user selected a tool preference and the tool is present on this page
76+
// Has to be client-side only for cookie reading
77+
const cookieValue = Cookies.get('toolPreferred')
78+
if (cookieValue && detectedTools.includes(cookieValue)) {
79+
setCurrentTool(cookieValue)
80+
}
81+
}, [])
82+
83+
function onClickTool(tool: string) {
84+
setCurrentTool(tool)
85+
preserveAnchorNodePosition(document, () => {
86+
showToolSpecificContent(tool)
87+
})
88+
sendEvent({
89+
type: EventType.preference,
90+
preference_name: 'application',
91+
preference_value: tool,
92+
})
93+
Cookies.set('toolPreferred', tool, { sameSite: 'strict', secure: true })
94+
}
95+
96+
if (variant === 'underlinenav') {
97+
return (
98+
<UnderlineNav {...sharedContainerProps}>
99+
{detectedTools.map((tool) => (
100+
<UnderlineNav.Link
101+
key={tool}
102+
data-tool={tool}
103+
as="button"
104+
selected={tool === currentTool}
105+
onClick={() => {
106+
onClickTool(tool)
107+
}}
108+
>
109+
{toolTitles[tool]}
110+
</UnderlineNav.Link>
111+
))}
112+
</UnderlineNav>
113+
)
114+
}
115+
116+
return null
117+
}

components/context/ArticleContext.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ export type ArticleContextT = {
2222
contributor: { name: string; URL: string } | null
2323
permissions?: string
2424
includesPlatformSpecificContent: boolean
25+
includesToolSpecificContent: boolean
2526
defaultPlatform?: string
27+
defaultTool?: string
2628
product?: string
2729
currentLearningTrack?: LearningTrack
2830
detectedPlatforms: Array<string>
31+
detectedTools: Array<string>
2932
}
3033

3134
export const ArticleContext = createContext<ArticleContextT | null>(null)
@@ -60,9 +63,12 @@ export const getArticleContextFromRequest = (req: any): ArticleContextT => {
6063
contributor: page.contributor || null,
6164
permissions: page.permissions || '',
6265
includesPlatformSpecificContent: page.includesPlatformSpecificContent || false,
66+
includesToolSpecificContent: page.includesToolSpecificContent || false,
6367
defaultPlatform: page.defaultPlatform || '',
68+
defaultTool: page.defaultTool || '',
6469
product: page.product || '',
6570
currentLearningTrack: req.context.currentLearningTrack,
6671
detectedPlatforms: page.detectedPlatforms || [],
72+
detectedTools: page.detectedTools || [],
6773
}
6874
}

components/lib/display-tool-specific-content.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

includes/tool-switcher.html

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
1-
<nav class="UnderlineNav my-3" id="tool-switcher"
2-
{%- if page.defaultTool %} data-default-tool="{{ page.defaultTool }}"{% endif %}>
3-
<div class="UnderlineNav-body">
4-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="webui">Web browser</a>
5-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="cli">GitHub CLI</a>
6-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="curl">cURL</a>
7-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="desktop">Desktop</a>
8-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="codespaces">Codespaces</a>
9-
<a href="#" class="UnderlineNav-item tool-switcher" data-tool="vscode">Visual Studio Code</a>
10-
</div>
11-
</nav>
1+
<span class="tool-switcher"></span>

lib/page.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,19 @@ class Page {
250250
})
251251
}
252252

253-
this.detectedPlatforms = [
254-
(html.includes('extended-markdown mac') || html.includes('platform-mac')) && 'mac',
255-
(html.includes('extended-markdown windows') || html.includes('platform-windows')) &&
256-
'windows',
257-
(html.includes('extended-markdown linux') || html.includes('platform-linux')) && 'linux',
258-
].filter(Boolean)
259-
260253
// set a flag so layout knows whether to render a mac/windows/linux switcher element
254+
this.detectedPlatforms = ['mac', 'windows', 'linux'].filter(
255+
(platform) =>
256+
html.includes(`extended-markdown ${platform}`) || html.includes(`platform-${platform}`)
257+
)
261258
this.includesPlatformSpecificContent = this.detectedPlatforms.length > 0
262259

260+
// set flags for webui, cli, etc switcher element
261+
this.detectedTools = ['cli', 'desktop', 'webui', 'curl', 'codespaces', 'vscode'].filter(
262+
(tool) => html.includes(`extended-markdown ${tool}`) || html.includes(`tool-${tool}`)
263+
)
264+
this.includesToolSpecificContent = this.detectedTools.length > 0
265+
263266
return html
264267
}
265268

pages/[versionId]/[productId]/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useRouter } from 'next/router'
44
// "legacy" javascript needed to maintain existing functionality
55
// typically operating on elements **within** an article.
66
import copyCode from 'components/lib/copy-code'
7-
import displayToolSpecificContent from 'components/lib/display-tool-specific-content'
87
import localization from 'components/lib/localization'
98
import wrapCodeTerms from 'components/lib/wrap-code-terms'
109

@@ -40,7 +39,6 @@ import { useEffect } from 'react'
4039

4140
function initiateArticleScripts() {
4241
copyCode()
43-
displayToolSpecificContent()
4442
localization()
4543
wrapCodeTerms()
4644
}

0 commit comments

Comments
 (0)