diff --git a/components/Breadcrumbs.tsx b/components/Breadcrumbs.tsx
index dad6685c5df5..90a59421c7ea 100644
--- a/components/Breadcrumbs.tsx
+++ b/components/Breadcrumbs.tsx
@@ -51,7 +51,11 @@ export const Breadcrumbs = ({ variant = 'default' }: Props) => {
{breadcrumb.title}
),
- i !== arr.length - 1 ? / : null,
+ i !== arr.length - 1 ? (
+
+ /
+
+ ) : null,
]
})}
diff --git a/components/DefaultLayout.tsx b/components/DefaultLayout.tsx
index aa5a702dd3a9..4869cf0d9c1b 100644
--- a/components/DefaultLayout.tsx
+++ b/components/DefaultLayout.tsx
@@ -1,6 +1,6 @@
import Head from 'next/head'
-import { SidebarNav } from 'components/SidebarNav'
+import { SidebarNav } from 'components/sidebar/SidebarNav'
import { Header } from 'components/page-header/Header'
import { SmallFooter } from 'components/page-footer/SmallFooter'
import { ScrollButton } from 'components/ScrollButton'
diff --git a/components/SidebarNav.tsx b/components/SidebarNav.tsx
deleted file mode 100644
index 38733ec1ea09..000000000000
--- a/components/SidebarNav.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useRouter } from 'next/router'
-import { LinkExternalIcon, MarkGithubIcon } from '@primer/octicons-react'
-
-import { Link } from 'components/Link'
-import { useTranslation } from './hooks/useTranslation'
-import { useMainContext } from './context/MainContext'
-import { SidebarProduct } from './product/SidebarProduct'
-import { AllProductsLink } from './product/AllProductsLink'
-import { useVersion } from './hooks/useVersion'
-
-export const SidebarNav = () => {
- const router = useRouter()
- const { error, relativePath, isFPT } = useMainContext()
- const { t } = useTranslation('header')
-
- return (
-
-
-
-
-
-
- {t('github_docs')}
-
-
-
-
-
-
- )
-}
-
-const SidebarHomepage = () => {
- const router = useRouter()
- const { currentVersion } = useVersion()
- const { activeProducts, isFPT } = useMainContext()
-
- return (
- <>
- {activeProducts.map((product) => {
- if (!isFPT && !product.versions?.includes(currentVersion) && !product.external) {
- return null
- }
-
- const href = `${!product.external ? `/${router.locale}` : ''}${
- product.versions?.includes(currentVersion) && !isFPT
- ? `/${currentVersion}/${product.id}`
- : product.href
- }`
-
- return (
-
-
- {product.name}
- {product.external && (
-
-
-
- )}
-
-
- )
- })}
- >
- )
-}
diff --git a/components/lib/events.ts b/components/lib/events.ts
index ccdc0045c45a..3fe9dbee39d4 100644
--- a/components/lib/events.ts
+++ b/components/lib/events.ts
@@ -207,38 +207,11 @@ function initExitEvent() {
document.addEventListener('visibilitychange', sendExit)
}
-function initNavigateEvent() {
- if (!document.querySelector('.sidebar-products')) return
-
- Array.from(document.querySelectorAll('.sidebar-products details')).forEach((details) =>
- details.addEventListener('toggle', (evt) => {
- const target = evt.target as HTMLDetailsElement
- sendEvent({
- type: EventType.navigate,
- navigate_label: `details ${target.open ? 'open' : 'close'}: ${
- target?.querySelector('summary')?.innerText
- }`,
- })
- })
- )
-
- document.querySelector('.sidebar-products')?.addEventListener('click', (evt) => {
- const target = evt.target as HTMLElement
- const link = target.closest('a') as HTMLAnchorElement
- if (!link) return
- sendEvent({
- type: EventType.navigate,
- navigate_label: `link: ${link.href}`,
- })
- })
-}
-
export default function initializeEvents() {
initPageEvent() // must come first
initExitEvent()
initLinkEvent()
initClipboardEvent()
- initNavigateEvent()
// print event in ./print.js
// survey event in ./survey.js
// experiment event in ./experiment.js
diff --git a/components/product/AllProductsLink.tsx b/components/sidebar/AllProductsLink.tsx
similarity index 100%
rename from components/product/AllProductsLink.tsx
rename to components/sidebar/AllProductsLink.tsx
diff --git a/components/sidebar/SidebarHomepage.tsx b/components/sidebar/SidebarHomepage.tsx
new file mode 100644
index 000000000000..ecc55ba69e87
--- /dev/null
+++ b/components/sidebar/SidebarHomepage.tsx
@@ -0,0 +1,53 @@
+import { useRouter } from 'next/router'
+import { LinkExternalIcon } from '@primer/octicons-react'
+
+import { useVersion } from 'components/hooks/useVersion'
+import { useMainContext } from 'components/context/MainContext'
+import { Link } from 'components/Link'
+
+import { AllProductsLink } from './AllProductsLink'
+
+export const SidebarHomepage = () => {
+ const router = useRouter()
+ const { currentVersion } = useVersion()
+ const { activeProducts, isFPT } = useMainContext()
+
+ return (
+
+ {!isFPT && }
+
+ {activeProducts.map((product) => {
+ if (!isFPT && !product.versions?.includes(currentVersion) && !product.external) {
+ return null
+ }
+
+ const href = `${!product.external ? `/${router.locale}` : ''}${
+ product.versions?.includes(currentVersion) && !isFPT
+ ? `/${currentVersion}/${product.id}`
+ : product.href
+ }`
+
+ return (
+ -
+
+ {product.name}
+ {product.external && (
+
+
+
+ )}
+
+
+ )
+ })}
+
+ )
+}
diff --git a/components/sidebar/SidebarNav.tsx b/components/sidebar/SidebarNav.tsx
new file mode 100644
index 000000000000..0f6962c9fca0
--- /dev/null
+++ b/components/sidebar/SidebarNav.tsx
@@ -0,0 +1,46 @@
+import { useRouter } from 'next/router'
+import { MarkGithubIcon } from '@primer/octicons-react'
+
+import { Link } from 'components/Link'
+import { useTranslation } from 'components/hooks/useTranslation'
+import { useMainContext } from 'components/context/MainContext'
+import { SidebarProduct } from './SidebarProduct'
+import { SidebarHomepage } from './SidebarHomepage'
+
+export const SidebarNav = () => {
+ const router = useRouter()
+ const { error, relativePath } = useMainContext()
+ const { t } = useTranslation('header')
+
+ return (
+
+
+
+
+
+
+ {t('github_docs')}
+
+
+
+
+ )
+}
diff --git a/components/sidebar/SidebarProduct.module.scss b/components/sidebar/SidebarProduct.module.scss
new file mode 100644
index 000000000000..49c8f812849f
--- /dev/null
+++ b/components/sidebar/SidebarProduct.module.scss
@@ -0,0 +1,13 @@
+.sidebarArticle::before {
+ content: "";
+ position: absolute;
+ left: 26px;
+ height: 100%;
+ border-left: 1px solid var(--color-text-primary);
+ width: 1px;
+ top: 0;
+}
+
+.sidebarArticleActive::before {
+ border-left-width: 2px;
+}
diff --git a/components/product/SidebarProduct.tsx b/components/sidebar/SidebarProduct.tsx
similarity index 53%
rename from components/product/SidebarProduct.tsx
rename to components/sidebar/SidebarProduct.tsx
index cab9cbbf0c32..ae80a37f5a5e 100644
--- a/components/product/SidebarProduct.tsx
+++ b/components/sidebar/SidebarProduct.tsx
@@ -1,17 +1,21 @@
import { useRouter } from 'next/router'
import cx from 'classnames'
-import { useState, useEffect } from 'react'
+import { useState, useEffect, SyntheticEvent } from 'react'
import { ChevronDownIcon } from '@primer/octicons-react'
import { Link } from 'components/Link'
import { ProductTreeNode, useMainContext } from 'components/context/MainContext'
-import { AllProductsLink } from 'components/product/AllProductsLink'
+import { AllProductsLink } from 'components/sidebar/AllProductsLink'
+import { EventType, sendEvent } from 'components/lib/events'
+
+import styles from './SidebarProduct.module.scss'
export const SidebarProduct = () => {
const router = useRouter()
const { currentProductTree } = useMainContext()
+
useEffect(() => {
- const activeArticle = document.querySelector('.sidebar-article.active')
+ const activeArticle = document.querySelector('[data-is-current-page=true]')
// Setting to the top doesn't give enough context of surrounding categories
activeArticle?.scrollIntoView({ block: 'center' })
// scrollIntoView affects some articles that are very low in the sidebar
@@ -26,16 +30,16 @@ export const SidebarProduct = () => {
const productTitle = currentProductTree.renderedShortTitle || currentProductTree.renderedFullTitle
const routePath = `/${router.locale}${router.asPath.split('?')[0]}` // remove query string
- const hasExactCategory = currentProductTree.childPages.find(({ href }) =>
+ const hasExactCategory = !!currentProductTree.childPages.find(({ href }) =>
routePath.includes(href)
)
return (
- <>
+
{!currentProductTree.page.hidden && (
<>
- -
+
-
{
- -
-
+ -
+
{currentProductTree.childPages.map((childPage, i) => {
const isStandaloneCategory = childPage.page.documentType === 'article'
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
- const isActive = routePath.includes(`${childPage.href}/`)
- const isCurrent = routePath === childPage.href
- const defaultOpen = hasExactCategory ? isActive || isCurrent : false
+ const isActive = routePath.includes(childPage.href) || routePath === childPage.href
+ const defaultOpen = hasExactCategory ? isActive : false
return (
-
{isStandaloneCategory ? (
{
>
)}
- >
+
)
}
@@ -100,8 +100,46 @@ const CollapsibleSection = (props: SectionProps) => {
const { routePath, defaultOpen, title, page } = props
const [isOpen, setIsOpen] = useState(defaultOpen)
+ const onToggle = (e: SyntheticEvent) => {
+ const newIsOpen = (e.target as HTMLDetailsElement).open
+ setIsOpen(newIsOpen)
+ sendEvent({
+ type: EventType.navigate,
+ navigate_label: `details ${newIsOpen ? 'open' : 'close'}: ${title}`,
+ })
+ }
+
+ // The lowest level page link displayed in the tree
+ const renderTerminalPageLink = (page: ProductTreeNode) => {
+ const title = page.renderedShortTitle || page.renderedFullTitle
+
+ const isCurrent = routePath === page.href
+ return (
+ -
+
+ {title}
+
+
+ )
+ }
+
return (
- setIsOpen((e.target as HTMLDetailsElement).open)}>
+
@@ -115,9 +153,9 @@ const CollapsibleSection = (props: SectionProps) => {
{
<>
- {/* */}
+ {/* */}
{page.childPages[0]?.page.documentType === 'mapTopic' ? (
-
+
{page.childPages.map((childPage, i) => {
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
@@ -125,42 +163,17 @@ const CollapsibleSection = (props: SectionProps) => {
const isCurrent = routePath === childPage.href
return (
- -
-
e.stopPropagation()}>
+ -
+
e.stopPropagation()}
+ className="details-reset"
+ >
{childTitle}
-
- {childPage.childPages.map((grandchildPage, i, arr) => {
- const grandchildTitle =
- grandchildPage.renderedShortTitle || grandchildPage.renderedFullTitle
- const isLast = i === arr.length - 1
- const isActive = routePath === grandchildPage.href
- return (
- -
-
- {grandchildTitle}
-
-
- )
- })}
+
+ {childPage.childPages.map(renderTerminalPageLink)}
@@ -168,36 +181,9 @@ const CollapsibleSection = (props: SectionProps) => {
})}
) : page.childPages[0]?.page.documentType === 'article' ? (
-
+
{/* */}
- {page.childPages.map((childPage, i, arr) => {
- const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
- const isLast = i === arr.length - 1
-
- const isActive = routePath.includes(childPage.href)
- const isCurrent = routePath === childPage.href
- return (
- -
-
- {childTitle}
-
-
- )
- })}
+ {page.childPages.map(renderTerminalPageLink)}
) : null}
>
diff --git a/stylesheets/images.scss b/stylesheets/images.scss
index b5e786219d29..8208e0358224 100644
--- a/stylesheets/images.scss
+++ b/stylesheets/images.scss
@@ -1,4 +1,4 @@
-.procedural-image-wrapper {
+.markdown-body .procedural-image-wrapper {
display: block;
padding: 10px 0;
margin: 20px auto 0 auto;
@@ -6,7 +6,7 @@
max-width: calc(100% - 32px);
}
-.procedural-image-wrapper img {
+.markdown-body .procedural-image-wrapper img {
border-radius: 5px;
border: 2px solid var(--color-auto-gray-2);
width: auto;
@@ -16,11 +16,10 @@
}
// make sure images that contain emoji render at the expected size
-img[src*="https://github.githubassets.com/images/icons/emoji"]
+.markdown-body img[src*="https://github.githubassets.com/images/icons/emoji"]
{
- height: 20;
- width: 20;
- align: absmiddle;
+ height: 20px;
+ width: 20px;
}
.markdown-body img {
diff --git a/stylesheets/index.scss b/stylesheets/index.scss
index 67fd2d7f12f6..2291c3181c8f 100644
--- a/stylesheets/index.scss
+++ b/stylesheets/index.scss
@@ -22,7 +22,6 @@ $marketing-font-path: "/assets/fonts/inter/";
@import "release-notes.scss";
@import "search.scss";
@import "shadows.scss";
-@import "sidebar.scss";
@import "summary.scss";
@import "syntax-highlighting.scss";
@import "tables.scss";
diff --git a/stylesheets/sidebar.scss b/stylesheets/sidebar.scss
deleted file mode 100644
index 723334cef72e..000000000000
--- a/stylesheets/sidebar.scss
+++ /dev/null
@@ -1,97 +0,0 @@
-.sidebar {
- width: 260px;
- position: sticky;
- top: 0;
- padding-bottom: $spacer-5;
- overflow-y: auto;
- height: 100vh;
- flex-shrink: 0;
-
- @include breakpoint(xl) {
- width: 280px;
- }
-}
-
-.sidebar-products > li {
- margin: 4px 0;
-}
-
-.sidebar-products a,
-.sidebar-products .arrow {
- text-decoration: none;
- display: block;
- line-height: 1.4;
-}
-
-.sidebar-category,
-.sidebar-product {
- > a,
- summary a {
- opacity: 0.8;
- }
-}
-
-.sidebar-category.active {
- background-color: rgba(#959da5, 0.1); // --color-auto-gray-4
- // We can't do rgba(var(--color-auto-gray-4, 0.1) quite yet
- // as the browsers are still working on supporting that combination
-}
-
-.sidebar-article {
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: $spacer-4;
- height: 100%;
- border-left: 1px solid var(--color-text-primary);
- width: 1px;
- top: 0;
- }
-
- &.active {
- &::before {
- border-left: 2px solid var(--color-text-primary);
- }
- }
-}
-
-.is-current-page > a {
- font-weight: bolder;
-}
-
-// only display child lists of active elements in sidebar
-.sidebar-product.active > ul,
-.sidebar-category.active > ul,
-.sidebar-maptopic.active > ul {
- display: block;
-}
-
-.sidebar-category {
- > ul {
- display: none;
- }
-}
-
-.sidebar-maptopic > ul {
- display: none;
-}
-
-.sidebar-maptopic > details > summary > div,
-.sidebar-article > a {
- color: var(--color-text-secondary);
-}
-
-.sidebar-maptopic > details > summary > div:hover,
-.sidebar-article > a:hover {
- color: var(--color-text-primary);
- transition: color 0.2s ease;
-}
-
-.sidebar-category summary {
- list-style: none;
-}
-summary::-webkit-details-marker {
- display: none;
-}
diff --git a/tests/browser/browser.js b/tests/browser/browser.js
index 2bf8e5347ed1..3b1740bf49cf 100644
--- a/tests/browser/browser.js
+++ b/tests/browser/browser.js
@@ -462,7 +462,9 @@ describe.skip('next/link client-side navigation', () => {
response.url().startsWith('http://localhost:4001/_next/data')
),
page.waitForNavigation({ waitUntil: 'networkidle2' }),
- page.click('.sidebar-articles:nth-child(2) .sidebar-article:nth-child(1) a'),
+ page.click(
+ '[data-testid=sidebar-article-group]:nth-child(2) [data-testid=sidebar-article]:nth-child(1) a'
+ ),
])
expect(response.status()).toBe(200)
diff --git a/tests/rendering/server.js b/tests/rendering/server.js
index 541e59ad7213..9fa842f9a24f 100644
--- a/tests/rendering/server.js
+++ b/tests/rendering/server.js
@@ -38,7 +38,7 @@ describe('server', () => {
test('renders the homepage with links to exptected products in both the sidebar and page body', async () => {
const $ = await getDOM('/en')
- const sidebarItems = $('.sidebar-products li a').get()
+ const sidebarItems = $('[data-testid=sidebar] li a').get()
const sidebarTitles = sidebarItems.map((el) => $(el).text().trim())
const sidebarHrefs = sidebarItems.map((el) => $(el).attr('href'))
@@ -73,7 +73,7 @@ describe('server', () => {
test('renders the Enterprise homepage with links to exptected products in both the sidebar and page body', async () => {
const $ = await getDOM(`/en/enterprise-server@${enterpriseServerReleases.latest}`)
- const sidebarItems = $('.sidebar-products li a').get()
+ const sidebarItems = $('[data-testid=sidebar] li a').get()
const sidebarTitles = sidebarItems.map((el) => $(el).text().trim())
const sidebarHrefs = sidebarItems.map((el) => $(el).attr('href'))
diff --git a/tests/rendering/sidebar.js b/tests/rendering/sidebar.js
index 6e45c1d20528..2953fb6fc39f 100644
--- a/tests/rendering/sidebar.js
+++ b/tests/rendering/sidebar.js
@@ -15,34 +15,45 @@ describe('sidebar', () => {
})
test('highlights active product on Enterprise pages', async () => {
- expect($enterprisePage('.sidebar-products li.sidebar-product').length).toBe(1)
- expect($enterprisePage('.sidebar-products li.sidebar-product > a').text().trim()).toBe(
- 'Enterprise administrators'
- )
+ expect($enterprisePage('[data-testid=sidebar] [data-testid=sidebar-product]').length).toBe(1)
+ expect(
+ $enterprisePage('[data-testid=sidebar] [data-testid=sidebar-product] > a').text().trim()
+ ).toBe('Enterprise administrators')
})
test('highlights active product on GitHub pages', async () => {
- expect($githubPage('.sidebar-products li.sidebar-product').length).toBe(1)
- expect($githubPage('.sidebar-products li.sidebar-product > a').text().trim()).toBe('GitHub')
+ expect($githubPage('[data-testid=sidebar] [data-testid=sidebar-product]').length).toBe(1)
+ expect(
+ $githubPage('[data-testid=sidebar] [data-testid=sidebar-product] > a').text().trim()
+ ).toBe('GitHub')
})
test('includes links to external products like the CLI, Atom, Electron, and CodeQL', async () => {
- expect($homePage('.sidebar-products a[href="https://cli.github.com/manual"]')).toHaveLength(1)
- expect($homePage('.sidebar-products a[href="https://atom.io/docs"]')).toHaveLength(1)
- expect($homePage('.sidebar-products a[href="https://electronjs.org/docs"]')).toHaveLength(1)
- expect($homePage('.sidebar-products a[href="https://codeql.github.com/docs"]')).toHaveLength(1)
+ expect($homePage('[data-testid=sidebar] a[href="https://cli.github.com/manual"]')).toHaveLength(
+ 1
+ )
+ expect($homePage('[data-testid=sidebar] a[href="https://atom.io/docs"]')).toHaveLength(1)
+ expect($homePage('[data-testid=sidebar] a[href="https://electronjs.org/docs"]')).toHaveLength(1)
+ expect(
+ $homePage('[data-testid=sidebar] a[href="https://codeql.github.com/docs"]')
+ ).toHaveLength(1)
})
- test('adds an `is-current-page` class to the sidebar link to the current page', async () => {
+ test('adds `data-is-current-page` and `data-is-active-category` properties to the sidebar link for the current page', async () => {
const url =
'/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings'
const $ = await getDOM(url)
- expect($('.sidebar-products .is-current-page').length).toBe(1)
- expect($('.sidebar-products .is-current-page a').attr('href')).toContain(url)
+ expect($('[data-testid=sidebar] [data-is-active-category=true]').length).toBe(1)
+ expect($('[data-testid=sidebar] [data-is-current-page=true]').length).toBe(1)
+ expect($('[data-testid=sidebar] [data-is-current-page=true] a').attr('href')).toContain(url)
})
test('does not display Early Access as a product', async () => {
- expect($homePage('.sidebar-products li.sidebar-product[title*="Early"]').length).toBe(0)
- expect($homePage('.sidebar-products li.sidebar-product[title*="early"]').length).toBe(0)
+ expect(
+ $homePage('[data-testid=sidebar] [data-testid=sidebar-product][title*="Early"]').length
+ ).toBe(0)
+ expect(
+ $homePage('[data-testid=sidebar] [data-testid=sidebar-product][title*="early"]').length
+ ).toBe(0)
})
})