Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions data/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ header:
menu: Menu
open_menu_label: Open menu
go_home: Home
collapse_sidebar: Collapse sidebar
expand_sidebar: Expand sidebar
scroll_breadcrumbs_left: Scroll breadcrumbs left
scroll_breadcrumbs_right: Scroll breadcrumbs right

picker:
language_picker_label: Language
Expand Down
11 changes: 1 addition & 10 deletions src/automated-pipelines/components/AutomatedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ArticleGridLayout } from '@/frame/components/article/ArticleGridLayout'
import { ArticleInlineLayout } from '@/frame/components/article/ArticleInlineLayout'
import { MiniTocs } from '@/frame/components/ui/MiniTocs'
import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext'
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'

type Props = {
Expand Down Expand Up @@ -70,7 +69,6 @@ export const AutomatedPage = ({ children, rawChildren, fullWidth }: Props) => {
topper={<ArticleTitle>{title}</ArticleTitle>}
intro={introProp}
toc={toc}
breadcrumbs={<Breadcrumbs />}
>
{articleContents}
</ArticleInlineLayout>
Expand All @@ -84,14 +82,7 @@ export const AutomatedPage = ({ children, rawChildren, fullWidth }: Props) => {
<div className="container-xl px-3 px-md-6 my-4">
<ArticleGridLayout
fullWidth={fullWidth}
topper={
<>
<div className="d-none d-xl-block my-3 mr-auto width-full">
<Breadcrumbs />
</div>
<ArticleTitle>{title}</ArticleTitle>
</>
}
topper={<ArticleTitle>{title}</ArticleTitle>}
intro={introProp}
toc={toc}
>
Expand Down
4 changes: 4 additions & 0 deletions src/fixtures/fixtures/data/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ header:
menu: Menu
open_menu_label: Open menu
go_home: Home
collapse_sidebar: Collapse sidebar
expand_sidebar: Expand sidebar
scroll_breadcrumbs_left: Scroll breadcrumbs left
scroll_breadcrumbs_right: Scroll breadcrumbs right

picker:
language_picker_label: Language
Expand Down
70 changes: 40 additions & 30 deletions src/fixtures/tests/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,70 +7,80 @@ import { getDOM } from '@/tests/helpers/e2etest'
describe('breadcrumbs', () => {
test('links always prefixed with language', async () => {
const $ = await getDOM('/get-started/start-your-journey/hello-world')
const links = $('[data-testid=breadcrumbs-in-article] a')
const links = $('[data-testid=breadcrumbs-bar] a')
links.each((i, element) => {
expect($(element).attr('href')!.startsWith('/en/')).toBe(true)
const href = $(element).attr('href')!
// The Home crumb points at the locale root (`/en` on the default version,
// no trailing slash); every other crumb is under `/en/…`. Both are
// language-prefixed, which is what this test guards.
expect(href === '/en' || href.startsWith('/en/')).toBe(true)
})
expect.assertions(3)
// Home crumb + the three trail crumbs for this path.
expect.assertions(4)
})

test('top-level hidden /search page has no breadcrumbs', async () => {
const $ = await getDOM('/search')
const links = $('[data-testid=breadcrumbs-in-article] a')
const links = $('[data-testid=breadcrumbs-bar] a')
expect(links.length).toBe(0)
const headers = $('[data-testid=breadcrumbs-header]')
expect(headers.length).toBe(0)
})

test('short titles are preferred', async () => {
const $ = await getDOM('/get-started/foo/bar')
const links = $('[data-testid=breadcrumbs-in-article] li:last-child a')
const links = $('[data-testid=breadcrumbs-bar] li:last-child a')
expect(links.text()).toBe('Bar')
})

test('article pages have breadcrumbs in article with product, category, subcategory, and article and last breadcrumb is not viewable', async () => {
test('article pages have breadcrumbs in the secondary bar with home, product, category, subcategory, and article (all shown)', async () => {
const $ = await getDOM('/get-started/start-your-journey/hello-world')
const links = $('[data-testid=breadcrumbs-in-article] a')
expect(links.length).toBe(3)
expect($(links[0]).text()).toBe('Get started')
expect($(links[0]).attr('class')!.includes('d-none')).toBe(false)
expect($(links[1]).text()).toBe('Start your journey')
const links = $('[data-testid=breadcrumbs-bar] a')
// The secondary bar leads with a Home crumb, then the page trail.
expect(links.length).toBe(4)
expect($(links[0]).text()).toBe('Home')
expect($(links[1]).text()).toBe('Get started')
expect($(links[1]).attr('class')!.includes('d-none')).toBe(false)
expect($(links[2]).text()).toBe('Hello World')
expect($(links[2]).attr('class')!.includes('d-none')).toBe(true)
expect($(links[2]).text()).toBe('Start your journey')
expect($(links[2]).attr('class')!.includes('d-none')).toBe(false)
expect($(links[3]).text()).toBe('Hello World')
// The secondary-bar variant shows the full trail (no hidden last crumb).
expect($(links[3]).attr('class')!.includes('d-none')).toBe(false)
})

test('works for enterprise-server articles too', async () => {
const $ = await getDOM('/enterprise-server@latest/get-started/start-your-journey/hello-world')
const links = $('[data-testid=breadcrumbs-in-article] a')
expect(links.length).toBe(3)
expect($(links[0]).text()).toBe('Get started')
expect($(links[1]).text()).toBe('Start your journey')
expect($(links[2]).text()).toBe('Hello World')
const links = $('[data-testid=breadcrumbs-bar] a')
expect(links.length).toBe(4)
expect($(links[0]).text()).toBe('Home')
expect($(links[1]).text()).toBe('Get started')
expect($(links[2]).text()).toBe('Start your journey')
expect($(links[3]).text()).toBe('Hello World')
})

test('works for titles that depend on Liquid', async () => {
const $fpt = await getDOM('/get-started/start-your-journey/dynamic-title')
const fptLinks = $fpt('[data-testid=breadcrumbs-in-article] a')
expect($fpt(fptLinks[2]).text()).toBe('Hello HubGit')
const fptLinks = $fpt('[data-testid=breadcrumbs-bar] a')
// [0] is the Home crumb; the article is the last crumb.
expect($fpt(fptLinks[3]).text()).toBe('Hello HubGit')

const $ghec = await getDOM(
'/enterprise-cloud@latest/get-started/start-your-journey/dynamic-title',
)
const ghecLinks = $ghec('[data-testid=breadcrumbs-in-article] a')
expect($ghec(ghecLinks[2]).text()).toBe('Greetings HubGit Enterprise Cloud')
const ghecLinks = $ghec('[data-testid=breadcrumbs-bar] a')
expect($ghec(ghecLinks[3]).text()).toBe('Greetings HubGit Enterprise Cloud')
})

test('early access article pages have breadcrumbs with product, category, and article', async () => {
test('early access article pages have breadcrumbs with home, product, category, and article', async () => {
const $ = await getDOM('/early-access/secrets/deeper/mariana-trench')
const $breadcrumbTitles = $(
'[data-testid=breadcrumbs-in-article] [data-testid=breadcrumb-title]',
)
const $breadcrumbLinks = $('[data-testid=breadcrumbs-in-article] a')
const $breadcrumbTitles = $('[data-testid=breadcrumbs-bar] [data-testid=breadcrumb-title]')
const $breadcrumbLinks = $('[data-testid=breadcrumbs-bar] a')

expect($breadcrumbTitles.length).toBe(0)
expect($breadcrumbLinks.length).toBe(2)
expect(($breadcrumbLinks[0] as Element).attribs.title).toBe('Deeper secrets')
expect(($breadcrumbLinks[1] as Element).attribs.title).toBe('Mariana Trench')
// Home crumb + the two early-access crumbs.
expect($breadcrumbLinks.length).toBe(3)
expect(($breadcrumbLinks[0] as Element).attribs.title).toBe('Home')
expect(($breadcrumbLinks[1] as Element).attribs.title).toBe('Deeper secrets')
expect(($breadcrumbLinks[2] as Element).attribs.title).toBe('Mariana Trench')
})
})
64 changes: 35 additions & 29 deletions src/fixtures/tests/playwright-rendering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,21 +540,21 @@ test.describe('test nav at different viewports', () => {
})
await page.goto('/get-started/foo/bar')

// in article breadcrumbs at our custom xl viewport should remove last
// breadcrumb so for this page we should only have 'Get Started / Foo'
expect(await page.getByTestId('breadcrumbs-in-article').getByRole('link').all()).toHaveLength(2)
await expect(page.getByTestId('breadcrumbs-in-article').getByText('Foo')).toBeVisible()
await expect(page.getByTestId('breadcrumbs-in-article').getByText('Bar')).not.toBeVisible()
// The Docs 2026 secondary bar leads with a Home crumb, then the full trail
// 'Get started / Foo / Bar' (no hidden last crumb).
expect(await page.getByTestId('breadcrumbs-bar').getByRole('link').all()).toHaveLength(4)
await expect(page.getByTestId('breadcrumbs-bar').getByText('Foo')).toBeVisible()
await expect(page.getByTestId('breadcrumbs-bar').getByText('Bar')).toBeVisible()

// breadcrumbs show up in rest reference pages
await page.goto('/rest/actions/artifacts')
await expect(page.getByTestId('breadcrumbs-in-article')).toBeVisible()
await expect(page.getByTestId('breadcrumbs-bar')).toBeVisible()

// breadcrumbs show up in one of the pages that use the AutomatedPage
// component (e.g. graphql, audit log, etc.) -- we test the webhooks
// reference page here
await page.goto('/webhooks/webhook-events-and-payloads')
await expect(page.getByTestId('breadcrumbs-in-article')).toBeVisible()
await expect(page.getByTestId('breadcrumbs-bar')).toBeVisible()
})

test('large -> x-large viewports - 1012+', async ({ page }) => {
Expand Down Expand Up @@ -584,17 +584,15 @@ test.describe('test nav at different viewports', () => {
})
await page.goto('/get-started/foo/bar')

// breadcrumbs show up in the header, for this page we should have
// 3 items 'Get Started / Foo / Bar'
// in-article breadcrumbs don't show up
await expect(page.getByTestId('breadcrumbs-header')).toBeVisible()
expect(await page.getByTestId('breadcrumbs-header').getByRole('link').all()).toHaveLength(3)
await expect(page.getByTestId('breadcrumbs-in-article')).not.toBeVisible()
// breadcrumbs show up in the secondary bar; for this page we should have
// a Home crumb plus 'Get started / Foo / Bar'
await expect(page.getByTestId('breadcrumbs-bar')).toBeVisible()
expect(await page.getByTestId('breadcrumbs-bar').getByRole('link').all()).toHaveLength(4)

// hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click()
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
// the mobile nav toggle is visible and expands the doc-tree nav inline
await expect(page.getByTestId('sidebar-mobile-toggle')).toBeVisible()
await page.getByTestId('sidebar-mobile-toggle').click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})

test('medium viewports - 768-1011', async ({ page }) => {
Expand All @@ -617,9 +615,9 @@ test.describe('test nav at different viewports', () => {
await expect(page.getByTestId('mobile-signup')).toBeVisible()

// hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click()
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
await expect(page.getByTestId('sidebar-mobile-toggle')).toBeVisible()
await page.getByTestId('sidebar-mobile-toggle').click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})

test('small viewports - 544-767', async ({ page }) => {
Expand All @@ -646,9 +644,9 @@ test.describe('test nav at different viewports', () => {
await expect(page.getByTestId('mobile-signup')).toBeVisible()

// hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click()
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
await expect(page.getByTestId('sidebar-mobile-toggle')).toBeVisible()
await page.getByTestId('sidebar-mobile-toggle').click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})

test('x-small viewports - 0-544', async ({ page }) => {
Expand Down Expand Up @@ -680,9 +678,9 @@ test.describe('test nav at different viewports', () => {
await expect(page.getByTestId('mobile-signup')).toBeVisible()

// hamburger button for sidebar overlay is visible
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
await page.getByTestId('sidebar-hamburger').click()
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
await expect(page.getByTestId('sidebar-mobile-toggle')).toBeVisible()
await page.getByTestId('sidebar-mobile-toggle').click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})

test('do a search when the viewport is x-small', async ({ page }) => {
Expand Down Expand Up @@ -881,7 +879,10 @@ test.describe('survey', () => {
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible()
await expect(page.locator('[for=survey-comment]')).toBeVisible()

await page.getByTestId('product-sidebar').getByLabel('Bar', { exact: true }).click()
await page
.getByTestId('product-sidebar')
.getByRole('link', { name: 'Bar', exact: true })
.click()
await expect(page.getByRole('button', { name: 'Send' })).not.toBeVisible()
await expect(page.locator('[for=survey-comment]')).not.toBeVisible()
})
Expand All @@ -894,8 +895,13 @@ test.describe('rest API reference pages', () => {
// URL that has that `?apiVersion=` query parameter.
await expect(page).toHaveURL(/\/en\/rest\?apiVersion=/)
await page.getByTestId('sidebar').getByText('Actions').click()
await page.getByTestId('sidebar').getByLabel('Artifacts').click()
await page.getByLabel('About artifacts in HubGit Actions').click()
// Brand NavList renders leaf articles as <a> links (not the label-associated
// controls Primer used), so locate them by link role rather than getByLabel.
await page.getByTestId('sidebar').getByRole('link', { name: 'Artifacts' }).click()
await page
.getByTestId('sidebar')
.getByRole('link', { name: 'About artifacts in HubGit Actions' })
.click()
await expect(page).toHaveURL(/\/en\/rest\/actions\/artifacts\?apiVersion=/)
await expect(page).toHaveTitle(/GitHub Actions Artifacts - GitHub Docs/)
})
Expand Down
12 changes: 6 additions & 6 deletions src/fixtures/tests/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ describe('sidebar', () => {
const sidebarProduct = $('[data-testid="sidebar-product-xl"]')
expect(sidebarProduct.text()).toBe('Get started')
expect(sidebarProduct.attr('href')).toBe('/en/get-started')
// Mobile
expect($('[data-testid="header-subnav"]').length).toBe(1)
expect($('[data-testid="header-subnav-hamburger"]').length).toBe(1)
// Docs 2026 secondary bar (breadcrumbs + nav toggle) replaces the old subnav
expect($('[data-testid="docs-secondary-bar"]').length).toBe(1)
expect($('[data-testid="sidebar-mobile-toggle"]').length).toBe(1)
})

test('REST pages get the REST sidebar', async () => {
Expand Down Expand Up @@ -73,9 +73,9 @@ describe('sidebar', () => {
const $: CheerioAPI = await getDOM('/early-access/secrets/deeper/mariana-trench')
// Deskop
expect($('[data-testid="sidebar-product-xl"]').length).toBe(0)
// Mobile
expect($('[data-testid="header-subnav"]').length).toBe(1)
expect($('[data-testid="header-subnav-hamburger"]').length).toBe(0)
// The secondary bar renders, but early-access has no nav toggle
expect($('[data-testid="docs-secondary-bar"]').length).toBe(1)
expect($('[data-testid="sidebar-mobile-toggle"]').length).toBe(0)
})

test('category-landing pages show title entry in sidebar', async () => {
Expand Down
13 changes: 13 additions & 0 deletions src/frame/components/DefaultLayout.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
@import "src/frame/stylesheets/breakpoint-xxl.scss";

.mainContent {
scroll-margin-top: 5rem;
}

// When the inline mobile/tablet nav is open, it takes over the viewport — hide
// the content column so the full-width rail isn't squeezed beside it. Above the
// xxl breakpoint the rail is a fixed-width sibling, so content always shows.
.contentHiddenForNav {
display: none;

@include breakpoint-xxl {
display: flex;
}
}
Loading
Loading