1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Update docs to use Primer React 37 (#57265)

Co-authored-by: Evan Bonsignori <evanabonsignori@gmail.com>
This commit is contained in:
Mardav Wala
2025-08-28 16:29:07 -04:00
committed by GitHub
parent 2c3fdf1990
commit 9b0d2beb9a
34 changed files with 353 additions and 1137 deletions

View File

@@ -15,6 +15,9 @@ const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
const productIds = data.children const productIds = data.children
export default { export default {
// Transpile @primer/react so Next's webpack can process its CSS and other assets
// This ensures CSS in node_modules/@primer/react is handled by the app's loaders.
transpilePackages: ['@primer/react'],
// speed up production `next build` by ignoring typechecking during that step of build. // speed up production `next build` by ignoring typechecking during that step of build.
// type-checking still occurs in the Dockerfile build // type-checking still occurs in the Dockerfile build
typescript: { typescript: {

1019
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -166,7 +166,7 @@
"@primer/live-region-element": "^0.7.2", "@primer/live-region-element": "^0.7.2",
"@primer/octicons": "^19.15.5", "@primer/octicons": "^19.15.5",
"@primer/octicons-react": "^19.14.0", "@primer/octicons-react": "^19.14.0",
"@primer/react": "36.27.0", "@primer/react": "^37.31.0",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-errors": "^3.0.0", "ajv-errors": "^3.0.0",
@@ -177,6 +177,7 @@
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"cheerio-to-text": "0.2.4", "cheerio-to-text": "0.2.4",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clsx": "^2.1.1",
"connect-timeout": "1.9.1", "connect-timeout": "1.9.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cuss": "2.2.0", "cuss": "2.2.0",
@@ -219,8 +220,8 @@
"ora": "^8.0.1", "ora": "^8.0.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"quick-lru": "7.0.1", "quick-lru": "7.0.1",
"react": "18.3.1", "react": "^18.3.1",
"react-dom": "18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",

View File

@@ -526,7 +526,7 @@ test.describe('test nav at different viewports', () => {
// hamburger button for sidebar overlay is visible // hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click() await page.getByTestId('sidebar-hamburger').click()
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
}) })
test('medium viewports - 768-1011', async ({ page }) => { test('medium viewports - 768-1011', async ({ page }) => {
@@ -555,7 +555,7 @@ test.describe('test nav at different viewports', () => {
// hamburger button for sidebar overlay is visible // hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click() await page.getByTestId('sidebar-hamburger').click()
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
}) })
test('small viewports - 544-767', async ({ page }) => { test('small viewports - 544-767', async ({ page }) => {
@@ -588,7 +588,7 @@ test.describe('test nav at different viewports', () => {
// hamburger button for sidebar overlay is visible // hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click() await page.getByTestId('sidebar-hamburger').click()
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
}) })
test('x-small viewports - 0-544', async ({ page }) => { test('x-small viewports - 0-544', async ({ page }) => {
@@ -627,7 +627,7 @@ test.describe('test nav at different viewports', () => {
// hamburger button for sidebar overlay is visible // hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click() await page.getByTestId('sidebar-hamburger').click()
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
}) })
test('do a search when the viewport is x-small', async ({ page }) => { test('do a search when the viewport is x-small', async ({ page }) => {

View File

@@ -24,7 +24,7 @@ describe('sidebar', () => {
const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world') const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world')
expect( expect(
$( $(
'[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] div span', '[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] span span',
).text(), ).text(),
).toBe('Hello World') ).toBe('Hello World')
}) })
@@ -35,7 +35,7 @@ describe('sidebar', () => {
// from its regular title. // from its regular title.
expect( expect(
$( $(
'[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/bar"] div span', '[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/bar"] span span',
).text(), ).text(),
).toBe('Bar') ).toBe('Bar')
}) })

View File

@@ -1,5 +1,6 @@
@import "@primer/css/support/variables/layout.scss"; @import "@primer/css/support/variables/layout.scss";
@import "@primer/css/support/mixins/layout.scss"; @import "@primer/css/support/mixins/layout.scss";
@import "src/frame/stylesheets/breakpoint-xxl.scss";
.header { .header {
display: unset; display: unset;
@@ -51,3 +52,13 @@
visibility: visible !important; visibility: visible !important;
} }
} }
.dialog {
@include breakpoint-xxl {
display: none;
}
[class*="prc-Dialog-Body"] {
padding: 0 !important;
}
}

View File

@@ -13,7 +13,6 @@ import { useTranslation } from '@/languages/components/useTranslation'
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
import { VersionPicker } from '@/versions/components/VersionPicker' import { VersionPicker } from '@/versions/components/VersionPicker'
import { SidebarNav } from '@/frame/components/sidebar/SidebarNav' import { SidebarNav } from '@/frame/components/sidebar/SidebarNav'
import { AllProductsLink } from '@/frame/components/sidebar/AllProductsLink'
import { SearchBarButton } from '@/search/components/input/SearchBarButton' import { SearchBarButton } from '@/search/components/input/SearchBarButton'
import { HeaderSearchAndWidgets } from './HeaderSearchAndWidgets' import { HeaderSearchAndWidgets } from './HeaderSearchAndWidgets'
import { useInnerWindowWidth } from './hooks/useInnerWindowWidth' import { useInnerWindowWidth } from './hooks/useInnerWindowWidth'
@@ -34,8 +33,8 @@ export const Header = () => {
const { params, updateParams } = useMultiQueryParams() const { params, updateParams } = useMultiQueryParams()
const [scroll, setScroll] = useState(false) const [scroll, setScroll] = useState(false)
const [isSidebarOpen, setIsSidebarOpen] = useState(false) const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const openSidebar = useCallback(() => setIsSidebarOpen(true), [isSidebarOpen]) const openSidebar = useCallback(() => setIsSidebarOpen(true), [])
const closeSidebar = useCallback(() => setIsSidebarOpen(false), [isSidebarOpen]) const closeSidebar = useCallback(() => setIsSidebarOpen(false), [])
const isMounted = useRef(false) const isMounted = useRef(false)
const menuButtonRef = useRef<HTMLButtonElement>(null) const menuButtonRef = useRef<HTMLButtonElement>(null)
const { asPath } = useRouter() const { asPath } = useRouter()
@@ -204,45 +203,23 @@ export const Header = () => {
onClick={openSidebar} onClick={openSidebar}
ref={returnFocusRef} ref={returnFocusRef}
/> />
<Dialog {isSidebarOpen && (
returnFocusRef={returnFocusRef} <Dialog
isOpen={isSidebarOpen} returnFocusRef={returnFocusRef}
onDismiss={closeSidebar} onClose={closeSidebar}
aria-labelledby="menu-title" className={cx(styles.dialog, 'd-xxl-none')}
sx={{ position="left"
position: 'fixed', title={
top: '0', error === '404' || !currentProduct || isSearchResultsPage
left: '0', ? null
marginTop: '0', : currentProductName || currentProduct.name
maxHeight: '100vh', }
width: 'auto !important', subtitle={isRestPage && <ApiVersionPicker />}
transform: 'none', width="medium"
borderRadius: '0',
borderRight:
'1px solid var(--borderColor-default, var(--color-border-default))',
}}
>
<Dialog.Header
style={{ paddingTop: '0px', background: 'none' }}
id="sidebar-overlay-header"
sx={{ display: 'block' }}
> >
<AllProductsLink /> <SidebarNav variant="overlay" />
{error === '404' || !currentProduct || isSearchResultsPage ? null : ( </Dialog>
<h2 className="mt-3"> )}
<Link
data-testid="sidebar-product-dialog"
href={currentProduct.href}
className="d-block pl-1 mb-2 h3 color-fg-default no-underline"
>
{currentProductName || currentProduct.name}
</Link>
</h2>
)}
{isRestPage && <ApiVersionPicker />}
</Dialog.Header>
<SidebarNav variant="overlay" />
</Dialog>
</div> </div>
)} )}
<div className="mr-auto width-full" data-search="breadcrumbs"> <div className="mr-auto width-full" data-search="breadcrumbs">

View File

@@ -33,7 +33,9 @@ export const SidebarNav = ({ variant = 'full' }: Props) => {
<div <div
data-container="nav" data-container="nav"
className={cx(variant === 'full' ? 'position-sticky d-none border-right d-xxl-block' : '')} className={cx(variant === 'full' ? 'position-sticky d-none border-right d-xxl-block' : '')}
style={{ width: 326, height: 'calc(100vh - 65px)', top: '65px' }} style={
variant === 'full' ? { width: 326, height: 'calc(100vh - 65px)', top: '65px' } : undefined
}
> >
<nav <nav
aria-labelledby="allproducts-menu" aria-labelledby="allproducts-menu"
@@ -62,10 +64,16 @@ export const SidebarNav = ({ variant = 'full' }: Props) => {
)} )}
<div <div
className={cx( className={cx(
variant === 'overlay' ? 'd-xxl-none' : 'border-right d-none d-xxl-block', variant === 'overlay'
'bg-primary overflow-y-auto flex-shrink-0', ? 'width-full d-xxl-none'
: 'border-right d-none d-xxl-block overflow-y-auto',
'bg-primary flex-shrink-0',
)} )}
style={{ width: 326, height: 'calc(100vh - 175px)', paddingBottom: sidebarPaddingBottom }} style={
variant === 'overlay'
? { paddingBottom: sidebarPaddingBottom }
: { width: 326, height: 'calc(100vh - 175px)', paddingBottom: sidebarPaddingBottom }
}
role="region" role="region"
aria-label="Page navigation content" aria-label="Page navigation content"
> >

View File

@@ -0,0 +1,12 @@
@import "breakpoint-xxl.scss";
#__primerPortalRoot__ [class*="prc-Dialog-Backdrop"] {
/* Make sure the dialog backdrop is hidden on large screens */
display: flex;
visibility: visible;
@include breakpoint-xxl {
display: none !important;
visibility: hidden !important;
}
}

View File

@@ -11,6 +11,7 @@
@import "headings.scss"; @import "headings.scss";
@import "scroll-top.scss"; @import "scroll-top.scss";
@import "utilities.scss"; @import "utilities.scss";
@import "dialog-overrides.scss";
@import "src/content-render/stylesheets/index.scss"; @import "src/content-render/stylesheets/index.scss";
@import "src/links/stylesheets/hover-card.scss"; @import "src/links/stylesheets/hover-card.scss";

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { readFile } from 'fs/promises' import fs from 'fs'
import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file' import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
import { getOpenApiVersion } from '@/versions/lib/all-versions' import { getOpenApiVersion } from '@/versions/lib/all-versions'
@@ -10,7 +10,7 @@ const githubAppsData = new Map()
// Initialize the Map with the page type keys listed under `pages` // Initialize the Map with the page type keys listed under `pages`
// in the config.json file. // in the config.json file.
const appsDataConfig = JSON.parse(await readFile('src/github-apps/lib/config.json', 'utf8')) const appsDataConfig = JSON.parse(fs.readFileSync('src/github-apps/lib/config.json', 'utf8'))
for (const pageType of Object.keys(appsDataConfig.pages)) { for (const pageType of Object.keys(appsDataConfig.pages)) {
githubAppsData.set(pageType, new Map()) githubAppsData.set(pageType, new Map())
} }

View File

@@ -0,0 +1,11 @@
.linkItem {
border-radius: 0;
&:hover {
border-radius: 0;
}
h3 {
color: var(--fgColor-accent, var(--color-accent-fg));
}
}

View File

@@ -1,11 +1,11 @@
import cx from 'classnames'
import dayjs from 'dayjs'
import { ActionList } from '@primer/react'
import { useTranslation } from '@/languages/components/useTranslation'
import { Link } from '@/frame/components/Link' import { Link } from '@/frame/components/Link'
import { ArrowRightIcon } from '@primer/octicons-react'
import { FeaturedLink } from '@/landings/components/ProductLandingContext' import { FeaturedLink } from '@/landings/components/ProductLandingContext'
import { BumpLink } from '@/frame/components/ui/BumpLink' import { useTranslation } from '@/languages/components/useTranslation'
import { ArrowRightIcon } from '@primer/octicons-react'
import { ActionList } from '@primer/react'
import { clsx } from 'clsx'
import dayjs from 'dayjs'
import styles from './ArticleList.module.css'
export type ArticleListPropsT = { export type ArticleListPropsT = {
title?: string title?: string
@@ -29,7 +29,7 @@ export const ArticleList = ({
<> <>
{title && ( {title && (
<div className="mb-4 d-flex flex-items-baseline"> <div className="mb-4 d-flex flex-items-baseline">
<h2 className={cx('f4 text-semibold')}>{title}</h2> <h2 className={clsx('f4', 'text-semibold')}>{title}</h2>
{viewAllHref && ( {viewAllHref && (
<Link <Link
href={viewAllHref} href={viewAllHref}
@@ -44,37 +44,26 @@ export const ArticleList = ({
</div> </div>
)} )}
<ActionList as="ul" data-testid="article-list" variant="full"> <ActionList data-testid="article-list" variant="full">
{articles.map((link) => { {articles.map((link) => {
return ( return (
<ActionList.Item <ActionList.LinkItem
as="li" as="a"
key={link.href} key={link.href}
className={cx('width-full border-top')} href={link.href}
sx={{ data-testid="bump-link"
borderRadius: 0, className={clsx(styles.linkItem, 'width-full', 'border-top', 'py-3')}
':hover': {
borderRadius: 0,
},
}}
tabIndex={undefined}
> >
<BumpLink <div>
as={Link} {link.intro ? (
href={link.href} <h3 className="f4" data-testid="link-with-intro-title">
className="py-3" <span>{link.fullTitle ? link.fullTitle : link.title}</span>
title={ </h3>
link.intro ? ( ) : (
<h3 className="f4" data-testid="link-with-intro-title"> <span className="f4 text-bold d-block" data-testid="link-with-intro-title">
<span>{link.fullTitle ? link.fullTitle : link.title}</span> {link.fullTitle ? link.fullTitle : link.title}
</h3> </span>
) : ( )}
<span className="f4 text-bold d-block" data-testid="link-with-intro-title">
{link.fullTitle ? link.fullTitle : link.title}
</span>
)
}
>
{link.intro && ( {link.intro && (
<p className="color-fg-muted mb-0 mt-1" data-testid="link-with-intro-intro"> <p className="color-fg-muted mb-0 mt-1" data-testid="link-with-intro-intro">
{link.intro} {link.intro}
@@ -88,8 +77,8 @@ export const ArticleList = ({
{dayjs(link.date).format('MMMM DD')} {dayjs(link.date).format('MMMM DD')}
</time> </time>
)} )}
</BumpLink> </div>
</ActionList.Item> </ActionList.LinkItem>
) )
})} })}
</ActionList> </ActionList>

View File

@@ -56,7 +56,7 @@ export const CookBookArticleCard = ({
/> />
)} )}
<div> <div>
<h3 className="h4"> <h3 className="h4 fgColor-accent">
<Link href={url}>{title}</Link> <Link href={url}>{title}</Link>
</h3> </h3>
<div className="fgColor-muted mb-3 mt-2">{description}</div> <div className="fgColor-muted mb-3 mt-2">{description}</div>

View File

@@ -0,0 +1,14 @@
.linkItem {
border-radius: 0;
&:hover {
border-radius: 0;
}
a {
span {
color: var(--fgColor-accent, var(--color-accent-fg));
text-decoration: underline;
}
}
}

View File

@@ -1,9 +1,9 @@
import cx from 'classnames'
import { ActionList } from '@primer/react' import { ActionList } from '@primer/react'
import { ProductTreeNode, useMainContext } from '@/frame/components/context/MainContext' import { ProductTreeNode, useMainContext } from '@/frame/components/context/MainContext'
import { Link } from '@/frame/components/Link' import { Link } from '@/frame/components/Link'
import clsx from 'clsx'
import styles from './ProductArticlesList.module.css'
export const ProductArticlesList = () => { export const ProductArticlesList = () => {
const { currentProductTree } = useMainContext() const { currentProductTree } = useMainContext()
@@ -35,28 +35,19 @@ const ProductTreeNodeList = ({ treeNode }: { treeNode: ProductTreeNode }) => {
<ActionList variant="full"> <ActionList variant="full">
{treeNode.childPages.map((childNode, index) => { {treeNode.childPages.map((childNode, index) => {
return ( return (
<ActionList.Item <ActionList.LinkItem
as="li" as="a"
key={childNode.href + index} key={childNode.href + index}
className={cx('width-full pl-0')} href={childNode.href}
sx={{ className={clsx(styles.linkItem, 'width-full', 'pl-0', 'd-block')}
borderRadius: 0,
':hover': {
borderRadius: 0,
},
}}
tabIndex={undefined}
aria-labelledby={undefined}
> >
<Link className="d-block width-full text-underline" href={childNode.href}> {childNode.title}
{childNode.title} {childNode.childPages.length > 0 ? (
{childNode.childPages.length > 0 ? ( <small className="color-fg-muted d-inline-block">
<small className="color-fg-muted d-inline-block"> &nbsp;&bull; {childNode.childPages.length} articles
&nbsp;&bull; {childNode.childPages.length} articles </small>
</small> ) : null}
) : null} </ActionList.LinkItem>
</Link>
</ActionList.Item>
) )
})} })}
</ActionList> </ActionList>

View File

@@ -0,0 +1,3 @@
.sidebar * {
font-size: var(--h5-size, 14px) !important;
}

View File

@@ -7,6 +7,8 @@ import { ProductTreeNode, useMainContext } from '@/frame/components/context/Main
import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext' import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext'
import { nonAutomatedRestPaths } from '@/rest/lib/config' import { nonAutomatedRestPaths } from '@/rest/lib/config'
import styles from './SidebarProduct.module.css'
export const SidebarProduct = () => { export const SidebarProduct = () => {
const router = useRouter() const router = useRouter()
const { const {
@@ -32,7 +34,7 @@ export const SidebarProduct = () => {
} }
const productSection = () => ( const productSection = () => (
<div className="ml-3" data-testid="product-sidebar"> <div data-testid="product-sidebar">
<NavList aria-label="Product sidebar" role="navigation"> <NavList aria-label="Product sidebar" role="navigation">
{sidebarTree && {sidebarTree &&
sidebarTree.childPages.map((childPage) => ( sidebarTree.childPages.map((childPage) => (
@@ -50,7 +52,7 @@ export const SidebarProduct = () => {
nonAutomatedRestPaths.every((item: string) => !page.href.includes(item)), nonAutomatedRestPaths.every((item: string) => !page.href.includes(item)),
) )
return ( return (
<div className="ml-3"> <div>
<NavList aria-label="REST sidebar overview articles" role="navigation"> <NavList aria-label="REST sidebar overview articles" role="navigation">
{conceptualPages.map((childPage) => ( {conceptualPages.map((childPage) => (
<NavListItem key={childPage.href} childPage={childPage} /> <NavListItem key={childPage.href} childPage={childPage} />
@@ -69,7 +71,7 @@ export const SidebarProduct = () => {
} }
return ( return (
<div data-testid="sidebar" style={{ overflowY: 'auto' }} className="pt-3"> <div data-testid="sidebar" className={styles.sidebar}>
{isRestPage ? restSection() : productSection()} {isRestPage ? restSection() : productSection()}
</div> </div>
) )
@@ -90,7 +92,7 @@ function NavListItem({ childPage }: { childPage: ProductTreeNode }) {
> >
{childPage.title} {childPage.title}
{childPage.childPages.length > 0 && ( {childPage.childPages.length > 0 && (
<NavList.SubNav aria-label={`${childPage.title} submenu`} sx={{ '*': { fontSize: 1 } }}> <NavList.SubNav aria-label={`${childPage.title} submenu`}>
{childPage.sidebarLink && ( {childPage.sidebarLink && (
<NavList.Item <NavList.Item
href={childPage.sidebarLink.href} href={childPage.sidebarLink.href}
@@ -166,7 +168,7 @@ function RestNavListItem({ category }: { category: ProductTreeNode }) {
> >
{category.title} {category.title}
{category.childPages.length > 0 && ( {category.childPages.length > 0 && (
<NavList.SubNav aria-label={`${category.title} submenu`} sx={{ '*': { fontSize: 1 } }}> <NavList.SubNav aria-label={`${category.title} submenu`}>
{category.childPages.map((childPage) => { {category.childPages.map((childPage) => {
return ( return (
<NavList.Item <NavList.Item

View File

@@ -1,9 +1,9 @@
import React from 'react'
import cx from 'classnames' import cx from 'classnames'
import React from 'react'
import { ActionList } from '@primer/react'
import { Link } from '@/frame/components/Link' import { Link } from '@/frame/components/Link'
import type { TocItem } from '@/landings/types' import type { TocItem } from '@/landings/types'
import { ActionList } from '@primer/react'
type Props = { type Props = {
items: Array<TocItem> items: Array<TocItem>
@@ -45,9 +45,13 @@ export const TableOfContents = (props: Props) => {
const { fullPath, title, childTocItems } = item const { fullPath, title, childTocItems } = item
return ( return (
<React.Fragment key={fullPath}> <React.Fragment key={fullPath}>
<ActionList.Item className="f4 color-fg-accent d-list-item d-block width-full text-underline"> <ActionList.LinkItem
<Link href={fullPath}>{title}</Link> href={fullPath}
</ActionList.Item> as="a"
className="f4 color-fg-accent d-list-item d-block width-full text-underline"
>
{title}
</ActionList.LinkItem>
{(childTocItems || []).length > 0 && ( {(childTocItems || []).length > 0 && (
<li className="f4 color-fg-accent d-list-item d-block width-full text-underline"> <li className="f4 color-fg-accent d-list-item d-block width-full text-underline">
<ActionList <ActionList
@@ -57,12 +61,14 @@ export const TableOfContents = (props: Props) => {
> >
{(childTocItems || []).filter(Boolean).map((childItem) => { {(childTocItems || []).filter(Boolean).map((childItem) => {
return ( return (
<ActionList.Item <ActionList.LinkItem
key={childItem.fullPath} key={childItem.fullPath}
href={childItem.fullPath}
as="a"
className="f4 color-fg-accent d-list-item d-block width-full text-underline" className="f4 color-fg-accent d-list-item d-block width-full text-underline"
> >
<Link href={childItem.fullPath}>{childItem.title}</Link> {childItem.title}
</ActionList.Item> </ActionList.LinkItem>
) )
})} })}
</ActionList> </ActionList>

View File

@@ -8,7 +8,9 @@ describe('curated homepage links', () => {
test('English', async () => { test('English', async () => {
const $ = await getDOM('/en') const $ = await getDOM('/en')
const $links = $('[data-testid=bump-link]')
// Update selector to find actual link elements within article list
const $links = $('[data-testid=article-list] a')
expect($links.length).toBeGreaterThanOrEqual(6) expect($links.length).toBeGreaterThanOrEqual(6)
// Check that each link is localized and includes a title and intro // Check that each link is localized and includes a title and intro

View File

@@ -7,7 +7,7 @@ describe('featuredLinks', () => {
vi.setConfig({ testTimeout: 60 * 1000 }) vi.setConfig({ testTimeout: 60 * 1000 })
test('non-TOC pages do not have intro links', async () => { test('non-TOC pages do not have intro links', async () => {
const $ = await getDOM('/en/get-started/start-your-journey') const $ = await getDOM('/en/get-started/start-your-journey/hello-world')
expect($('[data-testid=article-list]')).toHaveLength(0) expect($('[data-testid=article-list]')).toHaveLength(0)
}) })
@@ -16,19 +16,27 @@ describe('featuredLinks', () => {
const $featuredLinks = $('[data-testid=article-list] a') const $featuredLinks = $('[data-testid=article-list] a')
expect($featuredLinks).toHaveLength(7) expect($featuredLinks).toHaveLength(7)
expect($featuredLinks.eq(0).attr('href')).toBe('/en/get-started/start-your-journey/hello-world') expect($featuredLinks.eq(0).attr('href')).toBe('/en/get-started/start-your-journey/hello-world')
expect($featuredLinks.eq(0).children('h3').text()).toMatch('Hello World') expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-title]').text()).toMatch(
expect($featuredLinks.eq(0).children('p').text()).toMatch('Follow this Hello World exercise') 'Hello World',
)
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-intro]').text()).toMatch(
/follow.+this.+hello.+world.+exercise/i,
)
}) })
test('Enterprise intro links have expected values', async () => { test('Enterprise intro links have expected values', async () => {
const $ = await getDOM('/enterprise-server@latest/get-started') const $ = await getDOM('/en/enterprise-server@latest/get-started')
const $featuredLinks = $('[data-testid=article-list] a') const $featuredLinks = $('[data-testid=article-list] a')
expect($featuredLinks.length).toBeGreaterThan(0) expect($featuredLinks.length).toBeGreaterThan(0)
// Fixture content expectations (CI environment)
expect($featuredLinks.eq(0).attr('href')).toBe( expect($featuredLinks.eq(0).attr('href')).toBe(
`/en/enterprise-server@${enterpriseServerReleases.latestStable}/get-started/foo/bar`, `/en/enterprise-server@${enterpriseServerReleases.latestStable}/get-started/foo/bar`,
) )
expect($featuredLinks.eq(0).children('h3').text()).toMatch('Bar Usually Comes After Foo') expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-title]').text()).toMatch(
expect($featuredLinks.eq(0).children('p').text()).toMatch( 'Bar Usually Comes After Foo',
)
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-intro]').text()).toMatch(
"This page doesn't really have an intro", "This page doesn't really have an intro",
) )
}) })

View File

@@ -1,10 +1,10 @@
import { useRouter } from 'next/router'
import { GlobeIcon } from '@primer/octicons-react' import { GlobeIcon } from '@primer/octicons-react'
import { useRouter } from 'next/router'
import { useLanguages } from '@/languages/components/LanguagesContext' import { useLanguages } from '@/languages/components/LanguagesContext'
import { useTranslation } from '@/languages/components/useTranslation' import { useTranslation } from '@/languages/components/useTranslation'
import { useUserLanguage } from '@/languages/components/useUserLanguage' import { useUserLanguage } from '@/languages/components/useUserLanguage'
import { ActionList, ActionMenu, IconButton, Link } from '@primer/react' import { ActionList, ActionMenu, IconButton } from '@primer/react'
type Props = { type Props = {
xs?: boolean xs?: boolean
@@ -38,16 +38,16 @@ export const LanguagePicker = ({ xs, mediumOrLower }: Props) => {
// in a "denormalized" way. // in a "denormalized" way.
const routerPath = router.asPath.split('#')[0] const routerPath = router.asPath.split('#')[0]
// languageList is specifically <ActionList.Item>'s which are reused // languageList is specifically ActionList items which are reused
// for menus that behave differently at the breakpoints. // for menus that behave differently at the breakpoints.
const languageList = langs.map((lang) => ( const languageList = langs.map((lang) => (
<ActionList.Item <ActionList.LinkItem
key={`/${lang.code}${routerPath}`} key={`/${lang.code}${routerPath}`}
selected={lang === selectedLang} as="a"
as={Link} active={lang === selectedLang}
lang={lang.code} lang={lang.code}
href={`/${lang.code}${routerPath}`} href={`/${lang.code}${routerPath}`}
onSelect={() => { onClick={() => {
if (lang.code) { if (lang.code) {
try { try {
setUserLanguageCookie(lang.code) setUserLanguageCookie(lang.code)
@@ -63,7 +63,7 @@ export const LanguagePicker = ({ xs, mediumOrLower }: Props) => {
}} }}
> >
{lang.nativeName || lang.name} {lang.nativeName || lang.name}
</ActionList.Item> </ActionList.LinkItem>
)) ))
// At large breakpoints, we return the full <ActionMenu> with just the languages, // At large breakpoints, we return the full <ActionMenu> with just the languages,

View File

@@ -1,4 +1,4 @@
import fs from 'fs/promises' import fs from 'fs'
import path from 'path' import path from 'path'
import frontmatter from '@/frame/lib/read-frontmatter' import frontmatter from '@/frame/lib/read-frontmatter'
import getApplicableVersions from '@/versions/lib/get-applicable-versions' import getApplicableVersions from '@/versions/lib/get-applicable-versions'
@@ -40,7 +40,7 @@ export interface ProductMap {
// Both internal and external products are specified in content/index.md // Both internal and external products are specified in content/index.md
const homepage = path.posix.join(ROOT, 'content/index.md') const homepage = path.posix.join(ROOT, 'content/index.md')
export const { data } = frontmatter(await fs.readFile(homepage, 'utf8')) export const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
export const productIds: string[] = data?.children || [] export const productIds: string[] = data?.children || []
@@ -53,13 +53,13 @@ for (const productId of productIds) {
// Early Access may not exist in the current checkout // Early Access may not exist in the current checkout
try { try {
await fs.readdir(dir) fs.readdirSync(dir)
} catch { } catch {
continue continue
} }
const toc = path.posix.join(dir, 'index.md') const toc = path.posix.join(dir, 'index.md')
const fileContent = await fs.readFile(toc, 'utf8') const fileContent = fs.readFileSync(toc, 'utf8')
const { data: tocData } = frontmatter(fileContent) const { data: tocData } = frontmatter(fileContent)
if (tocData) { if (tocData) {
const applicableVersions = getApplicableVersions(tocData.versions, toc) const applicableVersions = getApplicableVersions(tocData.versions, toc)

View File

@@ -79,31 +79,29 @@ export const ApiVersionPicker = () => {
// This only shows the REST Version picker if it's calendar date versioned // This only shows the REST Version picker if it's calendar date versioned
return allVersions[currentVersion].apiVersions.length > 0 ? ( return allVersions[currentVersion].apiVersions.length > 0 ? (
<div className="mb-3"> <div data-testid="api-version-picker">
<div data-testid="api-version-picker"> <Picker
<Picker defaultText={currentDateDisplayText}
defaultText={currentDateDisplayText} items={apiVersionLinks}
items={apiVersionLinks} pickerLabel="API Version: "
pickerLabel="API Version: " alignment="start"
alignment="start" buttonBorder={true}
buttonBorder={true} dataTestId="version"
dataTestId="version" ariaLabel="Select API Version"
ariaLabel="Select API Version" onSelect={(item) => {
onSelect={(item) => { if (item.extra?.currentDate) rememberApiVersion(item.extra.currentDate)
if (item.extra?.currentDate) rememberApiVersion(item.extra.currentDate) }}
}} renderItem={(item) => {
renderItem={(item) => { return item.extra?.info ? (
return item.extra?.info ? ( <div className="f6">
<div className="f6"> {item.text}
{item.text} <InfoIcon verticalAlign="middle" size={15} className="ml-1" />
<InfoIcon verticalAlign="middle" size={15} className="ml-1" /> </div>
</div> ) : (
) : ( item.text
item.text )
) }}
}} />
/>
</div>
</div> </div>
) : null ) : null
} }

View File

@@ -31,3 +31,13 @@
.responseCodeBlock { .responseCodeBlock {
min-height: 120px; min-height: 120px;
} }
.segmentedControl {
padding: 0 !important;
margin-bottom: 0.5rem !important;
li + li {
// Same as the Primer CSS selector for segmented control
margin-top: -1px !important;
}
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, FormEvent } from 'react' import { useState, useEffect, useRef, FormEvent } from 'react'
import { FormControl, IconButton, Select, TabNav } from '@primer/react' import { FormControl, IconButton, Select, SegmentedControl } from '@primer/react'
import { CheckIcon, CopyIcon, InfoIcon } from '@primer/octicons-react' import { CheckIcon, CopyIcon, InfoIcon } from '@primer/octicons-react'
import { announce } from '@primer/live-region-element' import { announce } from '@primer/live-region-element'
import Cookies from '@/frame/components/lib/cookies' import Cookies from '@/frame/components/lib/cookies'
@@ -260,26 +260,28 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
</div> </div>
<div className="border-top d-inline-flex flex-justify-between width-full flex-items-center pt-2"> <div className="border-top d-inline-flex flex-justify-between width-full flex-items-center pt-2">
<div className="d-inline-flex ml-2"> <div className="d-inline-flex ml-2">
<TabNav aria-label={`Example language selector for ${operation.title}`}> <SegmentedControl
className={styles.segmentedControl}
aria-label={`Example language selector for ${operation.title}`}
>
{languageSelectOptions.map((optionKey) => ( {languageSelectOptions.map((optionKey) => (
<TabNav.Link <SegmentedControl.Button
key={optionKey} key={optionKey}
selected={optionKey === selectedLanguage} selected={optionKey === selectedLanguage}
onClick={(e) => { onClick={(e: React.MouseEvent) => {
e.preventDefault() e.preventDefault()
handleLanguageSelection(optionKey) handleLanguageSelection(optionKey)
}} }}
onKeyDown={(event) => { onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
handleLanguageSelection(optionKey) handleLanguageSelection(optionKey)
} }
}} }}
href="#"
> >
{t(`code_sample_options.${optionKey}`)} {t(`code_sample_options.${optionKey}`)}
</TabNav.Link> </SegmentedControl.Button>
))} ))}
</TabNav> </SegmentedControl>
</div> </div>
<div className="mr-2"> <div className="mr-2">
<IconButton <IconButton
@@ -316,31 +318,30 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
__html: displayedExample.response.description || t('response'), __html: displayedExample.response.description || t('response'),
}} }}
></h4> ></h4>
<div className="border rounded-1"> <div className="border rounded-1 pt-2">
{displayedExample.response.schema ? ( {displayedExample.response.schema ? (
<TabNav <SegmentedControl
className="pt-2 mx-2" className={cx(styles.segmentedControl, 'mx-2')}
aria-label={`Example response format selector for ${operation.title}`} aria-label={`Example response format selector for ${operation.title}`}
> >
{responseSelectOptions.map((optionKey) => ( {responseSelectOptions.map((optionKey) => (
<TabNav.Link <SegmentedControl.Button
key={optionKey} key={optionKey}
selected={optionKey === selectedResponse} selected={optionKey === selectedResponse}
onClick={(e) => { onClick={(e: React.MouseEvent) => {
e.preventDefault() e.preventDefault()
handleResponseSelection(optionKey) handleResponseSelection(optionKey)
}} }}
onKeyDown={(event) => { onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
handleResponseSelection(optionKey) handleResponseSelection(optionKey)
} }
}} }}
href="#"
> >
{t(`response_options.${optionKey}`)} {t(`response_options.${optionKey}`)}
</TabNav.Link> </SegmentedControl.Button>
))} ))}
</TabNav> </SegmentedControl>
) : null} ) : null}
<div className=""> <div className="">
{/* Status code */} {/* Status code */}

View File

@@ -30,6 +30,7 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
ul, ul,
ol { ol {
list-style-type: decimal !important; list-style-type: decimal !important;
padding-inline-start: 2em !important;
} }
} }
@@ -39,7 +40,7 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
} }
.referencesList { .referencesList {
padding-left: 16px !important; margin-top: 12px !important;
} }
.loadingContainer { .loadingContainer {

View File

@@ -500,9 +500,6 @@ export function AskAIResults({
const refIndex = index + referencesIndexOffset const refIndex = index + referencesIndexOffset
return ( return (
<ActionList.Item <ActionList.Item
sx={{
marginLeft: '0px',
}}
key={`reference-${index}`} key={`reference-${index}`}
id={`search-option-reference-${index + referencesIndexOffset}`} id={`search-option-reference-${index + referencesIndexOffset}`}
tabIndex={-1} tabIndex={-1}

View File

@@ -111,10 +111,11 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
} }
.viewAllSearchResults { .viewAllSearchResults {
color: var(--color-accent-emphasis) !important; button {
padding-left: 32px !important; span {
span { color: var(--color-accent-emphasis) !important;
font-weight: 500 !important; font-weight: 500 !important;
}
} }
} }

View File

@@ -29,7 +29,7 @@ import {
executeGeneralSearch, executeGeneralSearch,
GENERAL_SEARCH_CONTEXT, GENERAL_SEARCH_CONTEXT,
} from '../helpers/execute-search-actions' } from '../helpers/execute-search-actions'
import { Banner } from '@primer/react/drafts' import { Banner } from '@primer/react/experimental'
import { useCombinedSearchResults } from '@/search/components/hooks/useAISearchAutocomplete' import { useCombinedSearchResults } from '@/search/components/hooks/useAISearchAutocomplete'
import { AskAIResults } from './AskAIResults' import { AskAIResults } from './AskAIResults'
import { sendEvent, uuidv4 } from '@/events/components/events' import { sendEvent, uuidv4 } from '@/events/components/events'
@@ -948,8 +948,11 @@ function renderSearchGroups(
} }
}} }}
> >
{!option.isViewAllResults && !option.isNoResultsFound && ( {!option.isNoResultsFound && (
<ActionList.LeadingVisual aria-hidden> <ActionList.LeadingVisual
aria-hidden
sx={{ visibility: option.isViewAllResults ? 'hidden' : 'visible' }}
>
<FileIcon /> <FileIcon />
</ActionList.LeadingVisual> </ActionList.LeadingVisual>
)} )}

View File

@@ -0,0 +1,3 @@
.aggregations label {
font-weight: var(--base-text-weight-semibold, 600);
}

View File

@@ -5,6 +5,7 @@ import Link from 'next/link'
import { useTranslation } from '@/languages/components/useTranslation' import { useTranslation } from '@/languages/components/useTranslation'
import type { SearchResultAggregations } from '@/search/types' import type { SearchResultAggregations } from '@/search/types'
import styles from './Aggregations.module.scss'
type Props = { type Props = {
aggregations: SearchResultAggregations aggregations: SearchResultAggregations
@@ -46,7 +47,7 @@ export function SearchResultsAggregations({ aggregations }: Props) {
if (aggregations.toplevel && aggregations.toplevel.length > 0) { if (aggregations.toplevel && aggregations.toplevel.length > 0) {
return ( return (
<div> <div className={styles.aggregations}>
<CheckboxGroup> <CheckboxGroup>
<CheckboxGroup.Label> <CheckboxGroup.Label>
{t('filter')}{' '} {t('filter')}{' '}

View File

@@ -1,8 +1,7 @@
import { ReactNode } from 'react'
import { ActionList } from '@primer/react' import { ActionList } from '@primer/react'
import { ReactNode } from 'react'
import { PickerItem } from './Picker' import { PickerItem } from './Picker'
import { Link } from '@/frame/components/Link'
import styles from './Fields.module.scss' import styles from './Fields.module.scss'
@@ -21,11 +20,11 @@ export const Fields = (fieldProps: {
item.divider ? ( item.divider ? (
<ActionList.Divider key={`divider${i}`} /> <ActionList.Divider key={`divider${i}`} />
) : ( ) : (
<ActionList.Item <ActionList.LinkItem
as={Link} as="a"
key={item.text} key={item.text}
href={item.href} href={item.href}
selected={item.selected === true} active={item.selected === true}
onSelect={() => { onSelect={() => {
if (onSelect) onSelect(item) if (onSelect) onSelect(item)
setOpen(!open) setOpen(!open)
@@ -45,7 +44,7 @@ export const Fields = (fieldProps: {
role={item.extra?.arrow || item.extra?.info ? 'menuitem' : 'menuitemradio'} role={item.extra?.arrow || item.extra?.info ? 'menuitem' : 'menuitemradio'}
> >
{renderItem ? renderItem(item) : item.text} {renderItem ? renderItem(item) : item.text}
</ActionList.Item> </ActionList.LinkItem>
), ),
)} )}
</ActionList> </ActionList>

View File

@@ -1,9 +1,9 @@
import fs from 'fs/promises' import fs from 'fs'
import semver from 'semver' import semver from 'semver'
import versionSatisfiesRange from './version-satisfies-range' import versionSatisfiesRange from './version-satisfies-range'
const rawDates = JSON.parse(await fs.readFile('src/ghes-releases/lib/enterprise-dates.json')) const rawDates = JSON.parse(fs.readFileSync('src/ghes-releases/lib/enterprise-dates.json', 'utf8'))
// ============================================================================ // ============================================================================
// STATICALLY DEFINED VALUES // STATICALLY DEFINED VALUES